@e22m4u/ts-rest-router 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. package/.c8rc +9 -0
  2. package/.commitlintrc +5 -0
  3. package/.editorconfig +13 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +6 -0
  6. package/.mocharc.json +5 -0
  7. package/.prettierrc +7 -0
  8. package/LICENSE +21 -0
  9. package/README-ru.md +41 -0
  10. package/README.md +41 -0
  11. package/build-cjs.js +16 -0
  12. package/dist/cjs/index.cjs +692 -0
  13. package/dist/esm/controller-registry.d.ts +65 -0
  14. package/dist/esm/controller-registry.js +281 -0
  15. package/dist/esm/controller-registry.spec.d.ts +1 -0
  16. package/dist/esm/controller-registry.spec.js +719 -0
  17. package/dist/esm/debuggable-service.d.ts +18 -0
  18. package/dist/esm/debuggable-service.js +23 -0
  19. package/dist/esm/debuggable-service.spec.d.ts +1 -0
  20. package/dist/esm/debuggable-service.spec.js +16 -0
  21. package/dist/esm/decorators/action/action-decorator.d.ts +53 -0
  22. package/dist/esm/decorators/action/action-decorator.js +66 -0
  23. package/dist/esm/decorators/action/action-decorator.spec.d.ts +1 -0
  24. package/dist/esm/decorators/action/action-decorator.spec.js +59 -0
  25. package/dist/esm/decorators/action/action-metadata.d.ts +23 -0
  26. package/dist/esm/decorators/action/action-metadata.js +5 -0
  27. package/dist/esm/decorators/action/action-reflector.d.ts +22 -0
  28. package/dist/esm/decorators/action/action-reflector.js +29 -0
  29. package/dist/esm/decorators/action/action-reflector.spec.d.ts +1 -0
  30. package/dist/esm/decorators/action/action-reflector.spec.js +84 -0
  31. package/dist/esm/decorators/action/index.d.ts +3 -0
  32. package/dist/esm/decorators/action/index.js +3 -0
  33. package/dist/esm/decorators/controller/controller-decorator.d.ts +13 -0
  34. package/dist/esm/decorators/controller/controller-decorator.js +20 -0
  35. package/dist/esm/decorators/controller/controller-decorator.spec.d.ts +1 -0
  36. package/dist/esm/decorators/controller/controller-decorator.spec.js +53 -0
  37. package/dist/esm/decorators/controller/controller-metadata.d.ts +17 -0
  38. package/dist/esm/decorators/controller/controller-metadata.js +5 -0
  39. package/dist/esm/decorators/controller/controller-reflector.d.ts +20 -0
  40. package/dist/esm/decorators/controller/controller-reflector.js +24 -0
  41. package/dist/esm/decorators/controller/controller-reflector.spec.d.ts +1 -0
  42. package/dist/esm/decorators/controller/controller-reflector.spec.js +45 -0
  43. package/dist/esm/decorators/controller/index.d.ts +3 -0
  44. package/dist/esm/decorators/controller/index.js +3 -0
  45. package/dist/esm/decorators/index.d.ts +4 -0
  46. package/dist/esm/decorators/index.js +4 -0
  47. package/dist/esm/decorators/request-context/index.d.ts +3 -0
  48. package/dist/esm/decorators/request-context/index.js +3 -0
  49. package/dist/esm/decorators/request-context/request-context-decorator.d.ts +17 -0
  50. package/dist/esm/decorators/request-context/request-context-decorator.js +32 -0
  51. package/dist/esm/decorators/request-context/request-context-decorator.spec.d.ts +1 -0
  52. package/dist/esm/decorators/request-context/request-context-decorator.spec.js +59 -0
  53. package/dist/esm/decorators/request-context/request-context-metadata.d.ts +17 -0
  54. package/dist/esm/decorators/request-context/request-context-metadata.js +5 -0
  55. package/dist/esm/decorators/request-context/request-context-reflector.d.ts +24 -0
  56. package/dist/esm/decorators/request-context/request-context-reflector.js +31 -0
  57. package/dist/esm/decorators/request-context/request-context-reflector.spec.d.ts +1 -0
  58. package/dist/esm/decorators/request-context/request-context-reflector.spec.js +59 -0
  59. package/dist/esm/decorators/request-data/index.d.ts +3 -0
  60. package/dist/esm/decorators/request-data/index.js +3 -0
  61. package/dist/esm/decorators/request-data/request-data-decorator.d.ts +28 -0
  62. package/dist/esm/decorators/request-data/request-data-decorator.js +84 -0
  63. package/dist/esm/decorators/request-data/request-data-decorator.spec.d.ts +1 -0
  64. package/dist/esm/decorators/request-data/request-data-decorator.spec.js +534 -0
  65. package/dist/esm/decorators/request-data/request-data-metadata.d.ts +29 -0
  66. package/dist/esm/decorators/request-data/request-data-metadata.js +16 -0
  67. package/dist/esm/decorators/request-data/request-data-reflector.d.ts +24 -0
  68. package/dist/esm/decorators/request-data/request-data-reflector.js +31 -0
  69. package/dist/esm/decorators/request-data/request-data-reflector.spec.d.ts +1 -0
  70. package/dist/esm/decorators/request-data/request-data-reflector.spec.js +60 -0
  71. package/dist/esm/errors/index.d.ts +1 -0
  72. package/dist/esm/errors/index.js +1 -0
  73. package/dist/esm/errors/not-a-controller-error.d.ts +12 -0
  74. package/dist/esm/errors/not-a-controller-error.js +14 -0
  75. package/dist/esm/index.d.ts +5 -0
  76. package/dist/esm/index.js +5 -0
  77. package/dist/esm/rest-router.d.ts +19 -0
  78. package/dist/esm/rest-router.js +24 -0
  79. package/dist/esm/types.d.ts +57 -0
  80. package/dist/esm/types.js +2 -0
  81. package/dist/esm/utils/capitalize.d.ts +6 -0
  82. package/dist/esm/utils/capitalize.js +8 -0
  83. package/dist/esm/utils/capitalize.spec.d.ts +1 -0
  84. package/dist/esm/utils/capitalize.spec.js +8 -0
  85. package/dist/esm/utils/create-debugger.d.ts +11 -0
  86. package/dist/esm/utils/create-debugger.js +15 -0
  87. package/dist/esm/utils/create-debugger.spec.d.ts +1 -0
  88. package/dist/esm/utils/create-debugger.spec.js +8 -0
  89. package/dist/esm/utils/create-error.d.ts +10 -0
  90. package/dist/esm/utils/create-error.js +13 -0
  91. package/dist/esm/utils/create-error.spec.d.ts +1 -0
  92. package/dist/esm/utils/create-error.spec.js +8 -0
  93. package/dist/esm/utils/index.d.ts +4 -0
  94. package/dist/esm/utils/index.js +4 -0
  95. package/dist/esm/utils/to-camel-case.d.ts +6 -0
  96. package/dist/esm/utils/to-camel-case.js +11 -0
  97. package/dist/esm/utils/to-camel-case.spec.d.ts +1 -0
  98. package/dist/esm/utils/to-camel-case.spec.js +10 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -0
  100. package/eslint.config.js +43 -0
  101. package/package.json +74 -0
  102. package/src/controller-registry.spec.ts +592 -0
  103. package/src/controller-registry.ts +355 -0
  104. package/src/debuggable-service.spec.ts +18 -0
  105. package/src/debuggable-service.ts +27 -0
  106. package/src/decorators/action/action-decorator.spec.ts +42 -0
  107. package/src/decorators/action/action-decorator.ts +100 -0
  108. package/src/decorators/action/action-metadata.ts +28 -0
  109. package/src/decorators/action/action-reflector.spec.ts +84 -0
  110. package/src/decorators/action/action-reflector.ts +38 -0
  111. package/src/decorators/action/index.ts +3 -0
  112. package/src/decorators/controller/controller-decorator.spec.ts +41 -0
  113. package/src/decorators/controller/controller-decorator.ts +29 -0
  114. package/src/decorators/controller/controller-metadata.ts +21 -0
  115. package/src/decorators/controller/controller-reflector.spec.ts +45 -0
  116. package/src/decorators/controller/controller-reflector.ts +28 -0
  117. package/src/decorators/controller/index.ts +3 -0
  118. package/src/decorators/index.ts +4 -0
  119. package/src/decorators/request-context/index.ts +3 -0
  120. package/src/decorators/request-context/request-context-decorator.spec.ts +41 -0
  121. package/src/decorators/request-context/request-context-decorator.ts +57 -0
  122. package/src/decorators/request-context/request-context-metadata.ts +21 -0
  123. package/src/decorators/request-context/request-context-reflector.spec.ts +77 -0
  124. package/src/decorators/request-context/request-context-reflector.ts +57 -0
  125. package/src/decorators/request-data/index.ts +3 -0
  126. package/src/decorators/request-data/request-data-decorator.spec.ts +477 -0
  127. package/src/decorators/request-data/request-data-decorator.ts +106 -0
  128. package/src/decorators/request-data/request-data-metadata.ts +34 -0
  129. package/src/decorators/request-data/request-data-reflector.spec.ts +78 -0
  130. package/src/decorators/request-data/request-data-reflector.ts +57 -0
  131. package/src/errors/index.ts +1 -0
  132. package/src/errors/not-a-controller-error.ts +15 -0
  133. package/src/index.ts +5 -0
  134. package/src/rest-router.ts +31 -0
  135. package/src/types.ts +59 -0
  136. package/src/utils/capitalize.spec.ts +9 -0
  137. package/src/utils/capitalize.ts +8 -0
  138. package/src/utils/create-debugger.spec.ts +9 -0
  139. package/src/utils/create-debugger.ts +21 -0
  140. package/src/utils/create-error.spec.ts +9 -0
  141. package/src/utils/create-error.ts +19 -0
  142. package/src/utils/index.ts +4 -0
  143. package/src/utils/to-camel-case.spec.ts +11 -0
  144. package/src/utils/to-camel-case.ts +11 -0
  145. package/tsconfig.json +17 -0
