serverless-event-orchestrator 2.0.1 → 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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +489 -434
  3. package/dist/dispatcher.d.ts +6 -1
  4. package/dist/dispatcher.d.ts.map +1 -1
  5. package/dist/dispatcher.js +66 -7
  6. package/dist/dispatcher.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/types/event-type.enum.d.ts +1 -0
  11. package/dist/types/event-type.enum.d.ts.map +1 -1
  12. package/dist/types/event-type.enum.js +1 -0
  13. package/dist/types/event-type.enum.js.map +1 -1
  14. package/dist/types/routes.d.ts +6 -0
  15. package/dist/types/routes.d.ts.map +1 -1
  16. package/jest.config.js +32 -32
  17. package/package.json +82 -81
  18. package/src/dispatcher.ts +586 -519
  19. package/src/http/body-parser.ts +60 -60
  20. package/src/http/cors.ts +76 -76
  21. package/src/http/index.ts +3 -3
  22. package/src/http/response.ts +209 -209
  23. package/src/identity/extractor.ts +207 -207
  24. package/src/identity/index.ts +2 -2
  25. package/src/identity/jwt-verifier.ts +41 -41
  26. package/src/index.ts +128 -127
  27. package/src/middleware/crm-guard.ts +51 -51
  28. package/src/middleware/index.ts +3 -3
  29. package/src/middleware/init-tenant-context.ts +59 -59
  30. package/src/middleware/tenant-guard.ts +54 -54
  31. package/src/tenant/TenantContext.ts +115 -115
  32. package/src/tenant/helpers.ts +112 -112
  33. package/src/tenant/index.ts +21 -21
  34. package/src/tenant/types.ts +101 -101
  35. package/src/types/event-type.enum.ts +21 -20
  36. package/src/types/index.ts +2 -2
  37. package/src/types/routes.ts +218 -211
  38. package/src/utils/headers.ts +72 -72
  39. package/src/utils/index.ts +2 -2
  40. package/src/utils/path-matcher.ts +84 -84
  41. package/tests/cors.test.ts +133 -133
  42. package/tests/dispatcher.test.ts +795 -715
  43. package/tests/headers.test.ts +99 -99
  44. package/tests/identity.test.ts +301 -301
  45. package/tests/middleware/crm-guard.test.ts +69 -69
  46. package/tests/middleware/init-tenant-context.test.ts +147 -147
  47. package/tests/middleware/tenant-guard.test.ts +100 -100
  48. package/tests/path-matcher.test.ts +102 -102
  49. package/tests/response.test.ts +155 -155
  50. package/tests/tenant/TenantContext.test.ts +134 -134
  51. package/tests/tenant/helpers.test.ts +187 -187
  52. package/tsconfig.json +24 -24
