@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.
@@ -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
+ });