@e22m4u/ts-rest-router 0.4.3 → 0.5.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/README.md CHANGED
@@ -209,6 +209,7 @@ class UserController {
209
209
  - `@requestCookie` - определенный cookie запроса;
210
210
  - `@requestCookies` - все cookies запроса как объект;
211
211
  - `@requestContext` - доступ к контексту запроса;
212
+ - `@requestContainer` - сервис-контейнер запроса;
212
213
  - `@requestData` - доступ к данным запроса;
213
214
  - `@httpRequest` - экземпляр `IncomingMessage`;
214
215
  - `@httpResponse` - экземпляр `ServerResponse`;
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  postAction: () => postAction,
52
52
  putAction: () => putAction,
53
53
  requestBody: () => requestBody,
54
+ requestContainer: () => requestContainer,
54
55
  requestContext: () => requestContext,
55
56
  requestCookie: () => requestCookie,
56
57
  requestCookies: () => requestCookies,
@@ -602,6 +603,10 @@ function httpResponse() {
602
603
  return requestContext("res");
603
604
  }
604
605
  __name(httpResponse, "httpResponse");
606
+ function requestContainer() {
607
+ return requestContext("container");
608
+ }
609
+ __name(requestContainer, "requestContainer");
605
610
 
606
611
  // dist/esm/controller-registry.js
607
612
  var import_ts_data_schema4 = require("@e22m4u/ts-data-schema");
@@ -955,7 +960,9 @@ var _ControllerRegistry = class _ControllerRegistry extends DebuggableService {
955
960
  debug("No RequestDataMetadata specified for %v argument.", index);
956
961
  }
957
962
  });
958
- const controller = this.getService(controllerCtor);
963
+ if (requestContext2.container.has(controllerCtor))
964
+ throw new import_js_format3.Errorf("The controller %v is already registered, which breaks controller isolation per request.", controllerCtor.name);
965
+ const controller = requestContext2.container.get(controllerCtor);
959
966
  return controller[actionName](...args);
960
967
  };
961
968
  }
@@ -1016,6 +1023,7 @@ var RestRouter = _RestRouter;
1016
1023
  postAction,
1017
1024
  putAction,
1018
1025
  requestBody,
1026
+ requestContainer,
1019
1027
  requestContext,
1020
1028
  requestCookie,
1021
1029
  requestCookies,
@@ -417,7 +417,10 @@ export class ControllerRegistry extends DebuggableService {
417
417
  }
418
418
  });
419
419
  // выполнение операции контроллера
420
- const controller = this.getService(controllerCtor);
420
+ if (requestContext.container.has(controllerCtor))
421
+ throw new Errorf('The controller %v is already registered, which breaks ' +
422
+ 'controller isolation per request.', controllerCtor.name);
423
+ const controller = requestContext.container.get(controllerCtor);
421
424
  return controller[actionName](...args);
422
425
  };
423
426
  }
