meocord 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +92 -66
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +6 -6
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +54 -32
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +4 -4
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +8 -4
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -64,50 +64,50 @@ const merges = new WeakMap();
|
|
|
64
64
|
if (!merged) byLayer.set(layer, merged = deepFreeze(mergeInto(theme, layer)));
|
|
65
65
|
return merged;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The theme state outside a call, one for both builds of this version so either reads what the other set: the defaults
|
|
69
|
+
* with what the deprecated `Theme` statics set beneath every theme, the app whose theme is read outside a call, and how
|
|
70
|
+
* an answer outside a call finds its theme.
|
|
71
|
+
*/ const state = shared('themeState', ()=>({
|
|
72
|
+
defaults: DEFAULT_THEME,
|
|
73
|
+
legacyVersion: 0,
|
|
74
|
+
ambient: undefined,
|
|
75
|
+
ambientVersion: 0,
|
|
76
|
+
themeOutsideCalls: undefined
|
|
77
|
+
}));
|
|
71
78
|
/** MeoCord's defaults, with what the deprecated `Theme` statics set over them. */ function defaultTheme() {
|
|
72
|
-
return defaults;
|
|
79
|
+
return state.defaults;
|
|
73
80
|
}
|
|
74
81
|
/** The number of times the legacy layer has changed, so a theme built on an older one is built again. */ function themeLayersVersion() {
|
|
75
|
-
return legacyVersion;
|
|
82
|
+
return state.legacyVersion;
|
|
76
83
|
}
|
|
77
84
|
/** Sets the layer the deprecated `Theme` statics write, merged over MeoCord's defaults. */ function setLegacyThemeLayer(layer) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
legacyVersion++;
|
|
85
|
+
state.defaults = mergeTheme(DEFAULT_THEME, layer === undefined ? undefined : copyLayer(layer));
|
|
86
|
+
state.legacyVersion++;
|
|
81
87
|
}
|
|
82
|
-
// The theme outside any call: the one of the app or testing module that claimed it, until it gives it up
|
|
83
|
-
let ambient;
|
|
84
|
-
let ambientVersion = 0;
|
|
85
88
|
/** The number of times the app read outside a call has changed, so each app decides again whether its calls need a scope. */ function ambientThemeVersion() {
|
|
86
|
-
return ambientVersion;
|
|
87
|
-
}
|
|
88
|
-
/** Whether some app's theme is the one read outside a call. */ function hasAmbientTheme() {
|
|
89
|
-
return ambient !== undefined;
|
|
89
|
+
return state.ambientVersion;
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Makes an app's theme the one read outside a call, when no other app has: a bot runs one app, and code it runs
|
|
93
93
|
* outside a handler, such as a scheduled job, then reads that app's theme. A second app in the same process keeps
|
|
94
94
|
* its theme to its own calls.
|
|
95
95
|
*/ function claimAmbientTheme(owner, theme) {
|
|
96
|
-
if (ambient && ambient.owner !== owner) return false;
|
|
97
|
-
if (!ambient) ambientVersion++;
|
|
98
|
-
ambient = {
|
|
96
|
+
if (state.ambient && state.ambient.owner !== owner) return false;
|
|
97
|
+
if (!state.ambient) state.ambientVersion++;
|
|
98
|
+
state.ambient = {
|
|
99
99
|
owner,
|
|
100
100
|
theme
|
|
101
101
|
};
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
/** Gives up the theme read outside a call, when `owner` has it: its start failed, or it has shut down. */ function releaseAmbientTheme(owner) {
|
|
105
|
-
if (ambient?.owner !== owner) return;
|
|
106
|
-
ambient = undefined;
|
|
107
|
-
ambientVersion++;
|
|
105
|
+
if (state.ambient?.owner !== owner) return;
|
|
106
|
+
state.ambient = undefined;
|
|
107
|
+
state.ambientVersion++;
|
|
108
108
|
}
|
|
109
109
|
/** Whether `owner` is the app whose theme is read outside a call. */ function ownsAmbientTheme(owner) {
|
|
110
|
-
return ambient?.owner === owner;
|
|
110
|
+
return state.ambient?.owner === owner;
|
|
111
111
|
}
|
|
112
112
|
/** Runs `fn` with `theme` as the theme of the call, for everything it runs, awaits or starts. */ function runWithTheme(theme, fn) {
|
|
113
113
|
return scope.run({
|
|
@@ -123,10 +123,8 @@ let ambientVersion = 0;
|
|
|
123
123
|
/** Runs `fn` in `themeScope`, whose theme may be replaced while it runs, as a per-server theme is found. */ function runInThemeScope(themeScope, fn) {
|
|
124
124
|
return scope.run(themeScope, fn);
|
|
125
125
|
}
|
|
126
|
-
// Set by the runtime: the theme an app gives an interaction answered outside any call, as in a collector
|
|
127
|
-
let themeOutsideCalls;
|
|
128
126
|
/** Sets how an answer outside any call finds its theme: from the interaction's app, server and user. */ function setThemeOutsideCalls(resolve) {
|
|
129
|
-
themeOutsideCalls = resolve;
|
|
127
|
+
state.themeOutsideCalls = resolve;
|
|
130
128
|
}
|
|
131
129
|
/**
|
|
132
130
|
* The theme an answer to `interaction` uses: the running call's, or, outside any call, as in a collector's callback,
|
|
@@ -134,7 +132,7 @@ let themeOutsideCalls;
|
|
|
134
132
|
*/ function themeForInteraction(interaction) {
|
|
135
133
|
const call = scope.getStore();
|
|
136
134
|
if (call) return call.theme;
|
|
137
|
-
return themeOutsideCalls?.(interaction) ?? useTheme();
|
|
135
|
+
return state.themeOutsideCalls?.(interaction) ?? useTheme();
|
|
138
136
|
}
|
|
139
137
|
/**
|
|
140
138
|
* The theme of the running call, with every role present.
|
|
@@ -163,7 +161,7 @@ let themeOutsideCalls;
|
|
|
163
161
|
*
|
|
164
162
|
* @group Responses
|
|
165
163
|
*/ function useTheme() {
|
|
166
|
-
return scope.getStore()?.theme ?? ambient?.theme() ?? defaults;
|
|
164
|
+
return scope.getStore()?.theme ?? state.ambient?.theme() ?? state.defaults;
|
|
167
165
|
}
|
|
168
166
|
/**
|
|
169
167
|
* Makes a function run in the theme of the call that binds it, wherever it is called from later.
|
|
@@ -172,6 +170,9 @@ let themeOutsideCalls;
|
|
|
172
170
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
173
171
|
* Timers and promises the handler starts keep its theme without it.
|
|
174
172
|
*
|
|
173
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
174
|
+
* it is for, is answered in the starting member's theme.
|
|
175
|
+
*
|
|
175
176
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
176
177
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
177
178
|
*
|
|
@@ -181,7 +182,7 @@ let themeOutsideCalls;
|
|
|
181
182
|
* async poll(message: Message) {
|
|
182
183
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
183
184
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
184
|
-
* const { emojis } = useTheme() // the
|
|
185
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
185
186
|
* await click.reply(`${emojis.success} Picked`)
|
|
186
187
|
* }))
|
|
187
188
|
* }
|
|
@@ -195,4 +196,4 @@ let themeOutsideCalls;
|
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme,
|
|
199
|
+
export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
|
|
@@ -51,7 +51,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
|
|
|
51
51
|
const prototype = Object.getPrototypeOf(value);
|
|
52
52
|
return prototype === Object.prototype || prototype === null;
|
|
53
53
|
};
|
|
54
|
-
const PLAIN = "give a plain object, such as {
|
|
54
|
+
const PLAIN = "give a plain object: a database row's plain form, such as Mongoose's toObject() or Sequelize's get({ plain: true }), or an object literal of the values you set, since anything else would replace the whole";
|
|
55
55
|
/** What a non-plain object is: by its class, or as one without a plain prototype when it has none named. */ const describeInstance = (value)=>{
|
|
56
56
|
const name = value.constructor?.name;
|
|
57
57
|
return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
|
|
@@ -8,9 +8,12 @@ import { assertObservers } from '../core/observer-runner.js';
|
|
|
8
8
|
import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
|
|
9
9
|
import { copyLayer } from '../core/theme-scope.js';
|
|
10
10
|
import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
|
|
11
|
-
import { timeoutProblem } from '../util/timer-limit.util.js';
|
|
11
|
+
import { timeoutProblem, timerKeepsSeconds, MAX_TIMER_SECONDS } from '../util/timer-limit.util.js';
|
|
12
|
+
import { Logger } from '../common/logger.js';
|
|
13
|
+
import { isConstructor, describeValue } from '../util/value.util.js';
|
|
12
14
|
import { META } from '../util/metadata-keys.js';
|
|
13
15
|
|
|
16
|
+
const logger = new Logger('MeoCord');
|
|
14
17
|
/** Refuses a `messages` option of the wrong type where the app is declared, rather than at the first message. */ function assertMessageOptions(appName, messages, i18n) {
|
|
15
18
|
if (!messages) return;
|
|
16
19
|
const { prefix, mention, caseSensitive } = messages;
|
|
@@ -38,6 +41,9 @@ import { META } from '../util/metadata-keys.js';
|
|
|
38
41
|
if (deleteUsageRepliesAfter !== undefined && !(typeof deleteUsageRepliesAfter === 'number' && deleteUsageRepliesAfter >= 0 && Number.isFinite(deleteUsageRepliesAfter))) {
|
|
39
42
|
throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) takes a number of seconds, or 0 to keep usage replies.`));
|
|
40
43
|
}
|
|
44
|
+
if (deleteUsageRepliesAfter !== undefined && !timerKeepsSeconds(deleteUsageRepliesAfter)) {
|
|
45
|
+
logger.warn(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) is ${deleteUsageRepliesAfter} seconds, longer than a timer ` + `can wait (${MAX_TIMER_SECONDS}), so usage replies are kept. The next major version (5.0) refuses it.`);
|
|
46
|
+
}
|
|
41
47
|
const { help } = messages;
|
|
42
48
|
// One word each, since help is asked for by its first word
|
|
43
49
|
const isWord = (value)=>typeof value === 'string' && /^\S+$/.test(value);
|
|
@@ -52,6 +58,35 @@ import { META } from '../util/metadata-keys.js';
|
|
|
52
58
|
if (type.labelKey !== undefined) assertLabelKey(appName, name, type.labelKey, i18n);
|
|
53
59
|
}
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Refuses an option that takes a class given anything else, such as an object or an arrow function, where the app is
|
|
63
|
+
* declared: the startup checks read each class's prototype, and would otherwise fail on it without naming the option.
|
|
64
|
+
*/ function assertClassOptions(appName, { presenter, cooldownStore, controllers, services }) {
|
|
65
|
+
for (const [name, value, implementing] of [
|
|
66
|
+
[
|
|
67
|
+
'presenter',
|
|
68
|
+
presenter,
|
|
69
|
+
'ResponsePresenter'
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
'cooldownStore',
|
|
73
|
+
cooldownStore,
|
|
74
|
+
'CooldownStore'
|
|
75
|
+
]
|
|
76
|
+
]){
|
|
77
|
+
if (value !== undefined && !isConstructor(value)) {
|
|
78
|
+
throw refuse(new TypeError(`${appName}: @MeoCord({ ${name} }) takes a class implementing ${implementing}, not ${describeValue(value)}.`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const controller of controllers ?? []){
|
|
82
|
+
if (!isConstructor(controller)) throw refuse(new TypeError(`${appName}: @MeoCord({ controllers }) takes classes, not ${describeValue(controller)}.`));
|
|
83
|
+
}
|
|
84
|
+
// A string or symbol is a token, which a provider has to provide; that is checked as the app is created
|
|
85
|
+
for (const service of services ?? []){
|
|
86
|
+
if (isConstructor(service) || typeof service === 'string' || typeof service === 'symbol') continue;
|
|
87
|
+
throw refuse(new TypeError(`${appName}: @MeoCord({ services }) takes classes, or tokens that providers provide, not ${describeValue(service)}.`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
55
90
|
/** Refuses a type's `labelKey` that no message of the app's default catalog has, as it would be shown as the key. */ function assertLabelKey(appName, name, labelKey, i18n) {
|
|
56
91
|
const where = `${appName}: @MeoCord({ messages: { types } }): "${name}" has labelKey`;
|
|
57
92
|
if (typeof labelKey !== 'string') throw refuse(new TypeError(`${where} ${String(labelKey)}; it takes a message key, such as 'types.${name}'.`));
|
|
@@ -96,6 +131,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
96
131
|
*/ function MeoCord(options) {
|
|
97
132
|
return declaring((target, propertyKey)=>{
|
|
98
133
|
if (deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
|
|
134
|
+
assertClassOptions(target.name, options);
|
|
99
135
|
assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
|
|
100
136
|
assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
|
|
101
137
|
assertStageEntries('@MeoCord({ filters })', 'filter', target.name, options.filters ?? []);
|
|
@@ -2,7 +2,15 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
3
|
import { META } from '../util/metadata-keys.js';
|
|
4
4
|
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
5
|
+
import { Logger } from '../common/logger.js';
|
|
5
6
|
|
|
7
|
+
const logger = new Logger('CommandBuilder');
|
|
8
|
+
/** The nearest base class of `target` that gives `guilds`, which `target` inherits when it gives none. */ function guildsBase(target) {
|
|
9
|
+
for(let base = Object.getPrototypeOf(target); base && base !== Function.prototype; base = Object.getPrototypeOf(base)){
|
|
10
|
+
if (Reflect.hasOwnMetadata(META.builderGuilds, base)) return base;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
|
|
8
16
|
*
|
|
@@ -40,6 +48,12 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
40
48
|
if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
|
|
41
49
|
...options.guilds
|
|
42
50
|
], target);
|
|
51
|
+
else {
|
|
52
|
+
const base = guildsBase(target);
|
|
53
|
+
if (base) {
|
|
54
|
+
logger.warn(`${target.name} gives no guilds and inherits ${base.name}'s; declare guilds on ${target.name} to keep them. ` + 'The next major version (5.0) registers it in the configured scope.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
43
57
|
});
|
|
44
58
|
}
|
|
45
59
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dropInheritedRoutes } from './controller.decorator.js';
|
|
2
|
-
import { guardOwnHandlersWithBaseGuards } from '
|
|
2
|
+
import { guardOwnHandlersWithBaseGuards } from '../core/guard-runner.js';
|
|
3
3
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
4
4
|
import { META } from '../util/metadata-keys.js';
|
|
5
5
|
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
@@ -7,8 +7,8 @@ import { wrapsMethod } from '../util/method-wrapper.util.js';
|
|
|
7
7
|
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
8
8
|
import { Logger } from '../common/logger.js';
|
|
9
9
|
import { routeSpecificity } from '../core/route-specificity.js';
|
|
10
|
-
import { lookupTable, isSegmentType, choicesOf, parseSegment } from '../core/scalar-types.js';
|
|
11
|
-
import { declaring, refuse, refuseOnClass } from '../util/refusal.util.js';
|
|
10
|
+
import { lookupTable, isSegmentType, choicesOf, shortestSegment, parseSegment } from '../core/scalar-types.js';
|
|
11
|
+
import { declaring, refuse, refuseOnClass, decoratedName } from '../util/refusal.util.js';
|
|
12
12
|
import { withArticle, describeValue } from '../util/value.util.js';
|
|
13
13
|
import { META } from '../util/metadata-keys.js';
|
|
14
14
|
|
|
@@ -18,6 +18,12 @@ import { META } from '../util/metadata-keys.js';
|
|
|
18
18
|
return `; it was given ${describeValue(value)}`;
|
|
19
19
|
}
|
|
20
20
|
const logger = new Logger('Command');
|
|
21
|
+
/** The interaction types a method's `@Command` routes declare, each once. */ function routeTypesOf(prototype, method) {
|
|
22
|
+
const metas = Object.values(getCommandMap(prototype) ?? {}).flat();
|
|
23
|
+
return [
|
|
24
|
+
...new Set(metas.filter((meta)=>meta.methodName === method).map((meta)=>meta.type))
|
|
25
|
+
];
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
28
|
* The class's own handler list, started from a copy of the inherited one, so a subclass's
|
|
23
29
|
* handlers never land in its base class's metadata.
|
|
@@ -43,6 +49,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
43
49
|
method,
|
|
44
50
|
label: `autocomplete of ${optionName === undefined ? 'every option' : `"${optionName}"`} in "${commandPath}"`
|
|
45
51
|
});
|
|
52
|
+
/** An `@On` or `@Once` handler as a warning names its route; one event per method, as a subclass replaces it. */ const eventRoute = ({ method, event })=>({
|
|
53
|
+
method,
|
|
54
|
+
label: `event "${event}"`
|
|
55
|
+
});
|
|
46
56
|
/** Records a route a class's own decorator declares for a method, which the startup check reads. */ function declareRoute(target, declared) {
|
|
47
57
|
const routes = Reflect.getOwnMetadata(META.declaredRoutes, target) ?? [];
|
|
48
58
|
routes.push(declared);
|
|
@@ -70,13 +80,22 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
70
80
|
else handlers[index] = entry;
|
|
71
81
|
Reflect.defineMetadata(key, handlers, target);
|
|
72
82
|
}
|
|
83
|
+
/** The `@On` and `@Once` entries a class's own decorators declare, without those it inherits from its base. */ function ownEventHandlers(prototype) {
|
|
84
|
+
const inherited = Reflect.getMetadata(META.eventHandlers, Object.getPrototypeOf(prototype)) ?? [];
|
|
85
|
+
return ownHandlerList(META.eventHandlers, prototype).filter((entry)=>!inherited.includes(entry));
|
|
86
|
+
}
|
|
73
87
|
/**
|
|
74
88
|
* Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
|
|
75
89
|
* for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
|
|
76
90
|
*/ function dropInheritedRoutes(prototype) {
|
|
77
|
-
const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
|
|
78
|
-
if (redeclared.size === 0) return;
|
|
79
91
|
const base = Object.getPrototypeOf(prototype);
|
|
92
|
+
// The class's own @On and @Once re-declare their methods too
|
|
93
|
+
const ownEvents = ownEventHandlers(prototype);
|
|
94
|
+
const redeclared = new Set([
|
|
95
|
+
...getDeclaredRoutes(prototype).map((route)=>route.method),
|
|
96
|
+
...ownEvents.map((entry)=>entry.method)
|
|
97
|
+
]);
|
|
98
|
+
if (redeclared.size === 0) return;
|
|
80
99
|
// An entry the class takes from its base, for a method it re-declares
|
|
81
100
|
const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
|
|
82
101
|
const commands = ownCommandMap(prototype);
|
|
@@ -99,6 +118,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
99
118
|
[
|
|
100
119
|
META.autocompleteHandlers,
|
|
101
120
|
(entry)=>entry.methodName
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
META.eventHandlers,
|
|
124
|
+
(entry)=>entry.method
|
|
102
125
|
]
|
|
103
126
|
];
|
|
104
127
|
for (const [key, method] of lists){
|
|
@@ -125,13 +148,14 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
125
148
|
function MessageHandler(pattern, options = {}) {
|
|
126
149
|
return declaring(function(target, propertyKey) {
|
|
127
150
|
refuseOnClass('@MessageHandler', target, propertyKey);
|
|
128
|
-
// An empty pattern means every message, as no pattern does
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
// An empty pattern, or one of spaces alone, means every message, as no pattern does
|
|
152
|
+
const blank = pattern !== undefined && pattern.trim() === '';
|
|
153
|
+
if (blank) {
|
|
154
|
+
warnDeprecatedBehaviour(logger, `@MessageHandler('${pattern}') on ${decoratedName(target, propertyKey)}`, 'is refused', '@MessageHandler()');
|
|
131
155
|
}
|
|
132
156
|
const method = propertyKey.toString();
|
|
133
157
|
const declared = {
|
|
134
|
-
pattern:
|
|
158
|
+
pattern: blank ? undefined : pattern,
|
|
135
159
|
method,
|
|
136
160
|
options
|
|
137
161
|
};
|
|
@@ -253,13 +277,14 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
253
277
|
params: params.length,
|
|
254
278
|
typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
|
|
255
279
|
});
|
|
256
|
-
// Each param takes at least
|
|
280
|
+
// Each param takes at least the shortest value of its type
|
|
281
|
+
const shortest = literalLength + params.reduce((sum, param)=>sum + shortestSegment(types[param] ?? 'string'), 0);
|
|
257
282
|
return {
|
|
258
283
|
regex,
|
|
259
284
|
params,
|
|
260
285
|
types: lookupTable(types),
|
|
261
286
|
specificity,
|
|
262
|
-
shortest
|
|
287
|
+
shortest
|
|
263
288
|
};
|
|
264
289
|
}
|
|
265
290
|
/** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
|
|
@@ -332,13 +357,17 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
332
357
|
refuseOnClass('@Command', target, propertyKey);
|
|
333
358
|
const originalMethod = _descriptor.value;
|
|
334
359
|
if (!originalMethod) {
|
|
335
|
-
throw refuse(new Error(`${target
|
|
360
|
+
throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Command is on something with no implementation.`));
|
|
336
361
|
}
|
|
337
362
|
// Wrap original method for interaction type validation
|
|
338
363
|
const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
|
|
339
364
|
const checked = function(interaction, params) {
|
|
340
|
-
|
|
341
|
-
|
|
365
|
+
// Every type the method's routes declare, in the class the call is made on: each @Command wraps it, so one alone
|
|
366
|
+
// would refuse the others' interactions, and an override that adds a route type reaches this one through super
|
|
367
|
+
const called = typeof this === 'object' && this !== null && Object.prototype.isPrototypeOf.call(target, this) ? Object.getPrototypeOf(this) : target;
|
|
368
|
+
const types = routeTypesOf(called, propertyKey);
|
|
369
|
+
if (!types.some((type)=>matchesCommandType(type, interaction))) {
|
|
370
|
+
throw new Error(types.length <= 1 ? `${decoratedName(target, propertyKey)}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.` : `${decoratedName(target, propertyKey)}: its @Command routes take ${types.map(interactionClassName).join(' or ')}` + `${givenInstead(interaction)}.`);
|
|
342
371
|
}
|
|
343
372
|
return originalMethod.apply(this, [
|
|
344
373
|
interaction,
|
|
@@ -356,7 +385,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
356
385
|
let guilds;
|
|
357
386
|
// Determine command type and builder
|
|
358
387
|
if (typeof builderOrType === 'function') {
|
|
359
|
-
const where =
|
|
388
|
+
const where = decoratedName(target, propertyKey);
|
|
360
389
|
// A subcommand is part of its command's builder, which belongs on the command's own name
|
|
361
390
|
const subcommandPath = Reflect.getMetadata(META.commandType, builderOrType) === CommandType.SLASH && commandName.includes(' ');
|
|
362
391
|
const command = commandName.split(' ')[0];
|
|
@@ -403,12 +432,12 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
403
432
|
if (isCustomIdRouted(commandType)) {
|
|
404
433
|
// A route warned as it was made
|
|
405
434
|
const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
|
|
406
|
-
if (braces) logger.warn(`${target
|
|
435
|
+
if (braces) logger.warn(`${decoratedName(target, propertyKey)}: in the pattern "${commandName}", ${braces}`);
|
|
407
436
|
let pattern;
|
|
408
437
|
try {
|
|
409
438
|
pattern = createRegexFromPattern(commandName);
|
|
410
439
|
} catch (error) {
|
|
411
|
-
throw refuse(new Error(`${target
|
|
440
|
+
throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${error.message}`, {
|
|
412
441
|
cause: error
|
|
413
442
|
}));
|
|
414
443
|
}
|
|
@@ -418,7 +447,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
418
447
|
// Warned rather than refused, so a bot that carries such a handler still starts
|
|
419
448
|
if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
|
|
420
449
|
const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
|
|
421
|
-
logger.warn(`${target
|
|
450
|
+
logger.warn(`${decoratedName(target, propertyKey)}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
|
|
422
451
|
}
|
|
423
452
|
}
|
|
424
453
|
const declared = {
|
|
@@ -470,7 +499,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
|
470
499
|
const registered = built?.type;
|
|
471
500
|
if (!declared || registered === undefined || declared.kind === registered) return;
|
|
472
501
|
const kind = registered === ApplicationCommandType.User ? 'user' : 'message';
|
|
473
|
-
throw refuse(new Error(`${target
|
|
502
|
+
throw refuse(new Error(`${decoratedName(target, propertyKey)}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
|
|
474
503
|
}
|
|
475
504
|
/**
|
|
476
505
|
* Retrieves the command map for a given controller.
|
|
@@ -608,4 +637,4 @@ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
|
|
|
608
637
|
return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
|
|
609
638
|
}
|
|
610
639
|
|
|
611
|
-
export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
|
|
640
|
+
export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, addOwnHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, eventRoute, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownEventHandlers, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { nonInteractionHandler, deferMisuseError } from '../core/defer.js';
|
|
3
|
-
import { declaring, refuseOnClass, refuse } from '../util/refusal.util.js';
|
|
3
|
+
import { declaring, refuseOnClass, refuse, ownerName } from '../util/refusal.util.js';
|
|
4
4
|
import { META } from '../util/metadata-keys.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -43,7 +43,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
43
43
|
refuseOnClass('@Defer', target, propertyKey);
|
|
44
44
|
const methodName = String(propertyKey);
|
|
45
45
|
const kind = nonInteractionHandler(target, methodName);
|
|
46
|
-
if (kind) throw refuse(deferMisuseError(target
|
|
46
|
+
if (kind) throw refuse(deferMisuseError(ownerName(target), methodName, kind));
|
|
47
47
|
Reflect.defineMetadata(META.deferOptions, {
|
|
48
48
|
...options
|
|
49
49
|
}, target, methodName);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import {
|
|
2
|
+
import { addOwnHandler } from './controller.decorator.js';
|
|
3
3
|
import { META } from '../util/metadata-keys.js';
|
|
4
4
|
import { declaring, refuseOnClass } from '../util/refusal.util.js';
|
|
5
5
|
|
|
@@ -7,13 +7,12 @@ function eventDecorator(event, once) {
|
|
|
7
7
|
// Generic over the method, so it is checked against the event's arguments rather than required to match exactly
|
|
8
8
|
return declaring(function(target, propertyKey, _descriptor) {
|
|
9
9
|
refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// One the class inherits for the same method and event is replaced, as for every other kind of handler
|
|
11
|
+
addOwnHandler(META.eventHandlers, target, {
|
|
12
12
|
event,
|
|
13
13
|
method: propertyKey,
|
|
14
14
|
once
|
|
15
|
-
});
|
|
16
|
-
Reflect.defineMetadata(META.eventHandlers, handlers, target);
|
|
15
|
+
}, (other)=>other.method === propertyKey && other.event === event);
|
|
17
16
|
});
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
@@ -3,9 +3,9 @@ import { makeInjectable } from '../util/injectable.util.js';
|
|
|
3
3
|
import { assertStageEntries } from '../core/stage-scope.js';
|
|
4
4
|
import { Logger } from '../common/logger.js';
|
|
5
5
|
import { warnDeprecatedBehaviour } from '../common/deprecation.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isConstructible, describeValue } from '../util/value.util.js';
|
|
7
7
|
import { META } from '../util/metadata-keys.js';
|
|
8
|
-
import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
8
|
+
import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Marks a class as an exception filter for the given error types.
|
|
@@ -40,7 +40,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
|
40
40
|
refuseOnMethod('@Catch', target, propertyKey);
|
|
41
41
|
makeInjectable(target);
|
|
42
42
|
errorTypes.forEach((type, index)=>{
|
|
43
|
-
if (
|
|
43
|
+
if (isConstructible(type)) return;
|
|
44
44
|
const entry = ORDINALS[index] ? `${ORDINALS[index]} entry` : `entry ${index + 1}`;
|
|
45
45
|
warnDeprecatedBehaviour(logger, `${target.name}: @Catch's ${entry}, ${describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
|
|
46
46
|
});
|
|
@@ -88,7 +88,7 @@ const ORDINALS = [
|
|
|
88
88
|
* @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
|
|
89
89
|
*/ function UseFilter(...filters) {
|
|
90
90
|
return declaring(function(target, propertyKey) {
|
|
91
|
-
const where =
|
|
91
|
+
const where = decoratedName(target, propertyKey);
|
|
92
92
|
assertStageEntries('@UseFilter', 'filter', where, filters);
|
|
93
93
|
// Decorators apply bottom-up, so a higher decorator's filters are tried first.
|
|
94
94
|
if (propertyKey === undefined) {
|
|
@@ -1,47 +1,10 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import {
|
|
2
|
+
import { applyGuards, recordGuards, handlerMethods, classLevelGuards, declaringPrototype } from '../core/guard-runner.js';
|
|
3
3
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
4
|
-
import { wrapsMethod } from '../util/method-wrapper.util.js';
|
|
5
4
|
import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
|
|
6
5
|
import { META } from '../util/metadata-keys.js';
|
|
7
|
-
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
6
|
+
import { declaring, deprecatedOnMethod, decoratedName } from '../util/refusal.util.js';
|
|
8
7
|
|
|
9
|
-
/**
|
|
10
|
-
* Adds guards to a method's class or method list, then republishes the effective list under
|
|
11
|
-
* `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
|
|
12
|
-
* latest decorator first, since it wraps outermost.
|
|
13
|
-
*/ function recordGuards(key, guards, prototype, methodName) {
|
|
14
|
-
const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
|
|
15
|
-
const bases = key === META.classGuards ? Reflect.getOwnMetadata(META.baseClassGuards, prototype, methodName) ?? 0 : 0;
|
|
16
|
-
Reflect.defineMetadata(key, [
|
|
17
|
-
...existing.slice(0, bases),
|
|
18
|
-
...guards,
|
|
19
|
-
...existing.slice(bases)
|
|
20
|
-
], prototype, methodName);
|
|
21
|
-
const [classGuards, inheritedGuards, methodGuards] = [
|
|
22
|
-
META.classGuards,
|
|
23
|
-
META.inheritedGuards,
|
|
24
|
-
META.methodGuards
|
|
25
|
-
].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
|
|
26
|
-
Reflect.defineMetadata(META.guards, [
|
|
27
|
-
...inheritedGuards,
|
|
28
|
-
...classGuards,
|
|
29
|
-
...methodGuards
|
|
30
|
-
], prototype, methodName);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
|
|
34
|
-
* chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
|
|
35
|
-
* which has already run the guards, passes through every wrapper.
|
|
36
|
-
*/ function applyGuards(descriptor, prototype, propertyKey) {
|
|
37
|
-
const originalMethod = descriptor.value;
|
|
38
|
-
descriptor.value = wrapsMethod(async function(...args) {
|
|
39
|
-
if (consumeDispatchMark(args[0], this, propertyKey)) return originalMethod.apply(this, args);
|
|
40
|
-
return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args));
|
|
41
|
-
}, originalMethod);
|
|
42
|
-
const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
|
|
43
|
-
Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
|
|
44
|
-
}
|
|
45
8
|
/**
|
|
46
9
|
* The class's own descriptor for a handler. An inherited handler gets one that calls the inherited
|
|
47
10
|
* method, starting from its guard lists and wrapper count, so class guards wrap it like an own method.
|
|
@@ -69,26 +32,6 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
69
32
|
...inherited
|
|
70
33
|
};
|
|
71
34
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Gives each handler a controller declares itself the class-level guards of the classes it extends,
|
|
74
|
-
* as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
|
|
75
|
-
* runs them when no other guard wraps the handler. `@Controller` calls this.
|
|
76
|
-
*/ function guardOwnHandlersWithBaseGuards(target) {
|
|
77
|
-
const prototype = target.prototype;
|
|
78
|
-
for (const methodName of handlerMethods(prototype)){
|
|
79
|
-
const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
|
|
80
|
-
// Inherited handlers have the chain of the class that declares them
|
|
81
|
-
if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(META.inheritedFrom, prototype, methodName)) continue;
|
|
82
|
-
// Base first, as dispatch runs them
|
|
83
|
-
const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
|
|
84
|
-
if (guards.length === 0) continue;
|
|
85
|
-
if (!Reflect.getOwnMetadata(META.guardWrappers, prototype, methodName)) {
|
|
86
|
-
applyGuards(descriptor, prototype, methodName);
|
|
87
|
-
Object.defineProperty(prototype, methodName, descriptor);
|
|
88
|
-
}
|
|
89
|
-
recordGuards(META.inheritedGuards, guards, prototype, methodName);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
35
|
/**
|
|
93
36
|
* Marks a class as a guard, which decides whether a handler runs.
|
|
94
37
|
*
|
|
@@ -162,7 +105,7 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
162
105
|
*/ function UseGuard(...entries) {
|
|
163
106
|
const guards = entries;
|
|
164
107
|
return declaring(function(target, propertyKey, descriptor) {
|
|
165
|
-
const where =
|
|
108
|
+
const where = decoratedName(target, propertyKey);
|
|
166
109
|
assertStageEntries('@UseGuard', 'guard', where, guards);
|
|
167
110
|
if (descriptor && propertyKey) {
|
|
168
111
|
// Method Decorator
|
|
@@ -189,4 +132,4 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
189
132
|
});
|
|
190
133
|
}
|
|
191
134
|
|
|
192
|
-
export { Guard, UseGuard
|
|
135
|
+
export { Guard, UseGuard };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inject } from 'inversify';
|
|
2
|
-
import { declaring, refuseOnClass } from '../util/refusal.util.js';
|
|
2
|
+
import { declaring, refuseOnClass, refuse, decoratedName } from '../util/refusal.util.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Injects what a token provides into a constructor parameter.
|
|
@@ -30,6 +30,9 @@ import { declaring, refuseOnClass } from '../util/refusal.util.js';
|
|
|
30
30
|
return declaring((target, propertyKey, parameterIndex)=>{
|
|
31
31
|
// A constructor parameter has no property key either, but has an index
|
|
32
32
|
if (typeof parameterIndex !== 'number') refuseOnClass('@Inject', target, propertyKey, 'a constructor parameter or a property');
|
|
33
|
+
else if (propertyKey !== undefined) {
|
|
34
|
+
throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Inject goes on a constructor parameter or a property, not a method parameter.`));
|
|
35
|
+
}
|
|
33
36
|
decorate(target, propertyKey, parameterIndex);
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -2,7 +2,7 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
3
|
import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
|
|
4
4
|
import { META } from '../util/metadata-keys.js';
|
|
5
|
-
import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
5
|
+
import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Marks a class as an interceptor, which wraps a handler to act before and after it.
|
|
@@ -76,7 +76,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
|
|
|
76
76
|
* @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
|
|
77
77
|
*/ function UseInterceptor(...interceptors) {
|
|
78
78
|
return declaring(function(target, propertyKey) {
|
|
79
|
-
const where =
|
|
79
|
+
const where = decoratedName(target, propertyKey);
|
|
80
80
|
assertStageEntries('@UseInterceptor', 'interceptor', where, interceptors);
|
|
81
81
|
// Decorators apply bottom-up, so a higher decorator's interceptors go first, as with @UseGuard.
|
|
82
82
|
if (propertyKey === undefined) {
|