@facetlayer/prism-framework 0.4.0 → 0.4.1

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 (130) hide show
  1. package/README.md +176 -8
  2. package/dist/Errors.d.ts +38 -0
  3. package/dist/Errors.d.ts.map +1 -0
  4. package/dist/Metrics.d.ts +5 -0
  5. package/dist/Metrics.d.ts.map +1 -0
  6. package/dist/RequestContext.d.ts +17 -0
  7. package/dist/RequestContext.d.ts.map +1 -0
  8. package/dist/ServiceDefinition.d.ts +16 -0
  9. package/dist/ServiceDefinition.d.ts.map +1 -0
  10. package/dist/app/PrismApp.d.ts +31 -0
  11. package/dist/app/PrismApp.d.ts.map +1 -0
  12. package/dist/app/callEndpoint.d.ts +13 -0
  13. package/dist/app/callEndpoint.d.ts.map +1 -0
  14. package/dist/app/validateApp.d.ts +20 -0
  15. package/dist/app/validateApp.d.ts.map +1 -0
  16. package/dist/authorization/AuthSource.d.ts +8 -0
  17. package/dist/authorization/AuthSource.d.ts.map +1 -0
  18. package/dist/authorization/Authorization.d.ts +24 -0
  19. package/dist/authorization/Authorization.d.ts.map +1 -0
  20. package/dist/authorization/Resource.d.ts +5 -0
  21. package/dist/authorization/Resource.d.ts.map +1 -0
  22. package/dist/authorization/index.d.ts +5 -0
  23. package/dist/authorization/index.d.ts.map +1 -0
  24. package/dist/cli.js +1 -1
  25. package/dist/databases/DatabaseInitializationOptions.d.ts +9 -0
  26. package/dist/databases/DatabaseInitializationOptions.d.ts.map +1 -0
  27. package/dist/databases/DatabaseSetup.d.ts +3 -0
  28. package/dist/databases/DatabaseSetup.d.ts.map +1 -0
  29. package/dist/endpoints/createEndpoint.d.ts +4 -0
  30. package/dist/endpoints/createEndpoint.d.ts.map +1 -0
  31. package/dist/endpoints/getEffectiveOperationId.d.ts +19 -0
  32. package/dist/endpoints/getEffectiveOperationId.d.ts.map +1 -0
  33. package/dist/env/Env.d.ts +2 -0
  34. package/dist/env/Env.d.ts.map +1 -0
  35. package/dist/index.d.ts +34 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1364 -0
  38. package/dist/launch/launchConfig.d.ts +18 -0
  39. package/dist/launch/launchConfig.d.ts.map +1 -0
  40. package/dist/logging/index.d.ts +9 -0
  41. package/dist/logging/index.d.ts.map +1 -0
  42. package/dist/sse/ConnectionManager.d.ts +23 -0
  43. package/dist/sse/ConnectionManager.d.ts.map +1 -0
  44. package/dist/stdin/StdinServer.d.ts +38 -0
  45. package/dist/stdin/StdinServer.d.ts.map +1 -0
  46. package/dist/web/EndpointListing.d.ts +3 -0
  47. package/dist/web/EndpointListing.d.ts.map +1 -0
  48. package/dist/web/ExpressAppSetup.d.ts +18 -0
  49. package/dist/web/ExpressAppSetup.d.ts.map +1 -0
  50. package/dist/web/ExpressEndpointSetup.d.ts +31 -0
  51. package/dist/web/ExpressEndpointSetup.d.ts.map +1 -0
  52. package/dist/web/SseResponse.d.ts +15 -0
  53. package/dist/web/SseResponse.d.ts.map +1 -0
  54. package/dist/web/ViteIntegration.d.ts +19 -0
  55. package/dist/web/ViteIntegration.d.ts.map +1 -0
  56. package/dist/web/corsMiddleware.d.ts +14 -0
  57. package/dist/web/corsMiddleware.d.ts.map +1 -0
  58. package/dist/web/localhostOnlyMiddleware.d.ts +3 -0
  59. package/dist/web/localhostOnlyMiddleware.d.ts.map +1 -0
  60. package/dist/web/openapi/OpenAPI.d.ts +37 -0
  61. package/dist/web/openapi/OpenAPI.d.ts.map +1 -0
  62. package/dist/web/openapi/validateServicesForOpenapi.d.ts +32 -0
  63. package/dist/web/openapi/validateServicesForOpenapi.d.ts.map +1 -0
  64. package/dist/web/requestContextMiddleware.d.ts +3 -0
  65. package/dist/web/requestContextMiddleware.d.ts.map +1 -0
  66. package/docs/authorization.md +281 -0
  67. package/docs/cors-setup.md +172 -0
  68. package/docs/creating-services.md +220 -0
  69. package/docs/database-setup.md +134 -0
  70. package/docs/endpoint-tools.md +1 -11
  71. package/docs/env-files.md +12 -1
  72. package/docs/error-handling.md +70 -0
  73. package/docs/getting-started.md +22 -12
  74. package/docs/launch-configuration.md +223 -0
  75. package/docs/overview.md +62 -0
  76. package/docs/server-setup.md +144 -0
  77. package/docs/source-directory-organization.md +115 -0
  78. package/docs/stdin-protocol.md +176 -0
  79. package/package.json +42 -9
  80. package/src/Errors.ts +120 -0
  81. package/src/Metrics.ts +53 -0
  82. package/src/RequestContext.ts +36 -0
  83. package/src/ServiceDefinition.ts +35 -0
  84. package/src/__tests__/Authorization.test.ts +350 -0
  85. package/src/__tests__/Errors.test.ts +378 -0
  86. package/src/__tests__/ListEndpoints.test.ts +98 -0
  87. package/src/__tests__/PrismApp.test.ts +274 -0
  88. package/src/__tests__/RequestContext.test.ts +295 -0
  89. package/src/__tests__/SseResponse.test.ts +189 -0
  90. package/src/__tests__/StdinServer.test.ts +304 -0
  91. package/src/__tests__/corsMiddleware.test.ts +293 -0
  92. package/src/__tests__/createEndpoint.test.ts +412 -0
  93. package/src/__tests__/validateApp.test.ts +206 -0
  94. package/src/app/PrismApp.ts +117 -0
  95. package/src/app/callEndpoint.ts +55 -0
  96. package/src/app/validateApp.ts +78 -0
  97. package/src/authorization/AuthSource.ts +14 -0
  98. package/src/authorization/Authorization.ts +78 -0
  99. package/src/authorization/Resource.ts +8 -0
  100. package/src/authorization/index.ts +4 -0
  101. package/src/databases/DatabaseInitializationOptions.ts +9 -0
  102. package/src/databases/DatabaseSetup.ts +19 -0
  103. package/src/endpoints/createEndpoint.ts +39 -0
  104. package/src/endpoints/getEffectiveOperationId.ts +90 -0
  105. package/src/env/Env.ts +23 -0
  106. package/src/index.ts +78 -0
  107. package/src/launch/launchConfig.ts +59 -0
  108. package/src/list-endpoints-command.ts +1 -1
  109. package/src/logging/index.ts +25 -0
  110. package/src/sse/ConnectionManager.ts +79 -0
  111. package/src/stdin/StdinServer.ts +129 -0
  112. package/src/web/EndpointListing.ts +166 -0
  113. package/src/web/ExpressAppSetup.ts +125 -0
  114. package/src/web/ExpressEndpointSetup.ts +178 -0
  115. package/src/web/SseResponse.ts +78 -0
  116. package/src/web/ViteIntegration.ts +72 -0
  117. package/src/web/__tests__/OpenAPI.invalidZodSchemas.test.ts +250 -0
  118. package/src/web/corsMiddleware.ts +63 -0
  119. package/src/web/localhostOnlyMiddleware.ts +19 -0
  120. package/src/web/openapi/OpenAPI.ts +248 -0
  121. package/src/web/openapi/validateServicesForOpenapi.ts +76 -0
  122. package/src/web/requestContextMiddleware.ts +25 -0
  123. package/.claude/settings.local.json +0 -20
  124. package/CHANGELOG +0 -28
  125. package/CLAUDE.md +0 -44
  126. package/build.mts +0 -8
  127. package/test/call-command.test.ts +0 -96
  128. package/test/generate-api-clients.test.ts +0 -33
  129. package/test/generate-api-clients.test.ts.disabled +0 -75
  130. package/tsconfig.json +0 -21
