meocord 4.1.2 → 4.2.0
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 +98 -0
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +195 -157
- package/dist/cjs/decorator/index.cjs +66 -57
- package/dist/cjs/testing/index.cjs +733 -301
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/build/load-config.pre-entry.js +4 -1
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +2 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/route.js +10 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/meocord-factory.js +196 -157
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +38 -21
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +1 -1
- package/dist/esm/core/startup-roots.js +2 -3
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +110 -67
- package/dist/esm/decorator/cooldown.decorator.js +3 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +8 -8
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +1 -1
- package/dist/esm/testing/cooldown-store-suite.js +18 -10
- package/dist/esm/testing/discord-defaults.js +60 -7
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +229 -188
- package/dist/esm/testing/mock-fn.js +80 -10
- package/dist/esm/testing/mock-interaction.js +223 -72
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +45 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/interaction.util.js +1 -1
- package/dist/esm/util/meocord-config-validation.util.js +1 -0
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +8 -6
- package/dist/types/core/index.d.ts +8 -6
- package/dist/types/decorator/index.d.cts +30 -29
- package/dist/types/decorator/index.d.ts +30 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
- package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
- package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
- package/dist/types/testing/index.d.cts +245 -31
- package/dist/types/testing/index.d.ts +245 -31
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
- package/package.json +3 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { handlerMethods, classLevelGuards,
|
|
2
|
+
import { handlerMethods, classLevelGuards, consumeDispatchMark, runDirectCall, declaringPrototype, classChain } from '../core/guard-runner.js';
|
|
3
3
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
4
4
|
import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
|
|
5
5
|
import { META } from '../util/metadata-keys.js';
|
|
6
|
-
import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
6
|
+
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Adds guards to a method's class or method list, then republishes the effective list under
|
|
@@ -119,14 +119,14 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
119
119
|
* @see {@link UseGuard}
|
|
120
120
|
* @see {@link GuardDeniedError}
|
|
121
121
|
* @see {@link ExecutionContext}
|
|
122
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
122
|
+
* @see {@link https://meocord.dev/docs/4.2/guards | Guards}
|
|
123
123
|
*/ function Guard(options = {}) {
|
|
124
|
-
return function(target, propertyKey) {
|
|
124
|
+
return declaring(function(target, propertyKey) {
|
|
125
125
|
if (deprecatedOnMethod('@Guard', target, propertyKey)) return;
|
|
126
126
|
makeInjectable(target);
|
|
127
127
|
defineStageTypes(target, options.types, 'Guard');
|
|
128
128
|
Reflect.defineMetadata(META.guardClass, true, target);
|
|
129
|
-
};
|
|
129
|
+
});
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* Runs guards before a handler, or before every handler of a controller.
|
|
@@ -157,10 +157,10 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
157
157
|
* @see {@link Guard}
|
|
158
158
|
* @see {@link GuardDeniedError}
|
|
159
159
|
* @see {@link ExecutionContext}
|
|
160
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
160
|
+
* @see {@link https://meocord.dev/docs/4.2/guards | Guards}
|
|
161
161
|
*/ function UseGuard(...entries) {
|
|
162
162
|
const guards = entries;
|
|
163
|
-
return function(target, propertyKey, descriptor) {
|
|
163
|
+
return declaring(function(target, propertyKey, descriptor) {
|
|
164
164
|
const where = propertyKey === undefined ? String(target?.name) : `${target.constructor.name}.${String(propertyKey)}`;
|
|
165
165
|
assertStageEntries('@UseGuard', 'guard', where, guards);
|
|
166
166
|
if (descriptor && propertyKey) {
|
|
@@ -185,7 +185,7 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
};
|
|
188
|
+
});
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
export { Guard, UseGuard, guardOwnHandlersWithBaseGuards };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inject } from 'inversify';
|
|
2
|
-
import { refuseOnClass } from '../util/refusal.util.js';
|
|
2
|
+
import { declaring, refuseOnClass } from '../util/refusal.util.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Injects what a token provides into a constructor parameter.
|
|
@@ -24,14 +24,14 @@ import { refuseOnClass } from '../util/refusal.util.js';
|
|
|
24
24
|
* @category Params
|
|
25
25
|
* @see {@link Service}
|
|
26
26
|
* @see {@link createToken}
|
|
27
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
27
|
+
* @see {@link https://meocord.dev/docs/4.2/services | Services}
|
|
28
28
|
*/ function Inject(token) {
|
|
29
29
|
const decorate = inject(token);
|
|
30
|
-
return (target, propertyKey, parameterIndex)=>{
|
|
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
33
|
decorate(target, propertyKey, parameterIndex);
|
|
34
|
-
};
|
|
34
|
+
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export { Inject };
|
|
@@ -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 { refuseOnMethod } from '../util/refusal.util.js';
|
|
5
|
+
import { declaring, refuseOnMethod } 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.
|
|
@@ -36,13 +36,13 @@ import { refuseOnMethod } from '../util/refusal.util.js';
|
|
|
36
36
|
* @group Decorators
|
|
37
37
|
* @category Pipeline stages
|
|
38
38
|
* @see {@link UseInterceptor}
|
|
39
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
39
|
+
* @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
|
|
40
40
|
*/ function Interceptor(options = {}) {
|
|
41
|
-
return function(target, propertyKey) {
|
|
41
|
+
return declaring(function(target, propertyKey) {
|
|
42
42
|
refuseOnMethod('@Interceptor', target, propertyKey);
|
|
43
43
|
makeInjectable(target);
|
|
44
44
|
defineStageTypes(target, options.types, 'Interceptor');
|
|
45
|
-
};
|
|
45
|
+
});
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
48
|
* Runs interceptors around a handler, or around every handler of a controller.
|
|
@@ -73,9 +73,9 @@ import { refuseOnMethod } from '../util/refusal.util.js';
|
|
|
73
73
|
* @group Decorators
|
|
74
74
|
* @category Pipeline stages
|
|
75
75
|
* @see {@link Interceptor}
|
|
76
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
76
|
+
* @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
|
|
77
77
|
*/ function UseInterceptor(...interceptors) {
|
|
78
|
-
return function(target, propertyKey) {
|
|
78
|
+
return declaring(function(target, propertyKey) {
|
|
79
79
|
const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(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.
|
|
@@ -92,7 +92,7 @@ import { refuseOnMethod } from '../util/refusal.util.js';
|
|
|
92
92
|
...existing
|
|
93
93
|
], target, propertyKey);
|
|
94
94
|
}
|
|
95
|
-
};
|
|
95
|
+
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export { Interceptor, UseInterceptor };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
3
|
import { defineStageTypes } from '../core/stage-scope.js';
|
|
4
|
-
import { refuseOnMethod, refuse } from '../util/refusal.util.js';
|
|
4
|
+
import { declaring, refuseOnMethod, refuse } from '../util/refusal.util.js';
|
|
5
5
|
import { META } from '../util/metadata-keys.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -35,9 +35,9 @@ import { META } from '../util/metadata-keys.js';
|
|
|
35
35
|
* @pipeline observers-settled once the call has settled and been answered, however it ended
|
|
36
36
|
* @group Decorators
|
|
37
37
|
* @category Pipeline stages
|
|
38
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
38
|
+
* @see {@link https://meocord.dev/docs/4.2/observers | Observers}
|
|
39
39
|
*/ function Observer(options = {}) {
|
|
40
|
-
return function(target, propertyKey) {
|
|
40
|
+
return declaring(function(target, propertyKey) {
|
|
41
41
|
refuseOnMethod('@Observer', target, propertyKey);
|
|
42
42
|
if (typeof target.prototype.onSettled !== 'function') {
|
|
43
43
|
throw refuse(new Error(`${target.name}: an @Observer needs an onSettled method, and it has none.`));
|
|
@@ -45,7 +45,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
45
45
|
makeInjectable(target);
|
|
46
46
|
defineStageTypes(target, options.types, 'Observer');
|
|
47
47
|
Reflect.defineMetadata(META.observerClass, true, target);
|
|
48
|
-
};
|
|
48
|
+
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export { Observer };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
|
-
import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
3
|
+
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Marks a class as a service, which controllers and other services inject by its type.
|
|
@@ -27,12 +27,12 @@ import { deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
27
27
|
* @group Decorators
|
|
28
28
|
* @category App
|
|
29
29
|
* @see {@link Inject}
|
|
30
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
30
|
+
* @see {@link https://meocord.dev/docs/4.2/services | Services}
|
|
31
31
|
*/ function Service() {
|
|
32
|
-
return function(target, propertyKey) {
|
|
32
|
+
return declaring(function(target, propertyKey) {
|
|
33
33
|
if (deprecatedOnMethod('@Service', target, propertyKey)) return;
|
|
34
34
|
makeInjectable(target);
|
|
35
|
-
};
|
|
35
|
+
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export { Service };
|
|
@@ -2,7 +2,7 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { assertValidTheme } from '../core/theme-validation.js';
|
|
3
3
|
import { THEMED_CLASSES } from '../core/theme-runtime.js';
|
|
4
4
|
import { copyLayer } from '../core/theme-scope.js';
|
|
5
|
-
import { refuse } from '../util/refusal.util.js';
|
|
5
|
+
import { declaring, refuse } from '../util/refusal.util.js';
|
|
6
6
|
import { META } from '../util/metadata-keys.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -35,7 +35,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
35
35
|
* @group Decorators
|
|
36
36
|
* @category Controllers
|
|
37
37
|
*/ function UseTheme(theme) {
|
|
38
|
-
return function(target, propertyKey) {
|
|
38
|
+
return declaring(function(target, propertyKey) {
|
|
39
39
|
const onClass = propertyKey === undefined;
|
|
40
40
|
const owner = onClass ? target : target.constructor;
|
|
41
41
|
const where = `${owner.name}${onClass ? '' : `.${String(propertyKey)}`}: @UseTheme`;
|
|
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
47
47
|
if (onClass) Reflect.defineMetadata(key, layer, target);
|
|
48
48
|
else Reflect.defineMetadata(key, layer, target, propertyKey);
|
|
49
49
|
THEMED_CLASSES.add(owner);
|
|
50
|
-
};
|
|
50
|
+
});
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export { UseTheme };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
3
|
import { assertStageEntries } from '../core/stage-scope.js';
|
|
4
|
-
import {
|
|
4
|
+
import { declaring, refuseOnClass, refuse, refuseOnMethod } from '../util/refusal.util.js';
|
|
5
5
|
import { META } from '../util/metadata-keys.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -37,9 +37,9 @@ import { META } from '../util/metadata-keys.js';
|
|
|
37
37
|
* @category Pipeline stages
|
|
38
38
|
* @see {@link UsePipe}
|
|
39
39
|
* @see {@link ValidationError}
|
|
40
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
40
|
+
* @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
|
|
41
41
|
*/ function Validate(schema, options = {}) {
|
|
42
|
-
return function(target, propertyKey, _descriptor) {
|
|
42
|
+
return declaring(function(target, propertyKey, _descriptor) {
|
|
43
43
|
refuseOnClass('@Validate', target, propertyKey);
|
|
44
44
|
// Checked where it applies, so the refusal names the handler
|
|
45
45
|
if (typeof schema?.['~standard']?.validate !== 'function') {
|
|
@@ -57,7 +57,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
57
57
|
pipes: options.pipes ?? {}
|
|
58
58
|
};
|
|
59
59
|
Reflect.defineMetadata(META.methodValidation, metadata, target, propertyKey);
|
|
60
|
-
};
|
|
60
|
+
});
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Runs pipes on one value of a handler's input, to turn it into what the handler works with.
|
|
@@ -89,9 +89,9 @@ import { META } from '../util/metadata-keys.js';
|
|
|
89
89
|
* @category Pipeline stages
|
|
90
90
|
* @see {@link Pipe}
|
|
91
91
|
* @see {@link Validate}
|
|
92
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
92
|
+
* @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
|
|
93
93
|
*/ function UsePipe(key, ...pipes) {
|
|
94
|
-
return function(target, propertyKey, _descriptor) {
|
|
94
|
+
return declaring(function(target, propertyKey, _descriptor) {
|
|
95
95
|
refuseOnClass('@UsePipe', target, propertyKey);
|
|
96
96
|
assertStageEntries('@UsePipe', 'pipe', `${target.constructor.name}.${propertyKey}`, pipes);
|
|
97
97
|
// Decorators apply bottom-up, so a higher @UsePipe's pipes go first, in the order they read.
|
|
@@ -103,7 +103,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
103
103
|
})),
|
|
104
104
|
...existing
|
|
105
105
|
], target, propertyKey);
|
|
106
|
-
};
|
|
106
|
+
});
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Marks a class as a pipe, which turns one input value into what the handler works with.
|
|
@@ -129,12 +129,12 @@ import { META } from '../util/metadata-keys.js';
|
|
|
129
129
|
* @group Decorators
|
|
130
130
|
* @category Pipeline stages
|
|
131
131
|
* @see {@link UsePipe}
|
|
132
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
132
|
+
* @see {@link https://meocord.dev/docs/4.2/validation | Validation and pipes}
|
|
133
133
|
*/ function Pipe() {
|
|
134
|
-
return function(target, propertyKey) {
|
|
134
|
+
return declaring(function(target, propertyKey) {
|
|
135
135
|
refuseOnMethod('@Pipe', target, propertyKey);
|
|
136
136
|
makeInjectable(target);
|
|
137
|
-
};
|
|
137
|
+
});
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export { Pipe, UsePipe, Validate };
|
package/dist/esm/package.json.js
CHANGED
|
@@ -79,7 +79,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
|
|
|
79
79
|
* @group Testing
|
|
80
80
|
* @category Inspection
|
|
81
81
|
* @see {@link createTranslator}
|
|
82
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
82
|
+
* @see {@link https://meocord.dev/docs/4.2/localisation | Localisation}
|
|
83
83
|
*/ function expectCompleteCatalog(translator, options = {}) {
|
|
84
84
|
const catalogs = translator[CATALOGS];
|
|
85
85
|
if (!catalogs) throw new Error('expectCompleteCatalog takes a translator made by createTranslator.');
|
|
@@ -3,6 +3,7 @@ import { CooldownStore } from '../common/cooldown-store.js';
|
|
|
3
3
|
|
|
4
4
|
/** Long enough for a store over the network to answer each case, short enough that the suite takes seconds. */ const CASE_TIMEOUT_MS = 10000;
|
|
5
5
|
const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
6
|
+
/** How far a store's clock may run from this process's, as a store's server keeps its own time. */ const CLOCK_SLACK_MS = 50;
|
|
6
7
|
/**
|
|
7
8
|
* Checks that a `CooldownStore` counts calls as `MemoryCooldownStore` does, as a suite of test cases.
|
|
8
9
|
*
|
|
@@ -13,7 +14,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
13
14
|
* The cases cover what a shared store most often gets wrong: a sliding window rather than fixed buckets,
|
|
14
15
|
* `retryAfterMs` from the oldest call still in the window, calls in the same millisecond kept apart, and concurrent
|
|
15
16
|
* calls at the limit where exactly one may pass. They use real time, with windows short enough that the suite takes
|
|
16
|
-
* a few seconds
|
|
17
|
+
* a few seconds and margins wide enough that a correct store passes when timers fire late, as on a busy CI runner,
|
|
18
|
+
* and each case counts under keys of its own. Each window is a whole number of milliseconds, as
|
|
17
19
|
* `@Cooldown` gives a store: it rounds its `seconds` to the millisecond.
|
|
18
20
|
*
|
|
19
21
|
* @param name - What the store is called in the report.
|
|
@@ -30,7 +32,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
30
32
|
* @group Testing
|
|
31
33
|
* @category Inspection
|
|
32
34
|
* @see {@link CooldownStore}
|
|
33
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
35
|
+
* @see {@link https://meocord.dev/docs/4.2/recipes/cooldown-stores | Cooldown stores}
|
|
34
36
|
*/ function testCooldownStore(name, factory, { describe, it, expect }) {
|
|
35
37
|
const run = randomUUID();
|
|
36
38
|
const key = (label)=>`meocord-suite:${run}:${label}`;
|
|
@@ -53,32 +55,38 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
53
55
|
expect(refused.retryAfterMs).toBeLessThanOrEqual(limit.windowMs);
|
|
54
56
|
});
|
|
55
57
|
test('slides its window: a call is allowed once the oldest leaves it, not when a bucket resets', async (store)=>{
|
|
58
|
+
// A second between calls, in a 2 s window, leaves room for a timer that fires late on a busy runner
|
|
56
59
|
const limit = {
|
|
57
60
|
uses: 2,
|
|
58
|
-
windowMs:
|
|
61
|
+
windowMs: 2000
|
|
59
62
|
};
|
|
60
63
|
await store.consume(key('sliding'), limit);
|
|
61
|
-
await sleep(
|
|
64
|
+
await sleep(1000);
|
|
62
65
|
await store.consume(key('sliding'), limit);
|
|
63
66
|
const refused = await store.consume(key('sliding'), limit);
|
|
64
67
|
expect(refused.allowed).toBe(false);
|
|
65
68
|
await sleep(refused.retryAfterMs + 40);
|
|
66
|
-
// The first call has left the window; the second,
|
|
69
|
+
// The first call has left the window; the second, a second younger, still holds its use.
|
|
67
70
|
expect((await store.consume(key('sliding'), limit)).allowed).toBe(true);
|
|
68
71
|
expect((await store.consume(key('sliding'), limit)).allowed).toBe(false);
|
|
69
72
|
});
|
|
70
73
|
test('counts retryAfterMs from the oldest call still in the window', async (store)=>{
|
|
71
74
|
const limit = {
|
|
72
75
|
uses: 2,
|
|
73
|
-
windowMs:
|
|
76
|
+
windowMs: 2000
|
|
74
77
|
};
|
|
78
|
+
const firstSent = Date.now();
|
|
75
79
|
await store.consume(key('retry'), limit);
|
|
76
|
-
|
|
80
|
+
const firstDone = Date.now();
|
|
81
|
+
await sleep(1000);
|
|
82
|
+
const secondSent = Date.now();
|
|
77
83
|
await store.consume(key('retry'), limit);
|
|
78
|
-
// From the oldest call, about 700ms remain; from the newest, about 1000ms would.
|
|
79
84
|
const { retryAfterMs } = await store.consume(key('retry'), limit);
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const thirdDone = Date.now();
|
|
86
|
+
// What is left of the oldest call's window, by the times measured around each call, as timers may fire late;
|
|
87
|
+
// counted from the newest call, nearly the whole window would be left instead
|
|
88
|
+
expect(retryAfterMs).toBeLessThanOrEqual(limit.windowMs - (secondSent - firstDone) + CLOCK_SLACK_MS);
|
|
89
|
+
expect(retryAfterMs).toBeGreaterThanOrEqual(limit.windowMs - (thirdDone - firstSent) - CLOCK_SLACK_MS);
|
|
82
90
|
});
|
|
83
91
|
test('counts each key on its own', async (store)=>{
|
|
84
92
|
const limit = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale,
|
|
2
|
-
import { memberRoles, createMockUser,
|
|
1
|
+
import { Message, Guild, TextChannel, ThreadChannel, GuildChannel, BaseGuildVoiceChannel, MessageReaction, GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale, ClientUser, User, GuildMember, Role, Collection, MessageType, CommandInteraction, AutocompleteInteraction, PermissionsBitField, BaseInteraction, ChannelType, NewsChannel, VoiceChannel, ForumChannel, MediaChannel, DMChannel, BaseChannel, Client, BaseGuild, SnowflakeUtil } from 'discord.js';
|
|
2
|
+
import { createMockChannel, createMockGuild, memberRoles, createMockUser, isRawMember } from './mock-interaction.js';
|
|
3
|
+
import { strictMocks } from './strict-mocks.js';
|
|
3
4
|
|
|
4
5
|
/** A permission set with none, frozen as discord.js freezes the ones it gives. */ const noPermissions = ()=>new PermissionsBitField().freeze();
|
|
5
6
|
/** A property discord.js computes from the others: its own getter runs, reading the mock's values. */ const REAL_GETTER = Symbol('real getter');
|
|
@@ -123,7 +124,14 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
|
|
|
123
124
|
version: ()=>1,
|
|
124
125
|
context: ()=>null,
|
|
125
126
|
// What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
|
|
126
|
-
memberPermissions: (interaction)=>
|
|
127
|
+
memberPermissions: (interaction)=>{
|
|
128
|
+
const { guildId } = interaction;
|
|
129
|
+
const member = interaction.member;
|
|
130
|
+
if (!guildId) return null;
|
|
131
|
+
if (member instanceof GuildMember) return member.permissions;
|
|
132
|
+
// A raw member's permissions are the string Discord sends, read as discord.js reads them
|
|
133
|
+
return isRawMember(member) && !interaction.guild ? new PermissionsBitField(BigInt(member.permissions)).freeze() : null;
|
|
134
|
+
},
|
|
127
135
|
// The bot's permissions where the interaction was made: its member's in the channel, or none outside a server
|
|
128
136
|
appPermissions: (interaction)=>{
|
|
129
137
|
const { guild, channel } = interaction;
|
|
@@ -184,10 +192,55 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
|
|
|
184
192
|
nameAcronym: REAL_GETTER
|
|
185
193
|
})
|
|
186
194
|
]);
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
/**
|
|
196
|
+
* What strict mocks also read as Discord sends it, for discord.js's getters that read it: a server for a channel or
|
|
197
|
+
* thread made without one, a thread's parent text channel in it, and data that is otherwise a placeholder.
|
|
198
|
+
*/ const STRICT_DEFAULTS = new Map([
|
|
199
|
+
defaultsOf(Message, {
|
|
200
|
+
content: ()=>''
|
|
201
|
+
}),
|
|
202
|
+
defaultsOf(Guild, {
|
|
203
|
+
rulesChannelId: ()=>null,
|
|
204
|
+
publicUpdatesChannelId: ()=>null
|
|
205
|
+
}),
|
|
206
|
+
defaultsOf(ThreadChannel, {
|
|
207
|
+
guild: ()=>createMockGuild(),
|
|
208
|
+
guildId: (thread)=>thread.guild.id,
|
|
209
|
+
parentId: (thread)=>{
|
|
210
|
+
const parent = createMockChannel(TextChannel, {
|
|
211
|
+
guild: thread.guild
|
|
212
|
+
});
|
|
213
|
+
thread.guild.channels.cache.set(parent.id, parent);
|
|
214
|
+
return parent.id;
|
|
215
|
+
},
|
|
216
|
+
parent: REAL_GETTER,
|
|
217
|
+
ownerId: snowflake,
|
|
218
|
+
archived: ()=>false,
|
|
219
|
+
locked: ()=>false
|
|
220
|
+
}),
|
|
221
|
+
defaultsOf(GuildChannel, {
|
|
222
|
+
guild: ()=>createMockGuild()
|
|
223
|
+
}),
|
|
224
|
+
// Full once its members reach a user limit, which a voice channel has none of unless given one
|
|
225
|
+
defaultsOf(BaseGuildVoiceChannel, {
|
|
226
|
+
userLimit: ()=>0,
|
|
227
|
+
full: REAL_GETTER
|
|
228
|
+
}),
|
|
229
|
+
defaultsOf(MessageReaction, {
|
|
230
|
+
count: ()=>1
|
|
231
|
+
})
|
|
232
|
+
]);
|
|
233
|
+
/** The default a mock of `target`'s class has for `key`, read from the nearest class that has one, strict ones first. */ function discordDefault(target, key) {
|
|
234
|
+
for (const defaults of strictMocks() ? [
|
|
235
|
+
STRICT_DEFAULTS,
|
|
236
|
+
DEFAULTS
|
|
237
|
+
] : [
|
|
238
|
+
DEFAULTS
|
|
239
|
+
]){
|
|
240
|
+
for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
|
|
241
|
+
const found = defaults.get(proto)?.[key];
|
|
242
|
+
if (found !== undefined) return found;
|
|
243
|
+
}
|
|
191
244
|
}
|
|
192
245
|
return undefined;
|
|
193
246
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
|
|
2
|
-
export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockUser } from './mock-interaction.js';
|
|
3
|
-
export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks } from './mock-fn.js';
|
|
2
|
+
export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser } from './mock-interaction.js';
|
|
3
|
+
export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks, useMockFn } from './mock-fn.js';
|
|
4
|
+
export { useStrictMocks } from './strict-mocks.js';
|
|
5
|
+
export { reportAllStartupErrors } from './startup-errors.js';
|
|
4
6
|
export { findRouteConflicts, resolveRoute } from './routing.js';
|
|
5
7
|
export { createModalFields } from './modal-fields.js';
|
|
6
8
|
export { expectCompleteCatalog } from './catalog.js';
|
|
@@ -2,7 +2,7 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { HandlerExecutionContext } from '../common/execution-context.js';
|
|
3
3
|
import { appStages, handlerStages } from '../core/handler-pipeline.js';
|
|
4
4
|
import { handlerCooldowns } from '../core/cooldown-runner.js';
|
|
5
|
-
import { getMessageHandlers } from '../decorator/controller.decorator.js';
|
|
5
|
+
import { getMessageHandlers, getHandlerRoutes, getDeclaredRoutes } from '../decorator/controller.decorator.js';
|
|
6
6
|
import { appObservers } from '../core/observer-runner.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -37,7 +37,7 @@ import { appObservers } from '../core/observer-runner.js';
|
|
|
37
37
|
* @group Testing
|
|
38
38
|
* @category Inspection
|
|
39
39
|
* @see {@link createExecutionContext}
|
|
40
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
40
|
+
* @see {@link https://meocord.dev/docs/4.2/testing-recipes | Testing recipes}
|
|
41
41
|
*/ function inspectHandler(controller, methodName, options = {}) {
|
|
42
42
|
const context = new HandlerExecutionContext({
|
|
43
43
|
controller,
|
|
@@ -46,7 +46,7 @@ import { appObservers } from '../core/observer-runner.js';
|
|
|
46
46
|
});
|
|
47
47
|
const globals = options.app ? appStages(options.app) : undefined;
|
|
48
48
|
const { guards, interceptors, filters } = handlerStages(controller.prototype, methodName, globals);
|
|
49
|
-
|
|
49
|
+
const inspection = {
|
|
50
50
|
controller,
|
|
51
51
|
methodName,
|
|
52
52
|
guards: Object.freeze([
|
|
@@ -68,6 +68,23 @@ import { appObservers } from '../core/observer-runner.js';
|
|
|
68
68
|
get: (metadata)=>context.get(metadata),
|
|
69
69
|
getAll: (metadata)=>context.getAll(metadata)
|
|
70
70
|
};
|
|
71
|
+
// Read by a test but not compared by toEqual, so an assertion on the result's other fields holds
|
|
72
|
+
Object.defineProperty(inspection, 'inheritedRoutes', {
|
|
73
|
+
value: Object.freeze(inheritedRoutesOf(controller.prototype, methodName)),
|
|
74
|
+
enumerable: false
|
|
75
|
+
});
|
|
76
|
+
return inspection;
|
|
77
|
+
}
|
|
78
|
+
/** The routes `methodName` answers that the class declaring it last doesn't declare for it: those it inherits. */ function inheritedRoutesOf(prototype, methodName) {
|
|
79
|
+
const answered = [
|
|
80
|
+
...new Set(getHandlerRoutes(prototype).filter((route)=>route.method === methodName).map((route)=>route.label))
|
|
81
|
+
];
|
|
82
|
+
// The nearest class with its own decorators on the method; a class that only overrides it declares none
|
|
83
|
+
for(let at = prototype; at && at !== Object.prototype; at = Object.getPrototypeOf(at)){
|
|
84
|
+
const own = getDeclaredRoutes(at).filter((route)=>route.method === methodName).map((route)=>route.label);
|
|
85
|
+
if (own.length > 0) return at === prototype ? answered.filter((label)=>!own.includes(label)) : answered;
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
export { inspectHandler };
|