@@ -0,0 +1,355 @@
1
+ import {AnyObject} from './types.js';
2
+ import {Constructor} from './types.js';
3
+ import {Errorf} from '@e22m4u/js-format';
4
+ import {TrieRouter} from '@e22m4u/js-trie-router';
5
+ import {RouteHandler} from '@e22m4u/js-trie-router';
6
+ import {DataValidator} from '@e22m4u/ts-data-schema';
7
+ import {DataTypeCaster} from '@e22m4u/ts-data-schema';
8
+ import {ActionReflector} from './decorators/index.js';
9
+ import {NotAControllerError} from './errors/index.js';
10
+ import {RequestContext} from '@e22m4u/js-trie-router';
11
+ import {RoutePreHandler} from '@e22m4u/js-trie-router';
12
+ import {RoutePostHandler} from '@e22m4u/js-trie-router';
13
+ import {RequestDataSource} from './decorators/index.js';
14
+ import {ControllerMetadata} from './decorators/index.js';
15
+ import {DebuggableService} from './debuggable-service.js';
16
+ import {ControllerReflector} from './decorators/index.js';
17
+ import {RequestDataReflector} from './decorators/index.js';
18
+ import {RequestContextReflector} from './decorators/index.js';
19
+
20
+ /**
21
+ * Controller root options.
22
+ */
23
+ export type ControllerRootOptions = {
24
+ pathPrefix?: string;
25
+ before?: RoutePreHandler | RoutePreHandler[];
26
+ after?: RoutePostHandler | RoutePostHandler[];
27
+ };
28
+
29
+ /**
30
+ * Controller registry.
31
+ */
32
+ export class ControllerRegistry extends DebuggableService {
33
+ /**
34
+ * Controllers.
35
+ */
36
+ controllers = new Set();
37
+
38
+ /**
39
+ * Add controller.
40
+ *
41
+ * @param ctor
42
+ * @param options
43
+ */
44
+ addController<T extends object>(
45
+ ctor: Constructor<T>,
46
+ options?: ControllerRootOptions,
47
+ ): this {
48
+ // проверка повторной регистрации помогает
49
+ // заметить ошибку в коде, который использует
50
+ // интерфейс данного сервиса
51
+ if (this.hasController(ctor))
52
+ throw new Errorf('The controller %v is already registered.');
53
+ // так как контроллером может быть любой
54
+ // класс, выполняется проверка на наличие
55
+ // метаданных применяемых декоратором
56
+ const controllerMd = ControllerReflector.getMetadata(ctor);
57
+ if (!controllerMd) throw new NotAControllerError(ctor);
58
+ this.debug('Adding controller %s.', ctor.name);
59
+ // определение префикса применяемого
60
+ // к маршрутам контроллера
61
+ const pathPrefix = this.getPathPrefixByControllerMetadata(
62
+ controllerMd,
63
+ options,
64
+ );
65
+ this.debug('Path prefix is %v.', pathPrefix);
66
+ // подготовка pre-обработчиков
67
+ const preHandlers = this.getPreHandlersByControllerMetadata(
68
+ controllerMd,
69
+ options,
70
+ );
71
+ this.debug('%v total pre-handlers found.', preHandlers.length);
72
+ // подготовка post-обработчиков
73
+ const postHandlers = this.getPostHandlersByControllerMetadata(
74
+ controllerMd,
75
+ options,
76
+ );
77
+ this.debug('%v total post-handlers found.', postHandlers.length);
78
+ // обход всех операций контроллера
79
+ // для определения маршрутов
80
+ const actionsMd = ActionReflector.getMetadata(ctor);
81
+ this.debug('%v actions found.', actionsMd.size);
82
+ const router = this.getService(TrieRouter);
83
+ actionsMd.forEach((actionMd, actionName) => {
84
+ this.debug('Adding route for %s.%s.', ctor.name, actionName);
85
+ // подготовка пути маршрута с префиксом
86
+ this.debug('Route path is %v.', actionMd.path);
87
+ const prefixedRoutePath = `${pathPrefix}/${actionMd.path}`.replace(
88
+ /\/\//g,
89
+ '/',
90
+ );
91
+ this.debug('Prefixed route path is %v.', prefixedRoutePath);
92
+ // подготовка pre-обработчиков операции
93
+ const actionPreHandlers = Array.isArray(actionMd.before)
94
+ ? actionMd.before
95
+ : actionMd.before
96
+ ? [actionMd.before]
97
+ : [];
98
+ this.debug('%v action pre-handlers found.', actionPreHandlers.length);
99
+ const mergedPreHandlers = [...preHandlers, ...actionPreHandlers];
100
+ // подготовка post-обработчиков операции
101
+ const actionPostHandlers = Array.isArray(actionMd.after)
102
+ ? actionMd.after
103
+ : actionMd.after
104
+ ? [actionMd.after]
105
+ : [];
106
+ this.debug('%v action post-handlers found.', actionPostHandlers.length);
107
+ const mergedPostHandlers = [...postHandlers, ...actionPostHandlers];
108
+ // подготовка обработчика маршрута
109
+ const routeHandler = this.createRouteHandler(ctor, actionName);
110
+ router.defineRoute({
111
+ method: actionMd.method,
112
+ path: prefixedRoutePath,
113
+ preHandler: mergedPreHandlers,
114
+ handler: routeHandler,
115
+ postHandler: mergedPostHandlers,
116
+ });
117
+ this.debug(
118
+ 'Route %s %v is added.',
119
+ actionMd.method.toUpperCase(),
120
+ prefixedRoutePath,
121
+ );
122
+ });
123
+ this.controllers.add(ctor);
124
+ return this;
125
+ }
126
+
127
+ /**
128
+ * Has controller.
129
+ *
130
+ * @param ctor
131
+ */
132
+ hasController<T extends object>(ctor: Constructor<T>) {
133
+ return this.controllers.has(ctor);
134
+ }
135
+
136
+ /**
137
+ * Get path prefix by controller metadata.
138
+ *
139
+ * @param controllerMd
140
+ * @param options
141
+ */
142
+ getPathPrefixByControllerMetadata(
143
+ controllerMd: ControllerMetadata,
144
+ options?: ControllerRootOptions,
145
+ ) {
146
+ const rootPathPrefix = options?.pathPrefix || '';
147
+ this.debug('Root path prefix is %v.', rootPathPrefix);
148
+ const controllerPathPrefix = controllerMd.path || '';
149
+ this.debug('Controller path prefix is %v.', controllerPathPrefix);
150
+ const mergedPathPrefix = `/${rootPathPrefix}/${controllerPathPrefix}`
151
+ .replace(/\/\//g, '/')
152
+ .replace(/\/$/, '');
153
+ this.debug('Merged path prefix is %v.', mergedPathPrefix);
154
+ return mergedPathPrefix;
155
+ }
156
+
157
+ /**
158
+ * Get pre-handlers by controller metadata.
159
+ *
160
+ * @param controllerMd
161
+ * @param options
162
+ */
163
+ getPreHandlersByControllerMetadata(
164
+ controllerMd: ControllerMetadata,
165
+ options?: ControllerRootOptions,
166
+ ) {
167
+ // подготовка дополнительных
168
+ // pre-обработчиков запроса
169
+ let rootPreHandlers: RoutePreHandler[] = [];
170
+ if (options?.before)
171
+ rootPreHandlers = Array.isArray(options?.before)
172
+ ? options.before
173
+ : [options.before];
174
+ this.debug('%v root pre-handlers found.', rootPreHandlers.length);
175
+ // подготовка pre-обработчиков
176
+ // запроса контроллера
177
+ let ctlPreHandlers: RoutePreHandler[] = [];
178
+ if (controllerMd.before)
179
+ ctlPreHandlers = Array.isArray(controllerMd.before)
180
+ ? controllerMd.before
181
+ : [controllerMd.before];
182
+ this.debug('%v controller pre-handlers found.', ctlPreHandlers.length);
183
+ // подготовка объединенного набора
184
+ // pre-обработчиков запроса
185
+ const mergedPreHandlers = [...rootPreHandlers, ...ctlPreHandlers];
186
+ this.debug('%v merged pre-handlers.', mergedPreHandlers.length);
187
+ return mergedPreHandlers;
188
+ }
189
+
190
+ /**
191
+ * Get post-handlers by controller metadata.
192
+ *
193
+ * @param controllerMd
194
+ * @param options
195
+ */
196
+ getPostHandlersByControllerMetadata(
197
+ controllerMd: ControllerMetadata,
198
+ options?: ControllerRootOptions,
199
+ ) {
200
+ // подготовка дополнительных
201
+ // post-обработчиков запроса
202
+ let rootPostHandlers: RoutePostHandler[] = [];
203
+ if (options?.after)
204
+ rootPostHandlers = Array.isArray(options.after)
205
+ ? options.after
206
+ : [options.after];
207
+ this.debug('%v root post-handlers found.', rootPostHandlers.length);
208
+ // подготовка post-обработчиков
209
+ // запроса контроллера
210
+ let ctlPostHandlers: RoutePostHandler[] = [];
211
+ if (controllerMd.after)
212
+ ctlPostHandlers = Array.isArray(controllerMd.after)
213
+ ? controllerMd.after
214
+ : [controllerMd.after];
215
+ this.debug('%v controller post-handlers found.', ctlPostHandlers.length);
216
+ // подготовка объединенного набора
217
+ // post-обработчиков запроса
218
+ const mergedPostHandlers = [...rootPostHandlers, ...ctlPostHandlers];
219
+ this.debug('%v merged post-handlers.', mergedPostHandlers.length);
220
+ return mergedPostHandlers;
221
+ }
222
+
223
+ /**
224
+ * Create route handler.
225
+ *
226
+ * @param controllerCtor
227
+ * @param actionName
228
+ * @protected
229
+ */
230
+ createRouteHandler<T extends object>(
231
+ controllerCtor: Constructor<T>,
232
+ actionName: string,
233
+ ): RouteHandler {
234
+ this.debug(
235
+ 'Creating route handler for %s.%s.',
236
+ controllerCtor.name,
237
+ actionName,
238
+ );
239
+ const requestContextMetadataMap = RequestContextReflector.getMetadata(
240
+ controllerCtor,
241
+ actionName,
242
+ );
243
+ const requestDataMetadataMap = RequestDataReflector.getMetadata(
244
+ controllerCtor,
245
+ actionName,
246
+ );
247
+ const argsNumber = controllerCtor.prototype[actionName].length;
248
+ const dataTypeCaster = this.getService(DataTypeCaster);
249
+ const dataValidator = this.getService(DataValidator);
250
+ return (requestContext: RequestContext) => {
251
+ this.debug(
252
+ 'Executing route handler for %s.%s.',
253
+ controllerCtor.name,
254
+ actionName,
255
+ );
256
+ const args = Array(argsNumber).map((value, index) => {
257
+ if (value != null) return value;
258
+ // заполнение аргументов операции
259
+ // значениями из контекста запроса
260
+ const requestContextMd = requestContextMetadataMap.get(index);
261
+ if (requestContextMd != null) {
262
+ this.debug('Argument %v has request context metadata.', index);
263
+ // если свойство контекста не определено,
264
+ // то используем весь объект контекста
265
+ // в качестве значения текущего аргумента
266
+ if (requestContextMd.property == null) {
267
+ this.debug('Request context property is not specified.');
268
+ this.debug('Argument %v is set to %v.', index, requestContext);
269
+ return requestContext;
270
+ }
271
+ // если свойство контекста определено,
272
+ // то используем значение этого свойства
273
+ // в качестве текущего аргумента
274
+ const propName = requestContextMd.property;
275
+ const propValue = requestContext[propName];
276
+ this.debug('Request context property is %v.', propName);
277
+ this.debug('Argument %v is set to %v.', index, propValue);
278
+ return propValue;
279
+ } else {
280
+ this.debug(
281
+ 'No RequestContextMetadata specified for %v argument.',
282
+ index,
283
+ );
284
+ }
285
+ // заполнение аргументов операции
286
+ // значениями из данных запроса
287
+ const requestDataMd = requestDataMetadataMap.get(index);
288
+ if (requestDataMd != null) {
289
+ this.debug('Argument %v has request data metadata.', index);
290
+ // получение данных
291
+ // согласно источнику
292
+ let data: unknown;
293
+ switch (requestDataMd.source) {
294
+ case RequestDataSource.PARAMS:
295
+ data = requestContext.params;
296
+ break;
297
+ case RequestDataSource.QUERY:
298
+ data = requestContext.query;
299
+ break;
300
+ case RequestDataSource.HEADERS:
301
+ data = requestContext.headers;
302
+ break;
303
+ case RequestDataSource.COOKIE:
304
+ data = requestContext.cookie;
305
+ break;
306
+ case RequestDataSource.BODY:
307
+ data = requestContext.body;
308
+ break;
309
+ }
310
+ this.debug('Request data source is %v.', requestDataMd.source);
311
+ // при наличии схемы данных выполняется
312
+ // их конвертация и валидация
313
+ if (requestDataMd.schema) {
314
+ data = dataTypeCaster.cast(data, requestDataMd.schema, {
315
+ noTypeCastError: true,
316
+ sourcePath: requestDataMd.source,
317
+ });
318
+ this.debug('Data type casting is passed.');
319
+ dataValidator.validate(
320
+ data,
321
+ requestDataMd.schema,
322
+ requestDataMd.source,
323
+ );
324
+ this.debug('Data validation is passed.');
325
+ }
326
+ // если свойство данных не определено,
327
+ // то используем весь объекта данных
328
+ // в качестве значения текущего аргумента
329
+ if (requestDataMd.property == null) {
330
+ this.debug('Request data property is not specified.');
331
+ this.debug('Argument %v is set to %v.', index, data);
332
+ return data;
333
+ }
334
+ // если свойство данных определено,
335
+ // то используем значение этого свойства
336
+ // в качестве текущего аргумента
337
+ const dataAsObject = data as Record<string, unknown>;
338
+ const propName = requestDataMd.property;
339
+ const propValue = dataAsObject[propName];
340
+ this.debug('Request data property is %v.', propName);
341
+ this.debug('Argument %v is set to %v.', index, propValue);
342
+ return propValue;
343
+ } else {
344
+ this.debug(
345
+ 'No RequestDataMetadata specified for %v argument.',
346
+ index,
347
+ );
348
+ }
349
+ });
350
+ // выполнение операции контроллера
351
+ const controller = this.getService(controllerCtor);
352
+ return (controller as AnyObject)[actionName](...args);
353
+ };
354
+ }
355
+ }
@@ -0,0 +1,18 @@
1
+ import {expect} from 'chai';
2
+ import {describe} from 'mocha';
3
+ import {Service} from '@e22m4u/js-service';
4
+ import {DebuggableService} from './debuggable-service.js';
5
+
6
+ describe('DebuggableService', function () {
7
+ it('has the debug method', function () {
8
+ const res = new DebuggableService();
9
+ expect(typeof res.debug).to.be.eq('function');
10
+ });
11
+
12
+ describe('constructor', function () {
13
+ it('extends the Service class', function () {
14
+ const res = new DebuggableService();
15
+ expect(res).to.be.instanceof(Service);
16
+ });
17
+ });
18
+ });
@@ -0,0 +1,27 @@
1
+ import {Debugger} from './utils/index.js';
2
+ import {Service} from '@e22m4u/js-service';
3
+ import {toCamelCase} from './utils/index.js';
4
+ import {createDebugger} from './utils/index.js';
5
+ import {ServiceContainer} from '@e22m4u/js-service';
6
+
7
+ /**
8
+ * Service.
9
+ */
10
+ export class DebuggableService extends Service {
11
+ /**
12
+ * Debug.
13
+ */
14
+ debug: Debugger;
15
+
16
+ /**
17
+ * Constructor.
18
+ *
19
+ * @param container
20
+ */
21
+ constructor(container?: ServiceContainer) {
22
+ super(container);
23
+ const serviceName = toCamelCase(this.constructor.name);
24
+ this.debug = createDebugger(serviceName);
25
+ this.debug('%v is created.', this.constructor);
26
+ }
27
+ }
@@ -0,0 +1,42 @@
1
+ import {expect} from 'chai';
2
+ import {action} from './action-decorator.js';
3
+ import {HttpMethod} from '@e22m4u/js-trie-router';
4
+ import {ActionReflector} from './action-reflector.js';
5
+
6
+ describe('action', function () {
7
+ it('sets given options to the target metadata', function () {
8
+ const method = HttpMethod.GET;
9
+ const path = 'myPath';
10
+ const before = () => undefined;
11
+ const after = () => undefined;
12
+ const customOption = 'customOption';
13
+ class Target {
14
+ @action({method, path, before, after, customOption})
15
+ method() {}
16
+ }
17
+ const res = ActionReflector.getMetadata(Target);
18
+ expect(res.get('method')).to.be.eql({
19
+ propertyKey: 'method',
20
+ method,
21
+ path,
22
+ before,
23
+ after,
24
+ customOption,
25
+ });
26
+ });
27
+
28
+ it('overrides a given "propertyKey" option by the target method name', function () {
29
+ const method = HttpMethod.GET;
30
+ const path = 'myPath';
31
+ class Target {
32
+ @action({propertyKey: 'myMethod', method, path})
33
+ method() {}
34
+ }
35
+ const res = ActionReflector.getMetadata(Target);
36
+ expect(res.get('method')).to.be.eql({
37
+ propertyKey: 'method',
38
+ method,
39
+ path,
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,100 @@
1
+ import {Flatten} from '../../types.js';
2
+ import {Prototype} from '../../types.js';
3
+ import {Constructor} from '../../types.js';
4
+ import {HttpMethod} from '@e22m4u/js-trie-router';
5
+ import {ActionMetadata} from './action-metadata.js';
6
+ import {ActionReflector} from './action-reflector.js';
7
+ import {DecoratorTargetType} from '@e22m4u/ts-reflector';
8
+ import {getDecoratorTargetType} from '@e22m4u/ts-reflector';
9
+
10
+ /**
11
+ * Action options.
12
+ */
13
+ export type ActionOptions = Flatten<Omit<ActionMetadata, 'propertyKey'>>;
14
+
15
+ /**
16
+ * Action decorator.
17
+ *
18
+ * @param options
19
+ */
20
+ export function action<T extends object>(options: ActionOptions) {
21
+ return function (
22
+ target: Prototype<T>,
23
+ propertyKey: string,
24
+ descriptor: PropertyDescriptor,
25
+ ) {
26
+ const decoratorType = getDecoratorTargetType(
27
+ target,
28
+ propertyKey,
29
+ descriptor,
30
+ );
31
+ if (decoratorType !== DecoratorTargetType.INSTANCE_METHOD)
32
+ throw new Error(
33
+ '@action decorator is only supported on an instance method.',
34
+ );
35
+ const metadata = {
36
+ ...options,
37
+ propertyKey,
38
+ } as ActionMetadata;
39
+ ActionReflector.setMetadata(
40
+ metadata,
41
+ target.constructor as Constructor<T>,
42
+ propertyKey,
43
+ );
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Action alias options.
49
+ */
50
+ type ActionAliasOptions = Flatten<Omit<ActionOptions, 'method' | 'path'>>;
51
+
52
+ /**
53
+ * Get decorator.
54
+ *
55
+ * @param path
56
+ * @param options
57
+ */
58
+ export const get = (path: string, options?: ActionAliasOptions) => {
59
+ return action({...options, path, method: HttpMethod.GET});
60
+ };
61
+
62
+ /**
63
+ * Post decorator.
64
+ *
65
+ * @param path
66
+ * @param options
67
+ */
68
+ export const post = (path: string, options?: ActionAliasOptions) => {
69
+ return action({...options, path, method: HttpMethod.POST});
70
+ };
71
+
72
+ /**
73
+ * Put decorator.
74
+ *
75
+ * @param path
76
+ * @param options
77
+ */
78
+ export const put = (path: string, options?: ActionAliasOptions) => {
79
+ return action({...options, path, method: HttpMethod.PUT});
80
+ };
81
+
82
+ /**
83
+ * Patch decorator.
84
+ *
85
+ * @param path
86
+ * @param options
87
+ */
88
+ export const patch = (path: string, options?: ActionAliasOptions) => {
89
+ return action({...options, path, method: HttpMethod.PATCH});
90
+ };
91
+
92
+ /**
93
+ * Del decorator.
94
+ *
95
+ * @param path
96
+ * @param options
97
+ */
98
+ export const del = (path: string, options?: ActionAliasOptions) => {
99
+ return action({...options, path, method: HttpMethod.DELETE});
100
+ };
@@ -0,0 +1,28 @@
1
+ import {MetadataKey} from '@e22m4u/ts-reflector';
2
+ import {HttpMethod} from '@e22m4u/js-trie-router';
3
+ import {RoutePreHandler} from '@e22m4u/js-trie-router';
4
+ import {RoutePostHandler} from '@e22m4u/js-trie-router';
5
+
6
+ /**
7
+ * Action metadata.
8
+ */
9
+ export type ActionMetadata = {
10
+ propertyKey: string;
11
+ method: HttpMethod;
12
+ path: string;
13
+ before?: RoutePreHandler | RoutePreHandler[];
14
+ after?: RoutePostHandler | RoutePostHandler[];
15
+ [option: string]: unknown | undefined;
16
+ };
17
+
18
+ /**
19
+ * Action metadata map.
20
+ */
21
+ export type ActionMetadataMap = Map<string, ActionMetadata>;
22
+
23
+ /**
24
+ * Actions metadata key.
25
+ */
26
+ export const ACTIONS_METADATA_KEY = new MetadataKey<ActionMetadataMap>(
27
+ 'actionsMetadataKey',
28
+ );
@@ -0,0 +1,84 @@
1
+ import {expect} from 'chai';
2
+ import {describe} from 'mocha';
3
+ import {Reflector} from '@e22m4u/ts-reflector';
4
+ import {HttpMethod} from '@e22m4u/js-trie-router';
5
+ import {ActionReflector} from './action-reflector.js';
6
+ import {ACTIONS_METADATA_KEY} from './action-metadata.js';
7
+
8
+ describe('ActionReflector', function () {
9
+ describe('setMetadata', function () {
10
+ it('sets a given value as target metadata', function () {
11
+ class Target {}
12
+ const md1 = {
13
+ propertyKey: 'propertyKey1',
14
+ method: HttpMethod.GET,
15
+ path: '/foo',
16
+ };
17
+ const md2 = {
18
+ propertyKey: 'propertyKey2',
19
+ method: HttpMethod.GET,
20
+ path: '/bar',
21
+ };
22
+ ActionReflector.setMetadata(md1, Target, 'propertyKey1');
23
+ ActionReflector.setMetadata(md2, Target, 'propertyKey2');
24
+ const res = Reflector.getOwnMetadata(ACTIONS_METADATA_KEY, Target);
25
+ expect(res).to.be.instanceof(Map);
26
+ expect(res!.get('propertyKey1')).to.be.eq(md1);
27
+ expect(res!.get('propertyKey2')).to.be.eq(md2);
28
+ });
29
+
30
+ it('overrides existing metadata', function () {
31
+ class Target {}
32
+ const md1 = {
33
+ propertyKey: 'propertyKey',
34
+ method: HttpMethod.GET,
35
+ path: '/foo',
36
+ };
37
+ const md2 = {
38
+ propertyKey: 'propertyKey',
39
+ method: HttpMethod.POST,
40
+ path: '/bar',
41
+ };
42
+ ActionReflector.setMetadata(md1, Target, 'propertyKey');
43
+ const res1 = Reflector.getOwnMetadata(ACTIONS_METADATA_KEY, Target);
44
+ expect(res1).to.be.instanceof(Map);
45
+ expect(res1!.get('propertyKey')).to.be.eq(md1);
46
+ ActionReflector.setMetadata(md2, Target, 'propertyKey');
47
+ const res2 = Reflector.getOwnMetadata(ACTIONS_METADATA_KEY, Target);
48
+ expect(res2).to.be.instanceof(Map);
49
+ expect(res2!.get('propertyKey')).to.be.eq(md2);
50
+ });
51
+ });
52
+
53
+ describe('getMetadata', function () {
54
+ it('returns an existing metadata of the target', function () {
55
+ class Target {}
56
+ const md1 = {
57
+ propertyKey: 'propertyKey1',
58
+ method: HttpMethod.GET,
59
+ path: '/foo',
60
+ };
61
+ const md2 = {
62
+ propertyKey: 'propertyKey2',
63
+ method: HttpMethod.GET,
64
+ path: '/bar',
65
+ };
66
+ const mdMap = new Map([
67
+ ['propertyKey1', md1],
68
+ ['propertyKey2', md2],
69
+ ]);
70
+ Reflector.defineMetadata(ACTIONS_METADATA_KEY, mdMap, Target);
71
+ const res = ActionReflector.getMetadata(Target);
72
+ expect(res).to.be.instanceof(Map);
73
+ expect(res!.get('propertyKey1')).to.be.eq(md1);
74
+ expect(res!.get('propertyKey2')).to.be.eq(md2);
75
+ });
76
+
77
+ it('returns an empty map if no metadata', function () {
78
+ class Target {}
79
+ const res = ActionReflector.getMetadata(Target);
80
+ expect(res).to.be.instanceof(Map);
81
+ expect(res).to.be.empty;
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,38 @@
1
+ import {Constructor} from '../../types.js';
2
+ import {Reflector} from '@e22m4u/ts-reflector';
3
+ import {ActionMetadata} from './action-metadata.js';
4
+ import {ActionMetadataMap} from './action-metadata.js';
5
+ import {ACTIONS_METADATA_KEY} from './action-metadata.js';
6
+
7
+ /**
8
+ * Action reflector.
9
+ */
10
+ export class ActionReflector {
11
+ /**
12
+ * Set metadata.
13
+ *
14
+ * @param metadata
15
+ * @param target
16
+ * @param propertyKey
17
+ */
18
+ static setMetadata(
19
+ metadata: ActionMetadata,
20
+ target: Constructor,
21
+ propertyKey: string,
22
+ ) {
23
+ const oldMap = Reflector.getOwnMetadata(ACTIONS_METADATA_KEY, target);
24
+ const newMap = new Map(oldMap);
25
+ newMap.set(propertyKey, metadata);
26
+ Reflector.defineMetadata(ACTIONS_METADATA_KEY, newMap, target);
27
+ }
28
+
29
+ /**
30
+ * Get metadata.
31
+ *
32
+ * @param target
33
+ */
34
+ static getMetadata(target: Constructor): ActionMetadataMap {
35
+ const metadata = Reflector.getOwnMetadata(ACTIONS_METADATA_KEY, target);
36
+ return metadata ?? new Map();
37
+ }
38
+ }