@@ -0,0 +1,274 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { PrismApp } from '../app/PrismApp.ts';
3
+ import { ServiceDefinition } from '../ServiceDefinition.ts';
4
+ import { EndpointDefinition } from '../web/ExpressEndpointSetup.ts';
5
+
6
+ describe('PrismApp', () => {
7
+ describe('constructor', () => {
8
+ it('should create app with default values when no config provided', () => {
9
+ const app = new PrismApp();
10
+
11
+ expect(app.name).toBe('Prism App');
12
+ expect(app.description).toBe('');
13
+ expect(app.services).toEqual([]);
14
+ expect(app.listAllEndpoints()).toEqual([]);
15
+ });
16
+
17
+ it('should create app with empty config object', () => {
18
+ const app = new PrismApp({});
19
+
20
+ expect(app.name).toBe('Prism App');
21
+ expect(app.description).toBe('');
22
+ expect(app.services).toEqual([]);
23
+ });
24
+
25
+ it('should create app with custom name and description', () => {
26
+ const app = new PrismApp({
27
+ name: 'My API',
28
+ description: 'A test API',
29
+ });
30
+
31
+ expect(app.name).toBe('My API');
32
+ expect(app.description).toBe('A test API');
33
+ });
34
+
35
+ it('should register initial services from config', () => {
36
+ const endpoint: EndpointDefinition = {
37
+ method: 'GET',
38
+ path: '/test',
39
+ handler: async () => ({}),
40
+ };
41
+
42
+ const app = new PrismApp({
43
+ name: 'Test App',
44
+ services: [
45
+ { name: 'test-service', endpoints: [endpoint] },
46
+ ],
47
+ });
48
+
49
+ expect(app.services).toHaveLength(1);
50
+ expect(app.services[0].name).toBe('test-service');
51
+ expect(app.listAllEndpoints()).toHaveLength(1);
52
+ });
53
+ });
54
+
55
+ describe('addService', () => {
56
+ it('should add a service to the app', () => {
57
+ const app = new PrismApp({ name: 'Test App' });
58
+
59
+ const service: ServiceDefinition = {
60
+ name: 'users-service',
61
+ endpoints: [
62
+ { method: 'GET', path: '/users', handler: async () => [] },
63
+ ],
64
+ };
65
+
66
+ app.addService(service);
67
+
68
+ expect(app.services).toHaveLength(1);
69
+ expect(app.services[0].name).toBe('users-service');
70
+ });
71
+
72
+ it('should register endpoints from added service', () => {
73
+ const app = new PrismApp();
74
+
75
+ app.addService({
76
+ name: 'api-service',
77
+ endpoints: [
78
+ { method: 'GET', path: '/items', handler: async () => [] },
79
+ { method: 'POST', path: '/items', handler: async () => ({}) },
80
+ ],
81
+ });
82
+
83
+ const endpoints = app.listAllEndpoints();
84
+ expect(endpoints).toHaveLength(2);
85
+ });
86
+
87
+ it('should allow adding multiple services', () => {
88
+ const app = new PrismApp({ name: 'Multi-Service App' });
89
+
90
+ app.addService({ name: 'service-1', endpoints: [{ method: 'GET', path: '/s1', handler: async () => ({}) }] });
91
+ app.addService({ name: 'service-2', endpoints: [{ method: 'GET', path: '/s2', handler: async () => ({}) }] });
92
+ app.addService({ name: 'service-3', endpoints: [{ method: 'GET', path: '/s3', handler: async () => ({}) }] });
93
+
94
+ expect(app.services).toHaveLength(3);
95
+ expect(app.listAllEndpoints()).toHaveLength(3);
96
+ });
97
+
98
+ it('should allow adding service with no endpoints', () => {
99
+ const app = new PrismApp();
100
+
101
+ app.addService({ name: 'empty-service' });
102
+
103
+ expect(app.services).toHaveLength(1);
104
+ expect(app.listAllEndpoints()).toHaveLength(0);
105
+ });
106
+ });
107
+
108
+ describe('getEndpoint', () => {
109
+ it('should find endpoint by method and path', () => {
110
+ const handler = async () => ({});
111
+ const app = new PrismApp({
112
+ services: [
113
+ {
114
+ name: 'test',
115
+ endpoints: [{ method: 'GET', path: '/users', handler }],
116
+ },
117
+ ],
118
+ });
119
+
120
+ const endpoint = app.getEndpoint('GET', '/users');
121
+
122
+ expect(endpoint).toBeDefined();
123
+ expect(endpoint?.handler).toBe(handler);
124
+ });
125
+
126
+ it('should return undefined for non-existent endpoint', () => {
127
+ const app = new PrismApp();
128
+
129
+ const endpoint = app.getEndpoint('GET', '/nonexistent');
130
+
131
+ expect(endpoint).toBeUndefined();
132
+ });
133
+
134
+ it('should differentiate endpoints by method', () => {
135
+ const getHandler = async () => ({ method: 'get' });
136
+ const postHandler = async () => ({ method: 'post' });
137
+
138
+ const app = new PrismApp({
139
+ services: [
140
+ {
141
+ name: 'test',
142
+ endpoints: [
143
+ { method: 'GET', path: '/items', handler: getHandler },
144
+ { method: 'POST', path: '/items', handler: postHandler },
145
+ ],
146
+ },
147
+ ],
148
+ });
149
+
150
+ expect(app.getEndpoint('GET', '/items')?.handler).toBe(getHandler);
151
+ expect(app.getEndpoint('POST', '/items')?.handler).toBe(postHandler);
152
+ });
153
+ });
154
+
155
+ describe('matchEndpoint', () => {
156
+ it('should match exact paths', () => {
157
+ const app = new PrismApp({
158
+ services: [
159
+ {
160
+ name: 'test',
161
+ endpoints: [{ method: 'GET', path: '/users', handler: async () => ({}) }],
162
+ },
163
+ ],
164
+ });
165
+
166
+ const match = app.matchEndpoint('GET', '/users');
167
+
168
+ expect(match).toBeDefined();
169
+ expect(match?.params).toEqual({});
170
+ });
171
+
172
+ it('should match paths with parameters', () => {
173
+ const app = new PrismApp({
174
+ services: [
175
+ {
176
+ name: 'test',
177
+ endpoints: [{ method: 'GET', path: '/users/:userId', handler: async () => ({}) }],
178
+ },
179
+ ],
180
+ });
181
+
182
+ const match = app.matchEndpoint('GET', '/users/123');
183
+
184
+ expect(match).toBeDefined();
185
+ expect(match?.params).toEqual({ userId: '123' });
186
+ });
187
+
188
+ it('should match paths with multiple parameters', () => {
189
+ const app = new PrismApp({
190
+ services: [
191
+ {
192
+ name: 'test',
193
+ endpoints: [{ method: 'GET', path: '/users/:userId/posts/:postId', handler: async () => ({}) }],
194
+ },
195
+ ],
196
+ });
197
+
198
+ const match = app.matchEndpoint('GET', '/users/123/posts/456');
199
+
200
+ expect(match).toBeDefined();
201
+ expect(match?.params).toEqual({ userId: '123', postId: '456' });
202
+ });
203
+
204
+ it('should return undefined for non-matching paths', () => {
205
+ const app = new PrismApp({
206
+ services: [
207
+ {
208
+ name: 'test',
209
+ endpoints: [{ method: 'GET', path: '/users', handler: async () => ({}) }],
210
+ },
211
+ ],
212
+ });
213
+
214
+ const match = app.matchEndpoint('GET', '/posts');
215
+
216
+ expect(match).toBeUndefined();
217
+ });
218
+
219
+ it('should not match when method differs', () => {
220
+ const app = new PrismApp({
221
+ services: [
222
+ {
223
+ name: 'test',
224
+ endpoints: [{ method: 'GET', path: '/users', handler: async () => ({}) }],
225
+ },
226
+ ],
227
+ });
228
+
229
+ const match = app.matchEndpoint('POST', '/users');
230
+
231
+ expect(match).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ describe('getAllServices', () => {
236
+ it('should return all registered services', () => {
237
+ const app = new PrismApp();
238
+ app.addService({ name: 'service-1' });
239
+ app.addService({ name: 'service-2' });
240
+
241
+ const services = app.getAllServices();
242
+
243
+ expect(services).toHaveLength(2);
244
+ expect(services[0].name).toBe('service-1');
245
+ expect(services[1].name).toBe('service-2');
246
+ });
247
+ });
248
+
249
+ describe('listAllEndpoints', () => {
250
+ it('should return all endpoints from all services', () => {
251
+ const app = new PrismApp();
252
+
253
+ app.addService({
254
+ name: 'service-1',
255
+ endpoints: [
256
+ { method: 'GET', path: '/a', handler: async () => ({}) },
257
+ { method: 'GET', path: '/b', handler: async () => ({}) },
258
+ ],
259
+ });
260
+
261
+ app.addService({
262
+ name: 'service-2',
263
+ endpoints: [
264
+ { method: 'GET', path: '/c', handler: async () => ({}) },
265
+ ],
266
+ });
267
+
268
+ const endpoints = app.listAllEndpoints();
269
+
270
+ expect(endpoints).toHaveLength(3);
271
+ expect(endpoints.map(e => e.path).sort()).toEqual(['/a', '/b', '/c']);
272
+ });
273
+ });
274
+ });
@@ -0,0 +1,295 @@
1
+ import {
2
+ RequestContext,
3
+ withRequestContext,
4
+ getCurrentRequestContext,
5
+ } from '../RequestContext.ts';
6
+ import { Authorization } from '../authorization/Authorization.ts';
7
+ import { describe, expect, it, beforeEach } from 'vitest';
8
+
9
+ describe('RequestContext', () => {
10
+ describe('withRequestContext', () => {
11
+ it('should run function within request context', () => {
12
+ const auth = new Authorization();
13
+ const context: RequestContext = {
14
+ requestId: 'req123',
15
+ startTime: Date.now(),
16
+ auth,
17
+ };
18
+
19
+ const result = withRequestContext(context, () => {
20
+ const currentContext = getCurrentRequestContext();
21
+ expect(currentContext).toBe(context);
22
+ return 'success';
23
+ });
24
+
25
+ expect(result).toBe('success');
26
+ });
27
+
28
+ it('should return the function result', () => {
29
+ const auth = new Authorization();
30
+ const context: RequestContext = {
31
+ requestId: 'req123',
32
+ startTime: Date.now(),
33
+ auth,
34
+ };
35
+
36
+ const result = withRequestContext(context, () => {
37
+ return { data: 'test', count: 42 };
38
+ });
39
+
40
+ expect(result).toEqual({ data: 'test', count: 42 });
41
+ });
42
+
43
+ it('should properly clean up context after function executes', () => {
44
+ const auth = new Authorization();
45
+ const context: RequestContext = {
46
+ requestId: 'req123',
47
+ startTime: Date.now(),
48
+ auth,
49
+ };
50
+
51
+ withRequestContext(context, () => {
52
+ expect(getCurrentRequestContext()).toBe(context);
53
+ });
54
+
55
+ expect(getCurrentRequestContext()).toBeUndefined();
56
+ });
57
+
58
+ it('should handle nested contexts', () => {
59
+ const auth1 = new Authorization();
60
+ const auth2 = new Authorization();
61
+ const context1: RequestContext = {
62
+ requestId: 'req1',
63
+ startTime: Date.now(),
64
+ auth: auth1,
65
+ };
66
+ const context2: RequestContext = {
67
+ requestId: 'req2',
68
+ startTime: Date.now(),
69
+ auth: auth2,
70
+ };
71
+
72
+ withRequestContext(context1, () => {
73
+ expect(getCurrentRequestContext()?.requestId).toBe('req1');
74
+
75
+ withRequestContext(context2, () => {
76
+ expect(getCurrentRequestContext()?.requestId).toBe('req2');
77
+ });
78
+
79
+ expect(getCurrentRequestContext()?.requestId).toBe('req1');
80
+ });
81
+
82
+ expect(getCurrentRequestContext()).toBeUndefined();
83
+ });
84
+
85
+ it('should propagate errors thrown in the function', () => {
86
+ const auth = new Authorization();
87
+ const context: RequestContext = {
88
+ requestId: 'req123',
89
+ startTime: Date.now(),
90
+ auth,
91
+ };
92
+
93
+ expect(() => {
94
+ withRequestContext(context, () => {
95
+ throw new Error('Test error');
96
+ });
97
+ }).toThrow('Test error');
98
+ });
99
+
100
+ it('should clear context even when function throws', () => {
101
+ const auth = new Authorization();
102
+ const context: RequestContext = {
103
+ requestId: 'req123',
104
+ startTime: Date.now(),
105
+ auth,
106
+ };
107
+
108
+ try {
109
+ withRequestContext(context, () => {
110
+ throw new Error('Test error');
111
+ });
112
+ } catch (e) {
113
+ // Expected error
114
+ }
115
+
116
+ expect(getCurrentRequestContext()).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe('getCurrentRequestContext', () => {
121
+ it('should return undefined when no context is set', () => {
122
+ expect(getCurrentRequestContext()).toBeUndefined();
123
+ });
124
+
125
+ it('should return the current context when inside withRequestContext', () => {
126
+ const auth = new Authorization();
127
+ const context: RequestContext = {
128
+ requestId: 'req123',
129
+ startTime: Date.now(),
130
+ auth,
131
+ };
132
+
133
+ withRequestContext(context, () => {
134
+ const currentContext = getCurrentRequestContext();
135
+ expect(currentContext).toBeDefined();
136
+ expect(currentContext?.requestId).toBe('req123');
137
+ expect(currentContext?.auth).toBe(auth);
138
+ });
139
+ });
140
+
141
+ it('should return undefined after context exits', () => {
142
+ const auth = new Authorization();
143
+ const context: RequestContext = {
144
+ requestId: 'req123',
145
+ startTime: Date.now(),
146
+ auth,
147
+ };
148
+
149
+ withRequestContext(context, () => {
150
+ // Inside context
151
+ });
152
+
153
+ expect(getCurrentRequestContext()).toBeUndefined();
154
+ });
155
+ });
156
+
157
+ describe('RequestContext interface', () => {
158
+ it('should support all required fields', () => {
159
+ const auth = new Authorization();
160
+ const context: RequestContext = {
161
+ requestId: 'req123',
162
+ startTime: Date.now(),
163
+ auth,
164
+ };
165
+
166
+ expect(context.requestId).toBe('req123');
167
+ expect(typeof context.startTime).toBe('number');
168
+ expect(context.auth).toBe(auth);
169
+ expect(context.req).toBeUndefined();
170
+ expect(context.res).toBeUndefined();
171
+ });
172
+
173
+ it('should support optional req and res fields', () => {
174
+ const auth = new Authorization();
175
+ const mockReq = {} as any;
176
+ const mockRes = {} as any;
177
+
178
+ const context: RequestContext = {
179
+ requestId: 'req123',
180
+ startTime: Date.now(),
181
+ auth,
182
+ req: mockReq,
183
+ res: mockRes,
184
+ };
185
+
186
+ expect(context.req).toBe(mockReq);
187
+ expect(context.res).toBe(mockRes);
188
+ });
189
+ });
190
+
191
+ describe('async operations', () => {
192
+ it('should work with async functions', async () => {
193
+ const auth = new Authorization();
194
+ const context: RequestContext = {
195
+ requestId: 'req123',
196
+ startTime: Date.now(),
197
+ auth,
198
+ };
199
+
200
+ const result = await withRequestContext(context, async () => {
201
+ const currentContext = getCurrentRequestContext();
202
+ expect(currentContext?.requestId).toBe('req123');
203
+
204
+ await new Promise((resolve) => setTimeout(resolve, 10));
205
+
206
+ const stillCurrentContext = getCurrentRequestContext();
207
+ expect(stillCurrentContext?.requestId).toBe('req123');
208
+
209
+ return 'async success';
210
+ });
211
+
212
+ expect(result).toBe('async success');
213
+ });
214
+
215
+ it('should maintain context across async operations', async () => {
216
+ const auth = new Authorization();
217
+ const context: RequestContext = {
218
+ requestId: 'async-req',
219
+ startTime: Date.now(),
220
+ auth,
221
+ };
222
+
223
+ await withRequestContext(context, async () => {
224
+ expect(getCurrentRequestContext()?.requestId).toBe('async-req');
225
+
226
+ await Promise.resolve();
227
+ expect(getCurrentRequestContext()?.requestId).toBe('async-req');
228
+
229
+ await new Promise((resolve) => setTimeout(resolve, 10));
230
+ expect(getCurrentRequestContext()?.requestId).toBe('async-req');
231
+ });
232
+ });
233
+
234
+ it('should handle parallel async operations with different contexts', async () => {
235
+ const auth1 = new Authorization();
236
+ const auth2 = new Authorization();
237
+ const context1: RequestContext = {
238
+ requestId: 'req1',
239
+ startTime: Date.now(),
240
+ auth: auth1,
241
+ };
242
+ const context2: RequestContext = {
243
+ requestId: 'req2',
244
+ startTime: Date.now(),
245
+ auth: auth2,
246
+ };
247
+
248
+ const results = await Promise.all([
249
+ withRequestContext(context1, async () => {
250
+ await new Promise((resolve) => setTimeout(resolve, 10));
251
+ return getCurrentRequestContext()?.requestId;
252
+ }),
253
+ withRequestContext(context2, async () => {
254
+ await new Promise((resolve) => setTimeout(resolve, 5));
255
+ return getCurrentRequestContext()?.requestId;
256
+ }),
257
+ ]);
258
+
259
+ expect(results).toEqual(['req1', 'req2']);
260
+ });
261
+ });
262
+
263
+ describe('context isolation', () => {
264
+ it('should isolate contexts between different executions', () => {
265
+ const auth1 = new Authorization();
266
+ const auth2 = new Authorization();
267
+ const context1: RequestContext = {
268
+ requestId: 'req1',
269
+ startTime: 100,
270
+ auth: auth1,
271
+ };
272
+ const context2: RequestContext = {
273
+ requestId: 'req2',
274
+ startTime: 200,
275
+ auth: auth2,
276
+ };
277
+
278
+ let capturedContext1: RequestContext | undefined;
279
+ let capturedContext2: RequestContext | undefined;
280
+
281
+ withRequestContext(context1, () => {
282
+ capturedContext1 = getCurrentRequestContext();
283
+ });
284
+
285
+ withRequestContext(context2, () => {
286
+ capturedContext2 = getCurrentRequestContext();
287
+ });
288
+
289
+ expect(capturedContext1?.requestId).toBe('req1');
290
+ expect(capturedContext1?.startTime).toBe(100);
291
+ expect(capturedContext2?.requestId).toBe('req2');
292
+ expect(capturedContext2?.startTime).toBe(200);
293
+ });
294
+ });
295
+ });