@bluelibs/runner 3.1.0 → 3.2.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 +206 -10
- package/dist/define.d.ts +1 -1
- package/dist/define.js +3 -2
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +13 -1
- package/dist/models/EventManager.js +9 -0
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/StoreRegistry.js +1 -1
- package/dist/models/StoreRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/globalEvents.test.ts +419 -1
- package/src/__tests__/recursion/README.md +3 -0
- package/src/__tests__/recursion/a.resource.ts +25 -0
- package/src/__tests__/recursion/b.resource.ts +33 -0
- package/src/__tests__/recursion/c.resource.ts +18 -0
- package/src/__tests__/run.dynamic-register-and-dependencies.test.ts +1185 -0
- package/src/define.ts +4 -3
- package/src/defs.ts +13 -1
- package/src/models/EventManager.ts +11 -0
- package/src/models/StoreRegistry.ts +1 -1
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import { defineTask, defineResource, defineMiddleware } from "../define";
|
|
2
|
+
import { run } from "../run";
|
|
3
|
+
|
|
4
|
+
describe("Dynamic Register and Dependencies", () => {
|
|
5
|
+
describe("Dynamic Dependencies", () => {
|
|
6
|
+
it("should support function-based dependencies", async () => {
|
|
7
|
+
const serviceA = defineResource({
|
|
8
|
+
id: "service.a",
|
|
9
|
+
init: async () => "Service A",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const serviceB = defineResource({
|
|
13
|
+
id: "service.b",
|
|
14
|
+
init: async () => "Service B",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const dynamicService = defineResource({
|
|
18
|
+
id: "service.dynamic",
|
|
19
|
+
dependencies: () => ({
|
|
20
|
+
a: serviceA,
|
|
21
|
+
b: serviceB,
|
|
22
|
+
}),
|
|
23
|
+
init: async (_, { a, b }) => `Dynamic service with ${a} and ${b}`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const app = defineResource({
|
|
27
|
+
id: "app",
|
|
28
|
+
register: [serviceA, serviceB, dynamicService],
|
|
29
|
+
dependencies: { dynamicService },
|
|
30
|
+
init: async (_, { dynamicService }) => {
|
|
31
|
+
expect(dynamicService).toBe(
|
|
32
|
+
"Dynamic service with Service A and Service B"
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await run(app);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should support conditional dependencies based on environment", async () => {
|
|
41
|
+
const prodService = defineResource({
|
|
42
|
+
id: "service.prod",
|
|
43
|
+
init: async () => "Production Service",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const devService = defineResource({
|
|
47
|
+
id: "service.dev",
|
|
48
|
+
init: async () => "Development Service",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const originalEnv = process.env.NODE_ENV;
|
|
52
|
+
process.env.NODE_ENV = "production";
|
|
53
|
+
|
|
54
|
+
const conditionalService = defineResource({
|
|
55
|
+
id: "service.conditional",
|
|
56
|
+
dependencies: () => ({
|
|
57
|
+
service:
|
|
58
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
59
|
+
}),
|
|
60
|
+
init: async (_, { service }) => `Using ${service}`,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const app = defineResource({
|
|
64
|
+
id: "app",
|
|
65
|
+
register: [prodService, devService, conditionalService],
|
|
66
|
+
dependencies: { conditionalService },
|
|
67
|
+
init: async (_, { conditionalService }) => {
|
|
68
|
+
expect(conditionalService).toBe("Using Production Service");
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await run(app);
|
|
73
|
+
|
|
74
|
+
// Test dev environment
|
|
75
|
+
process.env.NODE_ENV = "development";
|
|
76
|
+
|
|
77
|
+
const conditionalServiceDev = defineResource({
|
|
78
|
+
id: "service.conditional.dev",
|
|
79
|
+
dependencies: () => ({
|
|
80
|
+
service:
|
|
81
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
82
|
+
}),
|
|
83
|
+
init: async (_, { service }) => `Using ${service}`,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const appDev = defineResource({
|
|
87
|
+
id: "app.dev",
|
|
88
|
+
register: [prodService, devService, conditionalServiceDev],
|
|
89
|
+
dependencies: { conditionalService: conditionalServiceDev },
|
|
90
|
+
init: async (_, { conditionalService }) => {
|
|
91
|
+
expect(conditionalService).toBe("Using Development Service");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await run(appDev);
|
|
96
|
+
|
|
97
|
+
// Restore original environment
|
|
98
|
+
process.env.NODE_ENV = originalEnv;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should support forward references in function dependencies", async () => {
|
|
102
|
+
// Define resourceB first, which depends on resourceA (defined later)
|
|
103
|
+
const resourceB = defineResource({
|
|
104
|
+
id: "resource.b",
|
|
105
|
+
dependencies: () => ({ a: resourceA }), // Forward reference
|
|
106
|
+
init: async (_, { a }) => `B depends on ${a}`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const resourceA = defineResource({
|
|
110
|
+
id: "resource.a",
|
|
111
|
+
init: async () => "A",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const app = defineResource({
|
|
115
|
+
id: "app",
|
|
116
|
+
register: [resourceA, resourceB],
|
|
117
|
+
dependencies: { resourceB },
|
|
118
|
+
init: async (_, { resourceB }) => {
|
|
119
|
+
expect(resourceB).toBe("B depends on A");
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await run(app);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should support dependencies with configurations", async () => {
|
|
127
|
+
type ServiceConfig = { name: string; version: number };
|
|
128
|
+
|
|
129
|
+
const baseService = defineResource({
|
|
130
|
+
id: "service.base",
|
|
131
|
+
init: async (config: ServiceConfig) =>
|
|
132
|
+
`${config.name} v${config.version}`,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const dynamicService = defineResource({
|
|
136
|
+
id: "service.dynamic",
|
|
137
|
+
register: [baseService.with({ name: "Dynamic Base", version: 2 })],
|
|
138
|
+
dependencies: { baseService },
|
|
139
|
+
init: async (_, { baseService }) =>
|
|
140
|
+
`Dynamic service using ${baseService}`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const app = defineResource({
|
|
144
|
+
id: "app",
|
|
145
|
+
register: [dynamicService],
|
|
146
|
+
dependencies: { dynamicService },
|
|
147
|
+
init: async (_, { dynamicService }) => {
|
|
148
|
+
expect(dynamicService).toBe("Dynamic service using Dynamic Base v2");
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await run(app);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("Dynamic Register", () => {
|
|
157
|
+
it("should support function-based register", async () => {
|
|
158
|
+
const serviceA = defineResource({
|
|
159
|
+
id: "service.a",
|
|
160
|
+
init: async () => "Service A",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const serviceB = defineResource({
|
|
164
|
+
id: "service.b",
|
|
165
|
+
init: async () => "Service B",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const dynamicApp = defineResource({
|
|
169
|
+
id: "app.dynamic",
|
|
170
|
+
register: () => [serviceA, serviceB],
|
|
171
|
+
dependencies: { serviceA, serviceB },
|
|
172
|
+
init: async (_, { serviceA, serviceB }) => {
|
|
173
|
+
expect(serviceA).toBe("Service A");
|
|
174
|
+
expect(serviceB).toBe("Service B");
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await run(dynamicApp);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should support conditional registration based on environment", async () => {
|
|
182
|
+
const prodService = defineResource({
|
|
183
|
+
id: "service.prod",
|
|
184
|
+
init: async () => "Production Service",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const devService = defineResource({
|
|
188
|
+
id: "service.dev",
|
|
189
|
+
init: async () => "Development Service",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const originalEnv = process.env.NODE_ENV;
|
|
193
|
+
process.env.NODE_ENV = "production";
|
|
194
|
+
|
|
195
|
+
const conditionalApp = defineResource({
|
|
196
|
+
id: "app.conditional",
|
|
197
|
+
register: () => [
|
|
198
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
199
|
+
],
|
|
200
|
+
dependencies: () => ({
|
|
201
|
+
service:
|
|
202
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
203
|
+
}),
|
|
204
|
+
init: async (_, { service }) => {
|
|
205
|
+
expect(service).toBe("Production Service");
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await run(conditionalApp);
|
|
210
|
+
|
|
211
|
+
// Test with development environment
|
|
212
|
+
process.env.NODE_ENV = "development";
|
|
213
|
+
|
|
214
|
+
const conditionalAppDev = defineResource({
|
|
215
|
+
id: "app.conditional.dev",
|
|
216
|
+
register: () => [
|
|
217
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
218
|
+
],
|
|
219
|
+
dependencies: () => ({
|
|
220
|
+
service:
|
|
221
|
+
process.env.NODE_ENV === "production" ? prodService : devService,
|
|
222
|
+
}),
|
|
223
|
+
init: async (_, { service }) => {
|
|
224
|
+
expect(service).toBe("Development Service");
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await run(conditionalAppDev);
|
|
229
|
+
|
|
230
|
+
// Restore original environment
|
|
231
|
+
process.env.NODE_ENV = originalEnv;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should support dynamic registration with configurations", async () => {
|
|
235
|
+
type DatabaseConfig = { host: string; port: number };
|
|
236
|
+
|
|
237
|
+
const database = defineResource({
|
|
238
|
+
id: "database",
|
|
239
|
+
init: async (config: DatabaseConfig) =>
|
|
240
|
+
`DB at ${config.host}:${config.port}`,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const app = defineResource({
|
|
244
|
+
id: "app",
|
|
245
|
+
register: () => [
|
|
246
|
+
database.with({
|
|
247
|
+
host: process.env.DB_HOST || "localhost",
|
|
248
|
+
port: parseInt(process.env.DB_PORT || "5432"),
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
dependencies: { database },
|
|
252
|
+
init: async (_, { database }) => {
|
|
253
|
+
expect(database).toBe("DB at localhost:5432");
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await run(app);
|
|
258
|
+
|
|
259
|
+
// Test with environment variables
|
|
260
|
+
const originalHost = process.env.DB_HOST;
|
|
261
|
+
const originalPort = process.env.DB_PORT;
|
|
262
|
+
|
|
263
|
+
process.env.DB_HOST = "prod-db";
|
|
264
|
+
process.env.DB_PORT = "3306";
|
|
265
|
+
|
|
266
|
+
const appWithEnv = defineResource({
|
|
267
|
+
id: "app.env",
|
|
268
|
+
register: () => [
|
|
269
|
+
database.with({
|
|
270
|
+
host: process.env.DB_HOST || "localhost",
|
|
271
|
+
port: parseInt(process.env.DB_PORT || "5432"),
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
dependencies: { database },
|
|
275
|
+
init: async (_, { database }) => {
|
|
276
|
+
expect(database).toBe("DB at prod-db:3306");
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await run(appWithEnv);
|
|
281
|
+
|
|
282
|
+
// Restore environment variables
|
|
283
|
+
if (originalHost !== undefined) {
|
|
284
|
+
process.env.DB_HOST = originalHost;
|
|
285
|
+
} else {
|
|
286
|
+
delete process.env.DB_HOST;
|
|
287
|
+
}
|
|
288
|
+
if (originalPort !== undefined) {
|
|
289
|
+
process.env.DB_PORT = originalPort;
|
|
290
|
+
} else {
|
|
291
|
+
delete process.env.DB_PORT;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("Resource Configurations in Dependencies", () => {
|
|
297
|
+
it("should pass configurations to resources in dependencies", async () => {
|
|
298
|
+
type LoggerConfig = { level: string; prefix: string };
|
|
299
|
+
|
|
300
|
+
const logger = defineResource({
|
|
301
|
+
id: "logger",
|
|
302
|
+
init: async (config: LoggerConfig) => ({
|
|
303
|
+
log: (message: string) =>
|
|
304
|
+
`[${config.level}] ${config.prefix}: ${message}`,
|
|
305
|
+
config,
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const service = defineResource({
|
|
310
|
+
id: "service",
|
|
311
|
+
register: [logger.with({ level: "INFO", prefix: "SERVICE" })],
|
|
312
|
+
dependencies: { logger },
|
|
313
|
+
init: async (_, { logger }) => {
|
|
314
|
+
expect((logger as any).config.level).toBe("INFO");
|
|
315
|
+
expect((logger as any).config.prefix).toBe("SERVICE");
|
|
316
|
+
return (logger as any).log("Service initialized");
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const app = defineResource({
|
|
321
|
+
id: "app",
|
|
322
|
+
register: [service],
|
|
323
|
+
dependencies: { service },
|
|
324
|
+
init: async (_, { service }) => {
|
|
325
|
+
expect(service).toBe("[INFO] SERVICE: Service initialized");
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await run(app);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should support dynamic configurations in resource dependencies", async () => {
|
|
333
|
+
type ApiConfig = { baseUrl: string; timeout: number };
|
|
334
|
+
|
|
335
|
+
const apiService = defineResource({
|
|
336
|
+
id: "api.service",
|
|
337
|
+
init: async (config: ApiConfig) => ({
|
|
338
|
+
baseUrl: config.baseUrl,
|
|
339
|
+
timeout: config.timeout,
|
|
340
|
+
call: (endpoint: string) =>
|
|
341
|
+
`${config.baseUrl}${endpoint} (timeout: ${config.timeout}ms)`,
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const dynamicService = defineResource({
|
|
346
|
+
id: "service.dynamic",
|
|
347
|
+
register: () => [
|
|
348
|
+
apiService.with({
|
|
349
|
+
baseUrl: process.env.API_URL || "https://localhost:3000",
|
|
350
|
+
timeout: parseInt(process.env.API_TIMEOUT || "5000"),
|
|
351
|
+
}),
|
|
352
|
+
],
|
|
353
|
+
dependencies: { apiService },
|
|
354
|
+
init: async (_, { apiService }) => {
|
|
355
|
+
return (apiService as any).call("/users");
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const app = defineResource({
|
|
360
|
+
id: "app",
|
|
361
|
+
register: [dynamicService],
|
|
362
|
+
dependencies: { dynamicService },
|
|
363
|
+
init: async (_, { dynamicService }) => {
|
|
364
|
+
expect(dynamicService).toBe(
|
|
365
|
+
"https://localhost:3000/users (timeout: 5000ms)"
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
await run(app);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("Middleware Configurations in Dependencies", () => {
|
|
375
|
+
it("should pass configurations to middleware in dependencies", async () => {
|
|
376
|
+
type ValidationConfig = { schema: string; strict: boolean };
|
|
377
|
+
|
|
378
|
+
const loggerResource = defineResource({
|
|
379
|
+
id: "logger.validation",
|
|
380
|
+
init: async () => ({
|
|
381
|
+
log: (message: string) => `LOG: ${message}`,
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const validationMiddleware = defineMiddleware({
|
|
386
|
+
id: "middleware.validation",
|
|
387
|
+
dependencies: {
|
|
388
|
+
logger: loggerResource,
|
|
389
|
+
},
|
|
390
|
+
run: async ({ next }, { logger }, config: ValidationConfig) => {
|
|
391
|
+
(logger as any).log(
|
|
392
|
+
`Validating with schema: ${config.schema} (strict: ${config.strict})`
|
|
393
|
+
);
|
|
394
|
+
const result = await next();
|
|
395
|
+
return `Validated[${config.schema}]: ${result}`;
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const testTask = defineTask({
|
|
400
|
+
id: "task.test",
|
|
401
|
+
middleware: [
|
|
402
|
+
validationMiddleware.with({ schema: "user", strict: true }),
|
|
403
|
+
],
|
|
404
|
+
run: async () => "Task result",
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const app = defineResource({
|
|
408
|
+
id: "app",
|
|
409
|
+
register: [loggerResource, validationMiddleware, testTask],
|
|
410
|
+
dependencies: { testTask },
|
|
411
|
+
init: async (_, { testTask }) => {
|
|
412
|
+
const result = await testTask();
|
|
413
|
+
expect(result).toBe("Validated[user]: Task result");
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await run(app);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should support dynamic middleware configurations", async () => {
|
|
421
|
+
type RetryConfig = { maxAttempts: number; delay: number };
|
|
422
|
+
|
|
423
|
+
const retryMiddleware = defineMiddleware({
|
|
424
|
+
id: "middleware.retry",
|
|
425
|
+
run: async ({ next }, _, config: RetryConfig) => {
|
|
426
|
+
let attempts = 0;
|
|
427
|
+
let lastError: Error | null = null;
|
|
428
|
+
|
|
429
|
+
while (attempts < config.maxAttempts) {
|
|
430
|
+
try {
|
|
431
|
+
attempts++;
|
|
432
|
+
const result = await next();
|
|
433
|
+
return `Attempt ${attempts}/${config.maxAttempts}: ${result}`;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
lastError = error as Error;
|
|
436
|
+
if (attempts < config.maxAttempts) {
|
|
437
|
+
// Simulate delay (in real scenario you'd use setTimeout)
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
throw lastError;
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const retryableTask = defineTask({
|
|
448
|
+
id: "task.retryable",
|
|
449
|
+
middleware: [
|
|
450
|
+
retryMiddleware.with({
|
|
451
|
+
maxAttempts: parseInt(process.env.MAX_RETRIES || "3"),
|
|
452
|
+
delay: parseInt(process.env.RETRY_DELAY || "1000"),
|
|
453
|
+
}),
|
|
454
|
+
],
|
|
455
|
+
run: async () => "Success",
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const app = defineResource({
|
|
459
|
+
id: "app",
|
|
460
|
+
register: [retryMiddleware, retryableTask],
|
|
461
|
+
dependencies: { retryableTask },
|
|
462
|
+
init: async (_, { retryableTask }) => {
|
|
463
|
+
const result = await retryableTask();
|
|
464
|
+
expect(result).toBe("Attempt 1/3: Success");
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await run(app);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("should support middleware with configured resource dependencies", async () => {
|
|
472
|
+
type CacheConfig = { ttl: number; maxSize: number };
|
|
473
|
+
type LoggerConfig = { level: string };
|
|
474
|
+
|
|
475
|
+
const cache = defineResource({
|
|
476
|
+
id: "cache",
|
|
477
|
+
init: async (config: CacheConfig) => ({
|
|
478
|
+
ttl: config.ttl,
|
|
479
|
+
maxSize: config.maxSize,
|
|
480
|
+
store: new Map(),
|
|
481
|
+
get: (key: string) => `cached-${key}`,
|
|
482
|
+
set: (key: string, value: any) => `stored-${key}:${value}`,
|
|
483
|
+
}),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const logger = defineResource({
|
|
487
|
+
id: "logger",
|
|
488
|
+
init: async (config: LoggerConfig) => ({
|
|
489
|
+
level: config.level,
|
|
490
|
+
log: (message: string) => `[${config.level}] ${message}`,
|
|
491
|
+
}),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const cachingMiddleware = defineMiddleware({
|
|
495
|
+
id: "middleware.caching",
|
|
496
|
+
dependencies: { cache, logger },
|
|
497
|
+
run: async (
|
|
498
|
+
{ next },
|
|
499
|
+
{ cache, logger },
|
|
500
|
+
config: { enabled: boolean }
|
|
501
|
+
) => {
|
|
502
|
+
if (!config.enabled) {
|
|
503
|
+
return next();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
(logger as any).log(
|
|
507
|
+
`Cache configured with TTL: ${(cache as any).ttl}, MaxSize: ${
|
|
508
|
+
(cache as any).maxSize
|
|
509
|
+
}`
|
|
510
|
+
);
|
|
511
|
+
const result = await next();
|
|
512
|
+
return `Cached: ${result}`;
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const cachedTask = defineTask({
|
|
517
|
+
id: "task.cached",
|
|
518
|
+
middleware: [cachingMiddleware.with({ enabled: true })],
|
|
519
|
+
run: async () => "Task result",
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const app = defineResource({
|
|
523
|
+
id: "app",
|
|
524
|
+
register: [
|
|
525
|
+
cache.with({ ttl: 60000, maxSize: 100 }),
|
|
526
|
+
logger.with({ level: "DEBUG" }),
|
|
527
|
+
cachingMiddleware,
|
|
528
|
+
cachedTask,
|
|
529
|
+
],
|
|
530
|
+
dependencies: { cachedTask },
|
|
531
|
+
init: async (_, { cachedTask }) => {
|
|
532
|
+
const result = await cachedTask();
|
|
533
|
+
expect(result).toBe("Cached: Task result");
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await run(app);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("Combined Dynamic Register and Dependencies", () => {
|
|
542
|
+
it("should support both dynamic register and dependencies together", async () => {
|
|
543
|
+
type DatabaseConfig = { host: string };
|
|
544
|
+
type CacheConfig = { ttl: number };
|
|
545
|
+
|
|
546
|
+
const database = defineResource({
|
|
547
|
+
id: "database",
|
|
548
|
+
init: async (config: DatabaseConfig) => `DB: ${config.host}`,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const cache = defineResource({
|
|
552
|
+
id: "cache",
|
|
553
|
+
init: async (config: CacheConfig) => `Cache: ${config.ttl}ms`,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const service = defineResource({
|
|
557
|
+
id: "service",
|
|
558
|
+
register: () => [
|
|
559
|
+
// Dynamic registration based on environment
|
|
560
|
+
database.with({
|
|
561
|
+
host: process.env.NODE_ENV === "test" ? "test-db" : "prod-db",
|
|
562
|
+
}),
|
|
563
|
+
cache.with({ ttl: process.env.NODE_ENV === "test" ? 1000 : 60000 }),
|
|
564
|
+
],
|
|
565
|
+
dependencies: { database, cache },
|
|
566
|
+
init: async (_, { database, cache }) =>
|
|
567
|
+
`Service with ${database} and ${cache}`,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const originalEnv = process.env.NODE_ENV;
|
|
571
|
+
process.env.NODE_ENV = "test";
|
|
572
|
+
|
|
573
|
+
const app = defineResource({
|
|
574
|
+
id: "app",
|
|
575
|
+
register: [service],
|
|
576
|
+
dependencies: { service },
|
|
577
|
+
init: async (_, { service }) => {
|
|
578
|
+
expect(service).toBe("Service with DB: test-db and Cache: 1000ms");
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
await run(app);
|
|
583
|
+
|
|
584
|
+
// Restore environment
|
|
585
|
+
process.env.NODE_ENV = originalEnv;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should handle complex conditional dependencies and registrations", async () => {
|
|
589
|
+
const mockService = defineResource({
|
|
590
|
+
id: "service.mock",
|
|
591
|
+
init: async () => "Mock Service",
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const realService = defineResource({
|
|
595
|
+
id: "service.real",
|
|
596
|
+
init: async () => "Real Service",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const featureToggle = defineResource({
|
|
600
|
+
id: "feature.toggle",
|
|
601
|
+
init: async () => ({
|
|
602
|
+
isEnabled: (feature: string) =>
|
|
603
|
+
process.env[`FEATURE_${feature}`] === "true",
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Set up environment for testing
|
|
608
|
+
process.env.FEATURE_ADVANCED = "true";
|
|
609
|
+
process.env.NODE_ENV = "development";
|
|
610
|
+
|
|
611
|
+
const complexApp = defineResource({
|
|
612
|
+
id: "app.complex",
|
|
613
|
+
register: () => {
|
|
614
|
+
const services: any[] = [featureToggle];
|
|
615
|
+
|
|
616
|
+
// Add services based on feature flags and environment
|
|
617
|
+
if (process.env.FEATURE_ADVANCED === "true") {
|
|
618
|
+
services.push(realService);
|
|
619
|
+
} else {
|
|
620
|
+
services.push(mockService);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return services;
|
|
624
|
+
},
|
|
625
|
+
dependencies: () => {
|
|
626
|
+
const deps: any = { featureToggle };
|
|
627
|
+
|
|
628
|
+
// Same logic for dependencies
|
|
629
|
+
if (process.env.FEATURE_ADVANCED === "true") {
|
|
630
|
+
deps.service = realService;
|
|
631
|
+
} else {
|
|
632
|
+
deps.service = mockService;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return deps;
|
|
636
|
+
},
|
|
637
|
+
init: async (_, { service, featureToggle }) => {
|
|
638
|
+
const isAdvanced = (featureToggle as any).isEnabled("ADVANCED");
|
|
639
|
+
expect(isAdvanced).toBe(true);
|
|
640
|
+
expect(service).toBe("Real Service");
|
|
641
|
+
return `App with ${service}`;
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
await run(complexApp);
|
|
646
|
+
|
|
647
|
+
// Test with feature disabled
|
|
648
|
+
process.env.FEATURE_ADVANCED = "false";
|
|
649
|
+
|
|
650
|
+
const complexAppDisabled = defineResource({
|
|
651
|
+
id: "app.complex.disabled",
|
|
652
|
+
register: () => {
|
|
653
|
+
const services: any[] = [featureToggle];
|
|
654
|
+
|
|
655
|
+
if (process.env.FEATURE_ADVANCED === "true") {
|
|
656
|
+
services.push(realService);
|
|
657
|
+
} else {
|
|
658
|
+
services.push(mockService);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return services;
|
|
662
|
+
},
|
|
663
|
+
dependencies: () => {
|
|
664
|
+
const deps: any = { featureToggle };
|
|
665
|
+
|
|
666
|
+
if (process.env.FEATURE_ADVANCED === "true") {
|
|
667
|
+
deps.service = realService;
|
|
668
|
+
} else {
|
|
669
|
+
deps.service = mockService;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return deps;
|
|
673
|
+
},
|
|
674
|
+
init: async (_, { service, featureToggle }) => {
|
|
675
|
+
const isAdvanced = (featureToggle as any).isEnabled("ADVANCED");
|
|
676
|
+
expect(isAdvanced).toBe(false);
|
|
677
|
+
expect(service).toBe("Mock Service");
|
|
678
|
+
return `App with ${service}`;
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
await run(complexAppDisabled);
|
|
683
|
+
|
|
684
|
+
// Clean up environment
|
|
685
|
+
delete process.env.FEATURE_ADVANCED;
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe("Dynamic Dependencies and Register with Config", () => {
|
|
690
|
+
it("should pass config to dependencies function in resource", async () => {
|
|
691
|
+
const loggerService = defineResource({
|
|
692
|
+
id: "service.logger",
|
|
693
|
+
init: async (config: { level: string; prefix: string }) => ({
|
|
694
|
+
log: (message: string) =>
|
|
695
|
+
`[${config.level}] ${config.prefix}: ${message}`,
|
|
696
|
+
}),
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const cacheService = defineResource({
|
|
700
|
+
id: "service.cache",
|
|
701
|
+
init: async (config: { ttl: number; size: number }) => ({
|
|
702
|
+
get: (key: string) => `cached-${key}-ttl:${config.ttl}`,
|
|
703
|
+
set: (key: string, value: any) => `set-${key}-size:${config.size}`,
|
|
704
|
+
}),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const dynamicService = defineResource({
|
|
708
|
+
id: "service.dynamic",
|
|
709
|
+
dependencies: (config: { useCache: boolean; logLevel: string }) => ({
|
|
710
|
+
logger: loggerService,
|
|
711
|
+
...(config.useCache && { cache: cacheService }),
|
|
712
|
+
}),
|
|
713
|
+
init: async (config: { useCache: boolean; logLevel: string }, deps) => {
|
|
714
|
+
const logger = deps.logger;
|
|
715
|
+
const cache = config.useCache ? deps.cache : null;
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
process: (data: string) => {
|
|
719
|
+
const logResult = logger.log(`Processing ${data}`);
|
|
720
|
+
const cacheResult = cache ? cache.get(data) : "no-cache";
|
|
721
|
+
return `${logResult} | ${cacheResult}`;
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const app = defineResource({
|
|
728
|
+
id: "app",
|
|
729
|
+
register: [
|
|
730
|
+
loggerService.with({ level: "DEBUG", prefix: "DYN" }),
|
|
731
|
+
cacheService.with({ ttl: 3600, size: 100 }),
|
|
732
|
+
dynamicService.with({ useCache: true, logLevel: "DEBUG" }),
|
|
733
|
+
],
|
|
734
|
+
dependencies: { dynamicService },
|
|
735
|
+
init: async (_, { dynamicService }) => {
|
|
736
|
+
const result = dynamicService.process("test-data");
|
|
737
|
+
expect(result).toBe(
|
|
738
|
+
"[DEBUG] DYN: Processing test-data | cached-test-data-ttl:3600"
|
|
739
|
+
);
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
await run(app);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("should pass config to register function in resource", async () => {
|
|
747
|
+
const emailService = defineResource({
|
|
748
|
+
id: "service.email",
|
|
749
|
+
init: async (config: { provider: string; apiKey: string }) => ({
|
|
750
|
+
send: (to: string, subject: string) =>
|
|
751
|
+
`${config.provider}:${config.apiKey} -> ${to}: ${subject}`,
|
|
752
|
+
}),
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const smsService = defineResource({
|
|
756
|
+
id: "service.sms",
|
|
757
|
+
init: async (config: { provider: string; apiKey: string }) => ({
|
|
758
|
+
send: (to: string, message: string) =>
|
|
759
|
+
`${config.provider}:${config.apiKey} -> ${to}: ${message}`,
|
|
760
|
+
}),
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const notificationService = defineResource({
|
|
764
|
+
id: "service.notification",
|
|
765
|
+
register: (config: {
|
|
766
|
+
enableEmail: boolean;
|
|
767
|
+
enableSms: boolean;
|
|
768
|
+
emailProvider: string;
|
|
769
|
+
smsProvider: string;
|
|
770
|
+
}) => [
|
|
771
|
+
...(config.enableEmail
|
|
772
|
+
? [
|
|
773
|
+
emailService.with({
|
|
774
|
+
provider: config.emailProvider,
|
|
775
|
+
apiKey: "email-key",
|
|
776
|
+
}),
|
|
777
|
+
]
|
|
778
|
+
: []),
|
|
779
|
+
...(config.enableSms
|
|
780
|
+
? [
|
|
781
|
+
smsService.with({
|
|
782
|
+
provider: config.smsProvider,
|
|
783
|
+
apiKey: "sms-key",
|
|
784
|
+
}),
|
|
785
|
+
]
|
|
786
|
+
: []),
|
|
787
|
+
],
|
|
788
|
+
dependencies: (config: {
|
|
789
|
+
enableEmail: boolean;
|
|
790
|
+
enableSms: boolean;
|
|
791
|
+
emailProvider: string;
|
|
792
|
+
smsProvider: string;
|
|
793
|
+
}) => ({
|
|
794
|
+
...(config.enableEmail && { emailService }),
|
|
795
|
+
...(config.enableSms && { smsService }),
|
|
796
|
+
}),
|
|
797
|
+
init: async (
|
|
798
|
+
config: {
|
|
799
|
+
enableEmail: boolean;
|
|
800
|
+
enableSms: boolean;
|
|
801
|
+
emailProvider: string;
|
|
802
|
+
smsProvider: string;
|
|
803
|
+
},
|
|
804
|
+
deps: any
|
|
805
|
+
) => ({
|
|
806
|
+
notify: (type: string, recipient: string, content: string) => {
|
|
807
|
+
if (type === "email" && config.enableEmail && deps.emailService) {
|
|
808
|
+
return deps.emailService.send(recipient, content);
|
|
809
|
+
}
|
|
810
|
+
if (type === "sms" && config.enableSms && deps.smsService) {
|
|
811
|
+
return deps.smsService.send(recipient, content);
|
|
812
|
+
}
|
|
813
|
+
return "notification-disabled";
|
|
814
|
+
},
|
|
815
|
+
}),
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const app = defineResource({
|
|
819
|
+
id: "app",
|
|
820
|
+
register: [
|
|
821
|
+
notificationService.with({
|
|
822
|
+
enableEmail: true,
|
|
823
|
+
enableSms: false,
|
|
824
|
+
emailProvider: "sendgrid",
|
|
825
|
+
smsProvider: "twilio",
|
|
826
|
+
}),
|
|
827
|
+
],
|
|
828
|
+
dependencies: { notificationService },
|
|
829
|
+
init: async (_, { notificationService }) => {
|
|
830
|
+
const emailResult = notificationService.notify(
|
|
831
|
+
"email",
|
|
832
|
+
"test@example.com",
|
|
833
|
+
"Hello World"
|
|
834
|
+
);
|
|
835
|
+
const smsResult = notificationService.notify(
|
|
836
|
+
"sms",
|
|
837
|
+
"+1234567890",
|
|
838
|
+
"Hello SMS"
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
expect(emailResult).toBe(
|
|
842
|
+
"sendgrid:email-key -> test@example.com: Hello World"
|
|
843
|
+
);
|
|
844
|
+
expect(smsResult).toBe("notification-disabled");
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
await run(app);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("should pass config to middleware dependencies function", async () => {
|
|
852
|
+
const auditService = defineResource({
|
|
853
|
+
id: "service.audit",
|
|
854
|
+
init: async (config: { enabled: boolean; level: string }) => ({
|
|
855
|
+
log: (action: string, user: string) =>
|
|
856
|
+
config.enabled
|
|
857
|
+
? `[${config.level}] ${user} performed ${action}`
|
|
858
|
+
: "audit-disabled",
|
|
859
|
+
}),
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
const authService = defineResource({
|
|
863
|
+
id: "service.auth",
|
|
864
|
+
init: async (config: { requireRole: string }) => ({
|
|
865
|
+
validateRole: (userRole: string) => userRole === config.requireRole,
|
|
866
|
+
getRequiredRole: () => config.requireRole,
|
|
867
|
+
}),
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const authMiddleware = defineMiddleware({
|
|
871
|
+
id: "middleware.auth",
|
|
872
|
+
dependencies: (config: {
|
|
873
|
+
auditEnabled: boolean;
|
|
874
|
+
requiredRole: string;
|
|
875
|
+
}) => ({
|
|
876
|
+
audit: auditService,
|
|
877
|
+
auth: authService,
|
|
878
|
+
}),
|
|
879
|
+
run: async (
|
|
880
|
+
{ task, next },
|
|
881
|
+
deps: any,
|
|
882
|
+
config: { auditEnabled: boolean; requiredRole: string }
|
|
883
|
+
) => {
|
|
884
|
+
const userRole = task?.input?.userRole || "guest";
|
|
885
|
+
|
|
886
|
+
if (!deps.auth.validateRole(userRole)) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Access denied. Required role: ${deps.auth.getRequiredRole()}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const auditResult = deps.audit.log("protected-action", userRole);
|
|
893
|
+
const result = await next(task?.input);
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
result,
|
|
897
|
+
audit: auditResult,
|
|
898
|
+
validatedRole: userRole,
|
|
899
|
+
};
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const protectedTask = defineTask({
|
|
904
|
+
id: "task.protected",
|
|
905
|
+
middleware: [
|
|
906
|
+
authMiddleware.with({ auditEnabled: true, requiredRole: "admin" }),
|
|
907
|
+
],
|
|
908
|
+
run: async (input: { userRole: string; data: string }) =>
|
|
909
|
+
`Protected data: ${input.data}`,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const app = defineResource({
|
|
913
|
+
id: "app",
|
|
914
|
+
register: [
|
|
915
|
+
auditService.with({ enabled: true, level: "INFO" }),
|
|
916
|
+
authService.with({ requireRole: "admin" }),
|
|
917
|
+
authMiddleware,
|
|
918
|
+
protectedTask,
|
|
919
|
+
],
|
|
920
|
+
dependencies: { protectedTask },
|
|
921
|
+
init: async (_, { protectedTask }) => {
|
|
922
|
+
const result = await protectedTask({
|
|
923
|
+
userRole: "admin",
|
|
924
|
+
data: "secret",
|
|
925
|
+
});
|
|
926
|
+
expect(result).toEqual({
|
|
927
|
+
result: "Protected data: secret",
|
|
928
|
+
audit: "[INFO] admin performed protected-action",
|
|
929
|
+
validatedRole: "admin",
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Test access denied
|
|
933
|
+
await expect(
|
|
934
|
+
protectedTask({ userRole: "user", data: "secret" })
|
|
935
|
+
).rejects.toThrow("Access denied. Required role: admin");
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
await run(app);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it("should handle complex config-driven dependency resolution", async () => {
|
|
943
|
+
const primaryDb = defineResource({
|
|
944
|
+
id: "db.primary",
|
|
945
|
+
init: async (config: { host: string; port: number }) => ({
|
|
946
|
+
query: (sql: string) =>
|
|
947
|
+
`primary-${config.host}:${config.port} -> ${sql}`,
|
|
948
|
+
}),
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const secondaryDb = defineResource({
|
|
952
|
+
id: "db.secondary",
|
|
953
|
+
init: async (config: { host: string; port: number }) => ({
|
|
954
|
+
query: (sql: string) =>
|
|
955
|
+
`secondary-${config.host}:${config.port} -> ${sql}`,
|
|
956
|
+
}),
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const cacheLayer = defineResource({
|
|
960
|
+
id: "cache.layer",
|
|
961
|
+
init: async (config: { redis: boolean; memory: boolean }) => ({
|
|
962
|
+
get: (key: string) =>
|
|
963
|
+
config.redis
|
|
964
|
+
? `redis-${key}`
|
|
965
|
+
: config.memory
|
|
966
|
+
? `memory-${key}`
|
|
967
|
+
: null,
|
|
968
|
+
set: (key: string, value: any) => `cache-set-${key}`,
|
|
969
|
+
}),
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const complexService = defineResource({
|
|
973
|
+
id: "service.complex",
|
|
974
|
+
register: (config: {
|
|
975
|
+
environment: "dev" | "prod";
|
|
976
|
+
features: { caching: boolean; readReplica: boolean };
|
|
977
|
+
}) => {
|
|
978
|
+
const services: any[] = [
|
|
979
|
+
primaryDb.with({
|
|
980
|
+
host:
|
|
981
|
+
config.environment === "prod" ? "prod-primary" : "dev-primary",
|
|
982
|
+
port: config.environment === "prod" ? 5432 : 5433,
|
|
983
|
+
}),
|
|
984
|
+
];
|
|
985
|
+
|
|
986
|
+
if (config.features.readReplica) {
|
|
987
|
+
services.push(
|
|
988
|
+
secondaryDb.with({
|
|
989
|
+
host:
|
|
990
|
+
config.environment === "prod"
|
|
991
|
+
? "prod-secondary"
|
|
992
|
+
: "dev-secondary",
|
|
993
|
+
port: config.environment === "prod" ? 5434 : 5435,
|
|
994
|
+
})
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (config.features.caching) {
|
|
999
|
+
services.push(
|
|
1000
|
+
cacheLayer.with({
|
|
1001
|
+
redis: config.environment === "prod",
|
|
1002
|
+
memory: config.environment === "dev",
|
|
1003
|
+
})
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return services;
|
|
1008
|
+
},
|
|
1009
|
+
dependencies: (config: {
|
|
1010
|
+
environment: "dev" | "prod";
|
|
1011
|
+
features: { caching: boolean; readReplica: boolean };
|
|
1012
|
+
}) => ({
|
|
1013
|
+
primaryDb,
|
|
1014
|
+
...(config.features.readReplica && { secondaryDb }),
|
|
1015
|
+
...(config.features.caching && { cacheLayer }),
|
|
1016
|
+
}),
|
|
1017
|
+
init: async (
|
|
1018
|
+
config: {
|
|
1019
|
+
environment: "dev" | "prod";
|
|
1020
|
+
features: { caching: boolean; readReplica: boolean };
|
|
1021
|
+
},
|
|
1022
|
+
deps: any
|
|
1023
|
+
) => ({
|
|
1024
|
+
getData: (query: string, useCache: boolean = false) => {
|
|
1025
|
+
const cacheResult =
|
|
1026
|
+
useCache && deps.cacheLayer ? deps.cacheLayer.get(query) : null;
|
|
1027
|
+
if (cacheResult) return cacheResult;
|
|
1028
|
+
|
|
1029
|
+
const dbResult =
|
|
1030
|
+
deps.secondaryDb && config.features.readReplica
|
|
1031
|
+
? deps.secondaryDb.query(query)
|
|
1032
|
+
: deps.primaryDb.query(query);
|
|
1033
|
+
|
|
1034
|
+
if (useCache && deps.cacheLayer) {
|
|
1035
|
+
deps.cacheLayer.set(query, dbResult);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return dbResult;
|
|
1039
|
+
},
|
|
1040
|
+
}),
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const app = defineResource({
|
|
1044
|
+
id: "app",
|
|
1045
|
+
register: [
|
|
1046
|
+
complexService.with({
|
|
1047
|
+
environment: "prod",
|
|
1048
|
+
features: { caching: true, readReplica: true },
|
|
1049
|
+
}),
|
|
1050
|
+
],
|
|
1051
|
+
dependencies: { complexService },
|
|
1052
|
+
init: async (_, { complexService }) => {
|
|
1053
|
+
const result1 = complexService.getData("SELECT * FROM users", false);
|
|
1054
|
+
const result2 = complexService.getData("SELECT * FROM posts", true);
|
|
1055
|
+
|
|
1056
|
+
expect(result1).toBe(
|
|
1057
|
+
"secondary-prod-secondary:5434 -> SELECT * FROM users"
|
|
1058
|
+
);
|
|
1059
|
+
expect(result2).toBe("redis-SELECT * FROM posts");
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
await run(app);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("should support nested config-driven dependencies and registrations", async () => {
|
|
1067
|
+
const configService = defineResource({
|
|
1068
|
+
id: "service.config",
|
|
1069
|
+
init: async (baseConfig: { app: string; version: number }) => ({
|
|
1070
|
+
get: (key: string) =>
|
|
1071
|
+
`${baseConfig.app}-v${baseConfig.version}-${key}`,
|
|
1072
|
+
getApp: () => baseConfig.app,
|
|
1073
|
+
getVersion: () => baseConfig.version,
|
|
1074
|
+
}),
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const metricsService = defineResource({
|
|
1078
|
+
id: "service.metrics",
|
|
1079
|
+
init: async (config: { enabled: boolean; endpoint: string }) => ({
|
|
1080
|
+
track: (event: string) =>
|
|
1081
|
+
config.enabled
|
|
1082
|
+
? `metrics:${config.endpoint}/${event}`
|
|
1083
|
+
: "metrics-disabled",
|
|
1084
|
+
}),
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
const parentService = defineResource({
|
|
1088
|
+
id: "service.parent",
|
|
1089
|
+
register: (config: {
|
|
1090
|
+
appName: string;
|
|
1091
|
+
enableMetrics: boolean;
|
|
1092
|
+
metricsEndpoint: string;
|
|
1093
|
+
}) => [
|
|
1094
|
+
configService.with({ app: config.appName, version: 1 }),
|
|
1095
|
+
...(config.enableMetrics
|
|
1096
|
+
? [
|
|
1097
|
+
metricsService.with({
|
|
1098
|
+
enabled: true,
|
|
1099
|
+
endpoint: config.metricsEndpoint,
|
|
1100
|
+
}),
|
|
1101
|
+
]
|
|
1102
|
+
: []),
|
|
1103
|
+
],
|
|
1104
|
+
dependencies: (config: {
|
|
1105
|
+
appName: string;
|
|
1106
|
+
enableMetrics: boolean;
|
|
1107
|
+
metricsEndpoint: string;
|
|
1108
|
+
}) => ({
|
|
1109
|
+
configService,
|
|
1110
|
+
...(config.enableMetrics && { metricsService }),
|
|
1111
|
+
}),
|
|
1112
|
+
init: async (
|
|
1113
|
+
config: {
|
|
1114
|
+
appName: string;
|
|
1115
|
+
enableMetrics: boolean;
|
|
1116
|
+
metricsEndpoint: string;
|
|
1117
|
+
},
|
|
1118
|
+
deps: any
|
|
1119
|
+
) => ({
|
|
1120
|
+
process: (action: string) => {
|
|
1121
|
+
const configResult = deps.configService.get(action);
|
|
1122
|
+
const metricsResult =
|
|
1123
|
+
config.enableMetrics && deps.metricsService
|
|
1124
|
+
? deps.metricsService.track(action)
|
|
1125
|
+
: "no-metrics";
|
|
1126
|
+
return `${configResult} | ${metricsResult}`;
|
|
1127
|
+
},
|
|
1128
|
+
}),
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const childService = defineResource({
|
|
1132
|
+
id: "service.child",
|
|
1133
|
+
dependencies: (config: {
|
|
1134
|
+
parentConfig: {
|
|
1135
|
+
appName: string;
|
|
1136
|
+
enableMetrics: boolean;
|
|
1137
|
+
metricsEndpoint: string;
|
|
1138
|
+
};
|
|
1139
|
+
}) => ({
|
|
1140
|
+
parent: parentService,
|
|
1141
|
+
}),
|
|
1142
|
+
init: async (
|
|
1143
|
+
config: {
|
|
1144
|
+
parentConfig: {
|
|
1145
|
+
appName: string;
|
|
1146
|
+
enableMetrics: boolean;
|
|
1147
|
+
metricsEndpoint: string;
|
|
1148
|
+
};
|
|
1149
|
+
},
|
|
1150
|
+
deps: any
|
|
1151
|
+
) => ({
|
|
1152
|
+
childProcess: (action: string) =>
|
|
1153
|
+
`child: ${deps.parent.process(action)}`,
|
|
1154
|
+
}),
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
const app = defineResource({
|
|
1158
|
+
id: "app",
|
|
1159
|
+
register: [
|
|
1160
|
+
parentService.with({
|
|
1161
|
+
appName: "MyApp",
|
|
1162
|
+
enableMetrics: true,
|
|
1163
|
+
metricsEndpoint: "http://metrics.example.com",
|
|
1164
|
+
}),
|
|
1165
|
+
childService.with({
|
|
1166
|
+
parentConfig: {
|
|
1167
|
+
appName: "MyApp",
|
|
1168
|
+
enableMetrics: true,
|
|
1169
|
+
metricsEndpoint: "http://metrics.example.com",
|
|
1170
|
+
},
|
|
1171
|
+
}),
|
|
1172
|
+
],
|
|
1173
|
+
dependencies: { childService },
|
|
1174
|
+
init: async (_, { childService }) => {
|
|
1175
|
+
const result = childService.childProcess("user-login");
|
|
1176
|
+
expect(result).toBe(
|
|
1177
|
+
"child: MyApp-v1-user-login | metrics:http://metrics.example.com/user-login"
|
|
1178
|
+
);
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
await run(app);
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
});
|