@@ -1,84 +1,84 @@
1
- /**
2
- * Path matching utilities for extracting path parameters
3
- * Supports patterns like /users/{id} and /users/{userId}/posts/{postId}
4
- */
5
-
6
- /**
7
- * Converts a route pattern to a regex and extracts parameter names
8
- * @param pattern - Route pattern like /users/{id}
9
- * @returns Object with regex and parameter names
10
- */
11
- export function patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {
12
- const paramNames: string[] = [];
13
-
14
- // First, handle :paramName format (Express style) BEFORE escaping
15
- // This converts :id to {id} for uniform processing
16
- let normalizedPattern = pattern.replace(/:(\w+)/g, '{$1}');
17
-
18
- // Escape special regex characters except for our parameter syntax
19
- let regexPattern = normalizedPattern
20
- .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
21
- .replace(/\\\{(\w+)\\\}/g, (_, paramName) => {
22
- paramNames.push(paramName);
23
- return '([^/]+)';
24
- });
25
-
26
- // Ensure exact match
27
- regexPattern = `^${regexPattern}$`;
28
-
29
- return {
30
- regex: new RegExp(regexPattern),
31
- paramNames,
32
- };
33
- }
34
-
35
- /**
36
- * Matches a path against a pattern and extracts parameters
37
- * @param pattern - Route pattern like /users/{id}
38
- * @param path - Actual path like /users/123
39
- * @returns Extracted parameters or null if no match
40
- */
41
- export function matchPath(pattern: string, path: string): Record<string, string> | null {
42
- const { regex, paramNames } = patternToRegex(pattern);
43
- const match = path.match(regex);
44
-
45
- if (!match) {
46
- return null;
47
- }
48
-
49
- const params: Record<string, string> = {};
50
- paramNames.forEach((name, index) => {
51
- params[name] = match[index + 1];
52
- });
53
-
54
- return params;
55
- }
56
-
57
- /**
58
- * Checks if a pattern contains path parameters
59
- * @param pattern - Route pattern to check
60
- * @returns True if pattern has parameters
61
- */
62
- export function hasPathParameters(pattern: string): boolean {
63
- // Support both {id} and :id formats
64
- return /\{[\w]+\}/.test(pattern) || /:[\w]+/.test(pattern);
65
- }
66
-
67
- /**
68
- * Normalizes a path by removing trailing slashes and ensuring leading slash
69
- * @param path - Path to normalize
70
- * @returns Normalized path
71
- */
72
- export function normalizePath(path: string): string {
73
- if (!path) return '/';
74
-
75
- // Ensure leading slash
76
- let normalized = path.startsWith('/') ? path : `/${path}`;
77
-
78
- // Remove trailing slash (except for root)
79
- if (normalized.length > 1 && normalized.endsWith('/')) {
80
- normalized = normalized.slice(0, -1);
81
- }
82
-
83
- return normalized;
84
- }
1
+ /**
2
+ * Path matching utilities for extracting path parameters
3
+ * Supports patterns like /users/{id} and /users/{userId}/posts/{postId}
4
+ */
5
+
6
+ /**
7
+ * Converts a route pattern to a regex and extracts parameter names
8
+ * @param pattern - Route pattern like /users/{id}
9
+ * @returns Object with regex and parameter names
10
+ */
11
+ export function patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {
12
+ const paramNames: string[] = [];
13
+
14
+ // First, handle :paramName format (Express style) BEFORE escaping
15
+ // This converts :id to {id} for uniform processing
16
+ let normalizedPattern = pattern.replace(/:(\w+)/g, '{$1}');
17
+
18
+ // Escape special regex characters except for our parameter syntax
19
+ let regexPattern = normalizedPattern
20
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
21
+ .replace(/\\\{(\w+)\\\}/g, (_, paramName) => {
22
+ paramNames.push(paramName);
23
+ return '([^/]+)';
24
+ });
25
+
26
+ // Ensure exact match
27
+ regexPattern = `^${regexPattern}$`;
28
+
29
+ return {
30
+ regex: new RegExp(regexPattern),
31
+ paramNames,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Matches a path against a pattern and extracts parameters
37
+ * @param pattern - Route pattern like /users/{id}
38
+ * @param path - Actual path like /users/123
39
+ * @returns Extracted parameters or null if no match
40
+ */
41
+ export function matchPath(pattern: string, path: string): Record<string, string> | null {
42
+ const { regex, paramNames } = patternToRegex(pattern);
43
+ const match = path.match(regex);
44
+
45
+ if (!match) {
46
+ return null;
47
+ }
48
+
49
+ const params: Record<string, string> = {};
50
+ paramNames.forEach((name, index) => {
51
+ params[name] = match[index + 1];
52
+ });
53
+
54
+ return params;
55
+ }
56
+
57
+ /**
58
+ * Checks if a pattern contains path parameters
59
+ * @param pattern - Route pattern to check
60
+ * @returns True if pattern has parameters
61
+ */
62
+ export function hasPathParameters(pattern: string): boolean {
63
+ // Support both {id} and :id formats
64
+ return /\{[\w]+\}/.test(pattern) || /:[\w]+/.test(pattern);
65
+ }
66
+
67
+ /**
68
+ * Normalizes a path by removing trailing slashes and ensuring leading slash
69
+ * @param path - Path to normalize
70
+ * @returns Normalized path
71
+ */
72
+ export function normalizePath(path: string): string {
73
+ if (!path) return '/';
74
+
75
+ // Ensure leading slash
76
+ let normalized = path.startsWith('/') ? path : `/${path}`;
77
+
78
+ // Remove trailing slash (except for root)
79
+ if (normalized.length > 1 && normalized.endsWith('/')) {
80
+ normalized = normalized.slice(0, -1);
81
+ }
82
+
83
+ return normalized;
84
+ }
@@ -1,133 +1,133 @@
1
- import {
2
- isPreflightRequest,
3
- createPreflightResponse,
4
- applyCorsHeaders,
5
- withCors
6
- } from '../src/http/cors';
7
- import { HttpResponse } from '../src/http/response';
8
-
9
- describe('isPreflightRequest', () => {
10
- it('should return true for OPTIONS request', () => {
11
- const event = { httpMethod: 'OPTIONS' };
12
- expect(isPreflightRequest(event)).toBe(true);
13
- });
14
-
15
- it('should return true for lowercase options', () => {
16
- const event = { httpMethod: 'options' };
17
- expect(isPreflightRequest(event)).toBe(true);
18
- });
19
-
20
- it('should return false for GET request', () => {
21
- const event = { httpMethod: 'GET' };
22
- expect(isPreflightRequest(event)).toBe(false);
23
- });
24
-
25
- it('should return false for POST request', () => {
26
- const event = { httpMethod: 'POST' };
27
- expect(isPreflightRequest(event)).toBe(false);
28
- });
29
-
30
- it('should return false for undefined httpMethod', () => {
31
- const event = {};
32
- expect(isPreflightRequest(event)).toBe(false);
33
- });
34
- });
35
-
36
- describe('createPreflightResponse', () => {
37
- it('should return 204 with default CORS headers', () => {
38
- const response = createPreflightResponse(true);
39
-
40
- expect(response.statusCode).toBe(204);
41
- expect(response.body).toBe('');
42
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
43
- expect(response.headers?.['Access-Control-Allow-Methods']).toContain('GET');
44
- });
45
-
46
- it('should use custom CORS config', () => {
47
- const response = createPreflightResponse({
48
- origins: ['https://myapp.com'],
49
- methods: ['GET', 'POST'],
50
- credentials: true
51
- });
52
-
53
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
54
- expect(response.headers?.['Access-Control-Allow-Methods']).toBe('GET, POST');
55
- expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
56
- });
57
- });
58
-
59
- describe('applyCorsHeaders', () => {
60
- const originalResponse: HttpResponse = {
61
- statusCode: 200,
62
- body: JSON.stringify({ data: 'test' }),
63
- headers: { 'Content-Type': 'application/json' }
64
- };
65
-
66
- it('should add CORS headers to response', () => {
67
- const response = applyCorsHeaders(originalResponse, true);
68
-
69
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
70
- expect(response.headers?.['Content-Type']).toBe('application/json');
71
- });
72
-
73
- it('should not modify response when cors is false', () => {
74
- const response = applyCorsHeaders(originalResponse, false);
75
-
76
- expect(response.headers?.['Access-Control-Allow-Origin']).toBeUndefined();
77
- });
78
-
79
- it('should use custom CORS config', () => {
80
- const response = applyCorsHeaders(originalResponse, {
81
- origins: ['https://example.com'],
82
- maxAge: 3600
83
- });
84
-
85
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://example.com');
86
- expect(response.headers?.['Access-Control-Max-Age']).toBe('3600');
87
- });
88
- });
89
-
90
- describe('withCors', () => {
91
- const mockHandler = jest.fn().mockResolvedValue({
92
- statusCode: 200,
93
- body: JSON.stringify({ success: true })
94
- });
95
-
96
- beforeEach(() => {
97
- mockHandler.mockClear();
98
- });
99
-
100
- it('should handle preflight requests without calling handler', async () => {
101
- const wrappedHandler = withCors(mockHandler, true);
102
-
103
- const event = { eventRaw: { httpMethod: 'OPTIONS' } };
104
- const response = await wrappedHandler(event);
105
-
106
- expect(response.statusCode).toBe(204);
107
- expect(mockHandler).not.toHaveBeenCalled();
108
- });
109
-
110
- it('should call handler and add CORS headers for non-preflight', async () => {
111
- const wrappedHandler = withCors(mockHandler, true);
112
-
113
- const event = { eventRaw: { httpMethod: 'GET' } };
114
- const response = await wrappedHandler(event);
115
-
116
- expect(mockHandler).toHaveBeenCalledTimes(1);
117
- expect(response.statusCode).toBe(200);
118
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
119
- });
120
-
121
- it('should work with custom CORS config', async () => {
122
- const wrappedHandler = withCors(mockHandler, {
123
- origins: ['https://myapp.com'],
124
- credentials: true
125
- });
126
-
127
- const event = { eventRaw: { httpMethod: 'POST' } };
128
- const response = await wrappedHandler(event);
129
-
130
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
131
- expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
132
- });
133
- });
1
+ import {
2
+ isPreflightRequest,
3
+ createPreflightResponse,
4
+ applyCorsHeaders,
5
+ withCors
6
+ } from '../src/http/cors';
7
+ import { HttpResponse } from '../src/http/response';
8
+
9
+ describe('isPreflightRequest', () => {
10
+ it('should return true for OPTIONS request', () => {
11
+ const event = { httpMethod: 'OPTIONS' };
12
+ expect(isPreflightRequest(event)).toBe(true);
13
+ });
14
+
15
+ it('should return true for lowercase options', () => {
16
+ const event = { httpMethod: 'options' };
17
+ expect(isPreflightRequest(event)).toBe(true);
18
+ });
19
+
20
+ it('should return false for GET request', () => {
21
+ const event = { httpMethod: 'GET' };
22
+ expect(isPreflightRequest(event)).toBe(false);
23
+ });
24
+
25
+ it('should return false for POST request', () => {
26
+ const event = { httpMethod: 'POST' };
27
+ expect(isPreflightRequest(event)).toBe(false);
28
+ });
29
+
30
+ it('should return false for undefined httpMethod', () => {
31
+ const event = {};
32
+ expect(isPreflightRequest(event)).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe('createPreflightResponse', () => {
37
+ it('should return 204 with default CORS headers', () => {
38
+ const response = createPreflightResponse(true);
39
+
40
+ expect(response.statusCode).toBe(204);
41
+ expect(response.body).toBe('');
42
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
43
+ expect(response.headers?.['Access-Control-Allow-Methods']).toContain('GET');
44
+ });
45
+
46
+ it('should use custom CORS config', () => {
47
+ const response = createPreflightResponse({
48
+ origins: ['https://myapp.com'],
49
+ methods: ['GET', 'POST'],
50
+ credentials: true
51
+ });
52
+
53
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
54
+ expect(response.headers?.['Access-Control-Allow-Methods']).toBe('GET, POST');
55
+ expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
56
+ });
57
+ });
58
+
59
+ describe('applyCorsHeaders', () => {
60
+ const originalResponse: HttpResponse = {
61
+ statusCode: 200,
62
+ body: JSON.stringify({ data: 'test' }),
63
+ headers: { 'Content-Type': 'application/json' }
64
+ };
65
+
66
+ it('should add CORS headers to response', () => {
67
+ const response = applyCorsHeaders(originalResponse, true);
68
+
69
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
70
+ expect(response.headers?.['Content-Type']).toBe('application/json');
71
+ });
72
+
73
+ it('should not modify response when cors is false', () => {
74
+ const response = applyCorsHeaders(originalResponse, false);
75
+
76
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBeUndefined();
77
+ });
78
+
79
+ it('should use custom CORS config', () => {
80
+ const response = applyCorsHeaders(originalResponse, {
81
+ origins: ['https://example.com'],
82
+ maxAge: 3600
83
+ });
84
+
85
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://example.com');
86
+ expect(response.headers?.['Access-Control-Max-Age']).toBe('3600');
87
+ });
88
+ });
89
+
90
+ describe('withCors', () => {
91
+ const mockHandler = jest.fn().mockResolvedValue({
92
+ statusCode: 200,
93
+ body: JSON.stringify({ success: true })
94
+ });
95
+
96
+ beforeEach(() => {
97
+ mockHandler.mockClear();
98
+ });
99
+
100
+ it('should handle preflight requests without calling handler', async () => {
101
+ const wrappedHandler = withCors(mockHandler, true);
102
+
103
+ const event = { eventRaw: { httpMethod: 'OPTIONS' } };
104
+ const response = await wrappedHandler(event);
105
+
106
+ expect(response.statusCode).toBe(204);
107
+ expect(mockHandler).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('should call handler and add CORS headers for non-preflight', async () => {
111
+ const wrappedHandler = withCors(mockHandler, true);
112
+
113
+ const event = { eventRaw: { httpMethod: 'GET' } };
114
+ const response = await wrappedHandler(event);
115
+
116
+ expect(mockHandler).toHaveBeenCalledTimes(1);
117
+ expect(response.statusCode).toBe(200);
118
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
119
+ });
120
+
121
+ it('should work with custom CORS config', async () => {
122
+ const wrappedHandler = withCors(mockHandler, {
123
+ origins: ['https://myapp.com'],
124
+ credentials: true
125
+ });
126
+
127
+ const event = { eventRaw: { httpMethod: 'POST' } };
128
+ const response = await wrappedHandler(event);
129
+
130
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
131
+ expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
132
+ });
133
+ });