serverless-event-orchestrator 2.2.0 → 2.3.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.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +489 -489
  3. package/dist/dispatcher.d.ts +6 -1
  4. package/dist/dispatcher.d.ts.map +1 -1
  5. package/dist/dispatcher.js +31 -7
  6. package/dist/dispatcher.js.map +1 -1
  7. package/jest.config.js +32 -32
  8. package/package.json +82 -81
  9. package/src/dispatcher.ts +586 -558
  10. package/src/http/body-parser.ts +60 -60
  11. package/src/http/cors.ts +76 -76
  12. package/src/http/index.ts +3 -3
  13. package/src/http/response.ts +209 -209
  14. package/src/identity/extractor.ts +207 -207
  15. package/src/identity/index.ts +2 -2
  16. package/src/identity/jwt-verifier.ts +41 -41
  17. package/src/index.ts +128 -128
  18. package/src/middleware/crm-guard.ts +51 -51
  19. package/src/middleware/index.ts +3 -3
  20. package/src/middleware/init-tenant-context.ts +59 -59
  21. package/src/middleware/tenant-guard.ts +54 -54
  22. package/src/tenant/TenantContext.ts +115 -115
  23. package/src/tenant/helpers.ts +112 -112
  24. package/src/tenant/index.ts +21 -21
  25. package/src/tenant/types.ts +101 -101
  26. package/src/types/event-type.enum.ts +21 -21
  27. package/src/types/index.ts +2 -2
  28. package/src/types/routes.ts +218 -218
  29. package/src/utils/headers.ts +72 -72
  30. package/src/utils/index.ts +2 -2
  31. package/src/utils/path-matcher.ts +84 -84
  32. package/tests/cors.test.ts +133 -133
  33. package/tests/dispatcher.test.ts +795 -715
  34. package/tests/headers.test.ts +99 -99
  35. package/tests/identity.test.ts +301 -301
  36. package/tests/middleware/crm-guard.test.ts +69 -69
  37. package/tests/middleware/init-tenant-context.test.ts +147 -147
  38. package/tests/middleware/tenant-guard.test.ts +100 -100
  39. package/tests/path-matcher.test.ts +102 -102
  40. package/tests/response.test.ts +155 -155
  41. package/tests/tenant/TenantContext.test.ts +134 -134
  42. package/tests/tenant/helpers.test.ts +187 -187
  43. package/tsconfig.json +24 -24