@@ -14,3 +14,7 @@ export declare function httpRequest(): (target: Prototype<object>, propertyKey:
14
14
  * HttpResponse decorator.
15
15
  */
16
16
  export declare function httpResponse(): (target: Prototype<object>, propertyKey: string, indexOrDescriptor: number) => void;
17
+ /**
18
+ * RequestContainer decorator.
19
+ */
20
+ export declare function requestContainer(): (target: Prototype<object>, propertyKey: string, indexOrDescriptor: number) => void;
@@ -27,3 +27,9 @@ export function httpRequest() {
27
27
  export function httpResponse() {
28
28
  return requestContext('res');
29
29
  }
30
+ /**
31
+ * RequestContainer decorator.
32
+ */
33
+ export function requestContainer() {
34
+ return requestContext('container');
35
+ }
@@ -15,13 +15,10 @@ import { expect } from 'chai';
15
15
  import { httpRequest } from './request-context-decorator.js';
16
16
  import { httpResponse } from './request-context-decorator.js';
17
17
  import { requestContext } from './request-context-decorator.js';
18
+ import { requestContainer } from './request-context-decorator.js';
18
19
  import { RequestContextReflector } from './request-context-reflector.js';
19
20
  describe('requestContext', function () {
20
- it('has aliases', function () {
21
- expect(httpRequest).to.be.instanceOf(Function);
22
- expect(httpResponse).to.be.instanceOf(Function);
23
- });
24
- it('does not require options', function () {
21
+ it('should not require options', function () {
25
22
  class Target {
26
23
  method(prop) { }
27
24
  }
@@ -34,7 +31,7 @@ describe('requestContext', function () {
34
31
  const res = RequestContextReflector.getMetadata(Target, 'method');
35
32
  expect(res.get(0)).to.be.eql({ property: undefined });
36
33
  });
37
- it('sets a given property to the target metadata', function () {
34
+ it('should set the given property to target metadata', function () {
38
35
  class Target {
39
36
  method(prop) { }
40
37
  }
@@ -47,4 +44,49 @@ describe('requestContext', function () {
47
44
  const res = RequestContextReflector.getMetadata(Target, 'method');
48
45
  expect(res.get(0)).to.be.eql({ property: 'res' });
49
46
  });
47
+ describe('httpRequest', function () {
48
+ it('should set the "req" property to target metadata', function () {
49
+ class Target {
50
+ method(prop) { }
51
+ }
52
+ __decorate([
53
+ __param(0, httpRequest()),
54
+ __metadata("design:type", Function),
55
+ __metadata("design:paramtypes", [Object]),
56
+ __metadata("design:returntype", void 0)
57
+ ], Target.prototype, "method", null);
58
+ const res = RequestContextReflector.getMetadata(Target, 'method');
59
+ expect(res.get(0)).to.be.eql({ property: 'req' });
60
+ });
61
+ });
62
+ describe('httpResponse', function () {
63
+ it('should set the "res" property to target metadata', function () {
64
+ class Target {
65
+ method(prop) { }
66
+ }
67
+ __decorate([
68
+ __param(0, httpResponse()),
69
+ __metadata("design:type", Function),
70
+ __metadata("design:paramtypes", [Object]),
71
+ __metadata("design:returntype", void 0)
72
+ ], Target.prototype, "method", null);
73
+ const res = RequestContextReflector.getMetadata(Target, 'method');
74
+ expect(res.get(0)).to.be.eql({ property: 'res' });
75
+ });
76
+ });
77
+ describe('requestContainer', function () {
78
+ it('should set the "container" property to target metadata', function () {
79
+ class Target {
80
+ method(prop) { }
81
+ }
82
+ __decorate([
83
+ __param(0, requestContainer()),
84
+ __metadata("design:type", Function),
85
+ __metadata("design:paramtypes", [Object]),
86
+ __metadata("design:returntype", void 0)
87
+ ], Target.prototype, "method", null);
88
+ const res = RequestContextReflector.getMetadata(Target, 'method');
89
+ expect(res.get(0)).to.be.eql({ property: 'container' });
90
+ });
91
+ });
50
92
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/ts-rest-router",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "REST маршрутизатор на основе контроллеров для TypeScript",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@ import {
13
13
  RouteRegistry,
14
14
  TrieRouter,
15
15
  } from '@e22m4u/js-trie-router';
16
+
16
17
  import {
17
18
  afterAction,
18
19
  beforeAction,
@@ -34,6 +35,7 @@ import {
34
35
  import {expect} from 'chai';
35
36
  import {DataType} from '@e22m4u/ts-data-schema';
36
37
  import {ControllerRegistry} from './controller-registry.js';
38
+ import {Service, ServiceContainer} from '@e22m4u/js-service';
37
39
 
38
40
  const PRE_HANDLER_1 = () => undefined;
39
41
  const PRE_HANDLER_2 = () => undefined;
@@ -969,5 +971,73 @@ describe('ControllerRegistry', function () {
969
971
  await handler(ctx);
970
972
  expect(invoked).to.be.true;
971
973
  });
974
+
975
+ it('should create a new controller instance for each request', async function () {
976
+ @restController()
977
+ class MyStatefulController {
978
+ public readonly instanceId: number;
979
+ constructor() {
980
+ this.instanceId = Math.random();
981
+ }
982
+ @getAction('/test')
983
+ myAction() {
984
+ return this;
985
+ }
986
+ }
987
+ const S = new ControllerRegistry();
988
+ S.addController(MyStatefulController);
989
+ // поиск обработчика
990
+ const router = S.getService(RouteRegistry);
991
+ const req = createRequestMock({method: HttpMethod.GET, path: '/test'});
992
+ const matching = router.matchRouteByRequest(req);
993
+ const handler = matching!.route.handler;
994
+ // симуляция первого запроса
995
+ const req1 = createRequestMock();
996
+ const res1 = createResponseMock();
997
+ const cont1 = new ServiceContainer(S.container);
998
+ const ctx1 = new RequestContext(cont1, req1, res1);
999
+ const controllerInstance1 = (await handler(ctx1)) as MyStatefulController;
1000
+ // симуляция второго запроса
1001
+ const req2 = createRequestMock();
1002
+ const res2 = createResponseMock();
1003
+ const cont2 = new ServiceContainer(S.container);
1004
+ const ctx2 = new RequestContext(cont2, req2, res2);
1005
+ const controllerInstance2 = (await handler(ctx2)) as MyStatefulController;
1006
+ // проверка, что это два разных экземпляра
1007
+ expect(controllerInstance1).to.be.instanceOf(MyStatefulController);
1008
+ expect(controllerInstance2).to.be.instanceOf(MyStatefulController);
1009
+ expect(controllerInstance1).to.not.equal(controllerInstance2);
1010
+ expect(controllerInstance1.instanceId).to.not.equal(
1011
+ controllerInstance2.instanceId,
1012
+ 'Controller instance IDs should be different',
1013
+ );
1014
+ });
1015
+
1016
+ it('should create controller using a request-scoped container parented by the application container', async function () {
1017
+ let counter = 0;
1018
+ const appContainer = new ServiceContainer();
1019
+ @restController()
1020
+ class MyStatefulController extends Service {
1021
+ @getAction('/test')
1022
+ myAction() {
1023
+ expect(this.container).to.be.not.eq(appContainer);
1024
+ expect(this.container.getParent()).to.be.eq(appContainer);
1025
+ counter++;
1026
+ }
1027
+ }
1028
+ const S = new ControllerRegistry(appContainer);
1029
+ S.addController(MyStatefulController);
1030
+ // поиск обработчика
1031
+ const router = S.getService(RouteRegistry);
1032
+ const req = createRequestMock({method: HttpMethod.GET, path: '/test'});
1033
+ const matching = router.matchRouteByRequest(req);
1034
+ const handler = matching!.route.handler;
1035
+ // симуляция запроса
1036
+ const res = createResponseMock();
1037
+ const cont1 = new ServiceContainer(S.container);
1038
+ const ctx1 = new RequestContext(cont1, req, res);
1039
+ await handler(ctx1);
1040
+ expect(counter).to.be.eq(1);
1041
+ });
972
1042
  });
973
1043
  });
@@ -501,7 +501,13 @@ export class ControllerRegistry extends DebuggableService {
501
501
  }
502
502
  });
503
503
  // выполнение операции контроллера
504
- const controller = this.getService(controllerCtor);
504
+ if (requestContext.container.has(controllerCtor))
505
+ throw new Errorf(
506
+ 'The controller %v is already registered, which breaks ' +
507
+ 'controller isolation per request.',
508
+ controllerCtor.name,
509
+ );
510
+ const controller = requestContext.container.get(controllerCtor);
505
511
  return (controller as AnyObject)[actionName](...args);
506
512
  };
507
513
  }
@@ -3,15 +3,11 @@ import {expect} from 'chai';
3
3
  import {httpRequest} from './request-context-decorator.js';
4
4
  import {httpResponse} from './request-context-decorator.js';
5
5
  import {requestContext} from './request-context-decorator.js';
6
+ import {requestContainer} from './request-context-decorator.js';
6
7
  import {RequestContextReflector} from './request-context-reflector.js';
7
8
 
8
9
  describe('requestContext', function () {
9
- it('has aliases', function () {
10
- expect(httpRequest).to.be.instanceOf(Function);
11
- expect(httpResponse).to.be.instanceOf(Function);
12
- });
13
-
14
- it('does not require options', function () {
10
+ it('should not require options', function () {
15
11
  class Target {
16
12
  method(
17
13
  @requestContext()
@@ -22,7 +18,7 @@ describe('requestContext', function () {
22
18
  expect(res.get(0)).to.be.eql({property: undefined});
23
19
  });
24
20
 
25
- it('sets a given property to the target metadata', function () {
21
+ it('should set the given property to target metadata', function () {
26
22
  class Target {
27
23
  method(
28
24
  @requestContext('res')
@@ -32,4 +28,43 @@ describe('requestContext', function () {
32
28
  const res = RequestContextReflector.getMetadata(Target, 'method');
33
29
  expect(res.get(0)).to.be.eql({property: 'res'});
34
30
  });
31
+
32
+ describe('httpRequest', function () {
33
+ it('should set the "req" property to target metadata', function () {
34
+ class Target {
35
+ method(
36
+ @httpRequest()
37
+ prop: unknown,
38
+ ) {}
39
+ }
40
+ const res = RequestContextReflector.getMetadata(Target, 'method');
41
+ expect(res.get(0)).to.be.eql({property: 'req'});
42
+ });
43
+ });
44
+
45
+ describe('httpResponse', function () {
46
+ it('should set the "res" property to target metadata', function () {
47
+ class Target {
48
+ method(
49
+ @httpResponse()
50
+ prop: unknown,
51
+ ) {}
52
+ }
53
+ const res = RequestContextReflector.getMetadata(Target, 'method');
54
+ expect(res.get(0)).to.be.eql({property: 'res'});
55
+ });
56
+ });
57
+
58
+ describe('requestContainer', function () {
59
+ it('should set the "container" property to target metadata', function () {
60
+ class Target {
61
+ method(
62
+ @requestContainer()
63
+ prop: unknown,
64
+ ) {}
65
+ }
66
+ const res = RequestContextReflector.getMetadata(Target, 'method');
67
+ expect(res.get(0)).to.be.eql({property: 'container'});
68
+ });
69
+ });
35
70
  });
@@ -50,3 +50,10 @@ export function httpRequest() {
50
50
  export function httpResponse() {
51
51
  return requestContext('res');
52
52
  }
53
+
54
+ /**
55
+ * RequestContainer decorator.
56
+ */
57
+ export function requestContainer() {
58
+ return requestContext('container');
59
+ }