@@ -1,102 +1,102 @@
1
- import {
2
- matchPath,
3
- patternToRegex,
4
- hasPathParameters,
5
- normalizePath
6
- } from '../src/utils/path-matcher';
7
-
8
- describe('patternToRegex', () => {
9
- it('should convert simple pattern without parameters', () => {
10
- const { regex, paramNames } = patternToRegex('/users');
11
- expect(paramNames).toEqual([]);
12
- expect(regex.test('/users')).toBe(true);
13
- expect(regex.test('/users/')).toBe(false);
14
- expect(regex.test('/other')).toBe(false);
15
- });
16
-
17
- it('should extract single parameter name', () => {
18
- const { regex, paramNames } = patternToRegex('/users/{id}');
19
- expect(paramNames).toEqual(['id']);
20
- expect(regex.test('/users/123')).toBe(true);
21
- expect(regex.test('/users/abc-def')).toBe(true);
22
- expect(regex.test('/users/')).toBe(false);
23
- });
24
-
25
- it('should extract multiple parameter names', () => {
26
- const { regex, paramNames } = patternToRegex('/users/{userId}/posts/{postId}');
27
- expect(paramNames).toEqual(['userId', 'postId']);
28
- expect(regex.test('/users/123/posts/456')).toBe(true);
29
- });
30
-
31
- it('should escape special regex characters', () => {
32
- const { regex } = patternToRegex('/api/v1.0/users');
33
- expect(regex.test('/api/v1.0/users')).toBe(true);
34
- expect(regex.test('/api/v1X0/users')).toBe(false);
35
- });
36
- });
37
-
38
- describe('matchPath', () => {
39
- it('should return null for non-matching paths', () => {
40
- expect(matchPath('/users', '/posts')).toBeNull();
41
- expect(matchPath('/users/{id}', '/posts/123')).toBeNull();
42
- });
43
-
44
- it('should return empty params for exact match without parameters', () => {
45
- const result = matchPath('/users', '/users');
46
- expect(result).toEqual({});
47
- });
48
-
49
- it('should extract single path parameter', () => {
50
- const result = matchPath('/users/{id}', '/users/123');
51
- expect(result).toEqual({ id: '123' });
52
- });
53
-
54
- it('should extract multiple path parameters', () => {
55
- const result = matchPath('/users/{userId}/posts/{postId}', '/users/abc/posts/xyz');
56
- expect(result).toEqual({ userId: 'abc', postId: 'xyz' });
57
- });
58
-
59
- it('should handle parameters with special characters', () => {
60
- const result = matchPath('/users/{id}', '/users/user-123_abc');
61
- expect(result).toEqual({ id: 'user-123_abc' });
62
- });
63
-
64
- it('should not match partial paths', () => {
65
- expect(matchPath('/users/{id}', '/users/123/extra')).toBeNull();
66
- expect(matchPath('/users/{id}/posts', '/users/123')).toBeNull();
67
- });
68
- });
69
-
70
- describe('hasPathParameters', () => {
71
- it('should return true for patterns with parameters', () => {
72
- expect(hasPathParameters('/users/{id}')).toBe(true);
73
- expect(hasPathParameters('/users/{userId}/posts/{postId}')).toBe(true);
74
- });
75
-
76
- it('should return false for patterns without parameters', () => {
77
- expect(hasPathParameters('/users')).toBe(false);
78
- expect(hasPathParameters('/users/list')).toBe(false);
79
- });
80
- });
81
-
82
- describe('normalizePath', () => {
83
- it('should add leading slash if missing', () => {
84
- expect(normalizePath('users')).toBe('/users');
85
- });
86
-
87
- it('should remove trailing slash', () => {
88
- expect(normalizePath('/users/')).toBe('/users');
89
- });
90
-
91
- it('should keep root path as is', () => {
92
- expect(normalizePath('/')).toBe('/');
93
- });
94
-
95
- it('should handle empty string', () => {
96
- expect(normalizePath('')).toBe('/');
97
- });
98
-
99
- it('should normalize complex paths', () => {
100
- expect(normalizePath('users/123/')).toBe('/users/123');
101
- });
102
- });
1
+ import {
2
+ matchPath,
3
+ patternToRegex,
4
+ hasPathParameters,
5
+ normalizePath
6
+ } from '../src/utils/path-matcher';
7
+
8
+ describe('patternToRegex', () => {
9
+ it('should convert simple pattern without parameters', () => {
10
+ const { regex, paramNames } = patternToRegex('/users');
11
+ expect(paramNames).toEqual([]);
12
+ expect(regex.test('/users')).toBe(true);
13
+ expect(regex.test('/users/')).toBe(false);
14
+ expect(regex.test('/other')).toBe(false);
15
+ });
16
+
17
+ it('should extract single parameter name', () => {
18
+ const { regex, paramNames } = patternToRegex('/users/{id}');
19
+ expect(paramNames).toEqual(['id']);
20
+ expect(regex.test('/users/123')).toBe(true);
21
+ expect(regex.test('/users/abc-def')).toBe(true);
22
+ expect(regex.test('/users/')).toBe(false);
23
+ });
24
+
25
+ it('should extract multiple parameter names', () => {
26
+ const { regex, paramNames } = patternToRegex('/users/{userId}/posts/{postId}');
27
+ expect(paramNames).toEqual(['userId', 'postId']);
28
+ expect(regex.test('/users/123/posts/456')).toBe(true);
29
+ });
30
+
31
+ it('should escape special regex characters', () => {
32
+ const { regex } = patternToRegex('/api/v1.0/users');
33
+ expect(regex.test('/api/v1.0/users')).toBe(true);
34
+ expect(regex.test('/api/v1X0/users')).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('matchPath', () => {
39
+ it('should return null for non-matching paths', () => {
40
+ expect(matchPath('/users', '/posts')).toBeNull();
41
+ expect(matchPath('/users/{id}', '/posts/123')).toBeNull();
42
+ });
43
+
44
+ it('should return empty params for exact match without parameters', () => {
45
+ const result = matchPath('/users', '/users');
46
+ expect(result).toEqual({});
47
+ });
48
+
49
+ it('should extract single path parameter', () => {
50
+ const result = matchPath('/users/{id}', '/users/123');
51
+ expect(result).toEqual({ id: '123' });
52
+ });
53
+
54
+ it('should extract multiple path parameters', () => {
55
+ const result = matchPath('/users/{userId}/posts/{postId}', '/users/abc/posts/xyz');
56
+ expect(result).toEqual({ userId: 'abc', postId: 'xyz' });
57
+ });
58
+
59
+ it('should handle parameters with special characters', () => {
60
+ const result = matchPath('/users/{id}', '/users/user-123_abc');
61
+ expect(result).toEqual({ id: 'user-123_abc' });
62
+ });
63
+
64
+ it('should not match partial paths', () => {
65
+ expect(matchPath('/users/{id}', '/users/123/extra')).toBeNull();
66
+ expect(matchPath('/users/{id}/posts', '/users/123')).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe('hasPathParameters', () => {
71
+ it('should return true for patterns with parameters', () => {
72
+ expect(hasPathParameters('/users/{id}')).toBe(true);
73
+ expect(hasPathParameters('/users/{userId}/posts/{postId}')).toBe(true);
74
+ });
75
+
76
+ it('should return false for patterns without parameters', () => {
77
+ expect(hasPathParameters('/users')).toBe(false);
78
+ expect(hasPathParameters('/users/list')).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('normalizePath', () => {
83
+ it('should add leading slash if missing', () => {
84
+ expect(normalizePath('users')).toBe('/users');
85
+ });
86
+
87
+ it('should remove trailing slash', () => {
88
+ expect(normalizePath('/users/')).toBe('/users');
89
+ });
90
+
91
+ it('should keep root path as is', () => {
92
+ expect(normalizePath('/')).toBe('/');
93
+ });
94
+
95
+ it('should handle empty string', () => {
96
+ expect(normalizePath('')).toBe('/');
97
+ });
98
+
99
+ it('should normalize complex paths', () => {
100
+ expect(normalizePath('users/123/')).toBe('/users/123');
101
+ });
102
+ });
@@ -1,155 +1,155 @@
1
- import {
2
- HttpStatus,
3
- DefaultResponseCode,
4
- createStandardResponse,
5
- successResponse,
6
- createdResponse,
7
- badRequestResponse,
8
- unauthorizedResponse,
9
- forbiddenResponse,
10
- notFoundResponse,
11
- conflictResponse,
12
- validationErrorResponse,
13
- internalErrorResponse,
14
- customErrorResponse
15
- } from '../src/http/response';
16
-
17
- describe('createStandardResponse', () => {
18
- it('should create response with data', () => {
19
- const response = createStandardResponse(200, { user: { id: 1 } });
20
- const body = JSON.parse(response.body);
21
-
22
- expect(response.statusCode).toBe(200);
23
- expect(body.status).toBe(200);
24
- expect(body.code).toBe(DefaultResponseCode.SUCCESS);
25
- expect(body.data).toEqual({ user: { id: 1 } });
26
- });
27
-
28
- it('should create response with custom code', () => {
29
- const response = createStandardResponse(200, null, 'CUSTOM_CODE');
30
- const body = JSON.parse(response.body);
31
-
32
- expect(body.code).toBe('CUSTOM_CODE');
33
- });
34
-
35
- it('should create response with message', () => {
36
- const response = createStandardResponse(400, undefined, undefined, 'Error message');
37
- const body = JSON.parse(response.body);
38
-
39
- expect(body.message).toBe('Error message');
40
- });
41
-
42
- it('should include custom headers', () => {
43
- const response = createStandardResponse(200, null, undefined, undefined, {
44
- 'X-Custom-Header': 'value'
45
- });
46
-
47
- expect(response.headers?.['X-Custom-Header']).toBe('value');
48
- expect(response.headers?.['Content-Type']).toBe('application/json');
49
- });
50
- });
51
-
52
- describe('successResponse', () => {
53
- it('should return 200 with data', () => {
54
- const response = successResponse({ items: [1, 2, 3] });
55
- const body = JSON.parse(response.body);
56
-
57
- expect(response.statusCode).toBe(200);
58
- expect(body.code).toBe(DefaultResponseCode.SUCCESS);
59
- expect(body.data).toEqual({ items: [1, 2, 3] });
60
- });
61
-
62
- it('should return 200 without data', () => {
63
- const response = successResponse();
64
- expect(response.statusCode).toBe(200);
65
- });
66
- });
67
-
68
- describe('createdResponse', () => {
69
- it('should return 201 with data', () => {
70
- const response = createdResponse({ id: 123 });
71
- const body = JSON.parse(response.body);
72
-
73
- expect(response.statusCode).toBe(201);
74
- expect(body.code).toBe(DefaultResponseCode.CREATED);
75
- expect(body.data).toEqual({ id: 123 });
76
- });
77
- });
78
-
79
- describe('error responses', () => {
80
- it('badRequestResponse should return 400', () => {
81
- const response = badRequestResponse('Invalid input');
82
- const body = JSON.parse(response.body);
83
-
84
- expect(response.statusCode).toBe(400);
85
- expect(body.code).toBe(DefaultResponseCode.BAD_REQUEST);
86
- expect(body.message).toBe('Invalid input');
87
- });
88
-
89
- it('unauthorizedResponse should return 401', () => {
90
- const response = unauthorizedResponse('Token expired');
91
-
92
- expect(response.statusCode).toBe(401);
93
- });
94
-
95
- it('forbiddenResponse should return 403', () => {
96
- const response = forbiddenResponse('Access denied');
97
-
98
- expect(response.statusCode).toBe(403);
99
- });
100
-
101
- it('notFoundResponse should return 404', () => {
102
- const response = notFoundResponse('User not found');
103
-
104
- expect(response.statusCode).toBe(404);
105
- });
106
-
107
- it('conflictResponse should return 409', () => {
108
- const response = conflictResponse('Email already exists');
109
-
110
- expect(response.statusCode).toBe(409);
111
- });
112
-
113
- it('validationErrorResponse should return 422', () => {
114
- const response = validationErrorResponse('Invalid email format');
115
-
116
- expect(response.statusCode).toBe(422);
117
- });
118
-
119
- it('internalErrorResponse should return 500', () => {
120
- const response = internalErrorResponse('Something went wrong');
121
-
122
- expect(response.statusCode).toBe(500);
123
- });
124
- });
125
-
126
- describe('customErrorResponse', () => {
127
- enum MyErrorCodes {
128
- USER_SUSPENDED = 'USER_SUSPENDED',
129
- QUOTA_EXCEEDED = 'QUOTA_EXCEEDED'
130
- }
131
-
132
- const codeToStatus: Record<MyErrorCodes, HttpStatus> = {
133
- [MyErrorCodes.USER_SUSPENDED]: HttpStatus.FORBIDDEN,
134
- [MyErrorCodes.QUOTA_EXCEEDED]: HttpStatus.UNPROCESSABLE_ENTITY
135
- };
136
-
137
- it('should map custom code to correct status', () => {
138
- const response = customErrorResponse(
139
- MyErrorCodes.USER_SUSPENDED,
140
- 'Account suspended',
141
- codeToStatus
142
- );
143
- const body = JSON.parse(response.body);
144
-
145
- expect(response.statusCode).toBe(403);
146
- expect(body.code).toBe('USER_SUSPENDED');
147
- expect(body.message).toBe('Account suspended');
148
- });
149
-
150
- it('should default to 500 for unmapped codes', () => {
151
- const response = customErrorResponse('UNKNOWN_CODE', 'Unknown error');
152
-
153
- expect(response.statusCode).toBe(500);
154
- });
155
- });
1
+ import {
2
+ HttpStatus,
3
+ DefaultResponseCode,
4
+ createStandardResponse,
5
+ successResponse,
6
+ createdResponse,
7
+ badRequestResponse,
8
+ unauthorizedResponse,
9
+ forbiddenResponse,
10
+ notFoundResponse,
11
+ conflictResponse,
12
+ validationErrorResponse,
13
+ internalErrorResponse,
14
+ customErrorResponse
15
+ } from '../src/http/response';
16
+
17
+ describe('createStandardResponse', () => {
18
+ it('should create response with data', () => {
19
+ const response = createStandardResponse(200, { user: { id: 1 } });
20
+ const body = JSON.parse(response.body);
21
+
22
+ expect(response.statusCode).toBe(200);
23
+ expect(body.status).toBe(200);
24
+ expect(body.code).toBe(DefaultResponseCode.SUCCESS);
25
+ expect(body.data).toEqual({ user: { id: 1 } });
26
+ });
27
+
28
+ it('should create response with custom code', () => {
29
+ const response = createStandardResponse(200, null, 'CUSTOM_CODE');
30
+ const body = JSON.parse(response.body);
31
+
32
+ expect(body.code).toBe('CUSTOM_CODE');
33
+ });
34
+
35
+ it('should create response with message', () => {
36
+ const response = createStandardResponse(400, undefined, undefined, 'Error message');
37
+ const body = JSON.parse(response.body);
38
+
39
+ expect(body.message).toBe('Error message');
40
+ });
41
+
42
+ it('should include custom headers', () => {
43
+ const response = createStandardResponse(200, null, undefined, undefined, {
44
+ 'X-Custom-Header': 'value'
45
+ });
46
+
47
+ expect(response.headers?.['X-Custom-Header']).toBe('value');
48
+ expect(response.headers?.['Content-Type']).toBe('application/json');
49
+ });
50
+ });
51
+
52
+ describe('successResponse', () => {
53
+ it('should return 200 with data', () => {
54
+ const response = successResponse({ items: [1, 2, 3] });
55
+ const body = JSON.parse(response.body);
56
+
57
+ expect(response.statusCode).toBe(200);
58
+ expect(body.code).toBe(DefaultResponseCode.SUCCESS);
59
+ expect(body.data).toEqual({ items: [1, 2, 3] });
60
+ });
61
+
62
+ it('should return 200 without data', () => {
63
+ const response = successResponse();
64
+ expect(response.statusCode).toBe(200);
65
+ });
66
+ });
67
+
68
+ describe('createdResponse', () => {
69
+ it('should return 201 with data', () => {
70
+ const response = createdResponse({ id: 123 });
71
+ const body = JSON.parse(response.body);
72
+
73
+ expect(response.statusCode).toBe(201);
74
+ expect(body.code).toBe(DefaultResponseCode.CREATED);
75
+ expect(body.data).toEqual({ id: 123 });
76
+ });
77
+ });
78
+
79
+ describe('error responses', () => {
80
+ it('badRequestResponse should return 400', () => {
81
+ const response = badRequestResponse('Invalid input');
82
+ const body = JSON.parse(response.body);
83
+
84
+ expect(response.statusCode).toBe(400);
85
+ expect(body.code).toBe(DefaultResponseCode.BAD_REQUEST);
86
+ expect(body.message).toBe('Invalid input');
87
+ });
88
+
89
+ it('unauthorizedResponse should return 401', () => {
90
+ const response = unauthorizedResponse('Token expired');
91
+
92
+ expect(response.statusCode).toBe(401);
93
+ });
94
+
95
+ it('forbiddenResponse should return 403', () => {
96
+ const response = forbiddenResponse('Access denied');
97
+
98
+ expect(response.statusCode).toBe(403);
99
+ });
100
+
101
+ it('notFoundResponse should return 404', () => {
102
+ const response = notFoundResponse('User not found');
103
+
104
+ expect(response.statusCode).toBe(404);
105
+ });
106
+
107
+ it('conflictResponse should return 409', () => {
108
+ const response = conflictResponse('Email already exists');
109
+
110
+ expect(response.statusCode).toBe(409);
111
+ });
112
+
113
+ it('validationErrorResponse should return 422', () => {
114
+ const response = validationErrorResponse('Invalid email format');
115
+
116
+ expect(response.statusCode).toBe(422);
117
+ });
118
+
119
+ it('internalErrorResponse should return 500', () => {
120
+ const response = internalErrorResponse('Something went wrong');
121
+
122
+ expect(response.statusCode).toBe(500);
123
+ });
124
+ });
125
+
126
+ describe('customErrorResponse', () => {
127
+ enum MyErrorCodes {
128
+ USER_SUSPENDED = 'USER_SUSPENDED',
129
+ QUOTA_EXCEEDED = 'QUOTA_EXCEEDED'
130
+ }
131
+
132
+ const codeToStatus: Record<MyErrorCodes, HttpStatus> = {
133
+ [MyErrorCodes.USER_SUSPENDED]: HttpStatus.FORBIDDEN,
134
+ [MyErrorCodes.QUOTA_EXCEEDED]: HttpStatus.UNPROCESSABLE_ENTITY
135
+ };
136
+
137
+ it('should map custom code to correct status', () => {
138
+ const response = customErrorResponse(
139
+ MyErrorCodes.USER_SUSPENDED,
140
+ 'Account suspended',
141
+ codeToStatus
142
+ );
143
+ const body = JSON.parse(response.body);
144
+
145
+ expect(response.statusCode).toBe(403);
146
+ expect(body.code).toBe('USER_SUSPENDED');
147
+ expect(body.message).toBe('Account suspended');
148
+ });
149
+
150
+ it('should default to 500 for unmapped codes', () => {
151
+ const response = customErrorResponse('UNKNOWN_CODE', 'Unknown error');
152
+
153
+ expect(response.statusCode).toBe(500);
154
+ });
155
+ });