@objectstack/core 4.0.4 → 4.0.5

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 (75) hide show
  1. package/README.md +95 -10
  2. package/dist/index.cjs +169 -507
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +24 -223
  5. package/dist/index.d.ts +24 -223
  6. package/dist/index.js +175 -505
  7. package/dist/index.js.map +1 -1
  8. package/dist/logger.cjs +177 -0
  9. package/dist/logger.cjs.map +1 -0
  10. package/dist/logger.d.cts +26 -0
  11. package/dist/logger.d.ts +26 -0
  12. package/dist/logger.js +158 -0
  13. package/dist/logger.js.map +1 -0
  14. package/package.json +36 -15
  15. package/.turbo/turbo-build.log +0 -22
  16. package/ADVANCED_FEATURES.md +0 -380
  17. package/API_REGISTRY.md +0 -392
  18. package/CHANGELOG.md +0 -472
  19. package/PHASE2_IMPLEMENTATION.md +0 -388
  20. package/REFACTORING_SUMMARY.md +0 -40
  21. package/examples/api-registry-example.ts +0 -559
  22. package/examples/kernel-features-example.ts +0 -311
  23. package/examples/phase2-integration.ts +0 -357
  24. package/src/api-registry-plugin.test.ts +0 -393
  25. package/src/api-registry-plugin.ts +0 -89
  26. package/src/api-registry.test.ts +0 -1089
  27. package/src/api-registry.ts +0 -739
  28. package/src/contracts/data-engine.ts +0 -57
  29. package/src/contracts/http-server.ts +0 -151
  30. package/src/contracts/logger.ts +0 -72
  31. package/src/dependency-resolver.test.ts +0 -287
  32. package/src/dependency-resolver.ts +0 -390
  33. package/src/fallbacks/fallbacks.test.ts +0 -281
  34. package/src/fallbacks/index.ts +0 -26
  35. package/src/fallbacks/memory-cache.ts +0 -34
  36. package/src/fallbacks/memory-i18n.ts +0 -112
  37. package/src/fallbacks/memory-job.ts +0 -23
  38. package/src/fallbacks/memory-metadata.ts +0 -50
  39. package/src/fallbacks/memory-queue.ts +0 -28
  40. package/src/health-monitor.test.ts +0 -81
  41. package/src/health-monitor.ts +0 -318
  42. package/src/hot-reload.ts +0 -382
  43. package/src/index.ts +0 -50
  44. package/src/kernel-base.ts +0 -273
  45. package/src/kernel.test.ts +0 -624
  46. package/src/kernel.ts +0 -631
  47. package/src/lite-kernel.test.ts +0 -248
  48. package/src/lite-kernel.ts +0 -137
  49. package/src/logger.test.ts +0 -116
  50. package/src/logger.ts +0 -355
  51. package/src/namespace-resolver.test.ts +0 -130
  52. package/src/namespace-resolver.ts +0 -188
  53. package/src/package-manager.test.ts +0 -225
  54. package/src/package-manager.ts +0 -428
  55. package/src/plugin-loader.test.ts +0 -421
  56. package/src/plugin-loader.ts +0 -484
  57. package/src/qa/adapter.ts +0 -16
  58. package/src/qa/http-adapter.ts +0 -116
  59. package/src/qa/index.ts +0 -5
  60. package/src/qa/runner.ts +0 -189
  61. package/src/security/index.ts +0 -50
  62. package/src/security/permission-manager.test.ts +0 -256
  63. package/src/security/permission-manager.ts +0 -338
  64. package/src/security/plugin-config-validator.test.ts +0 -276
  65. package/src/security/plugin-config-validator.ts +0 -193
  66. package/src/security/plugin-permission-enforcer.test.ts +0 -251
  67. package/src/security/plugin-permission-enforcer.ts +0 -436
  68. package/src/security/plugin-signature-verifier.ts +0 -403
  69. package/src/security/sandbox-runtime.ts +0 -462
  70. package/src/security/security-scanner.ts +0 -367
  71. package/src/types.ts +0 -120
  72. package/src/utils/env.test.ts +0 -62
  73. package/src/utils/env.ts +0 -53
  74. package/tsconfig.json +0 -10
  75. package/vitest.config.ts +0 -10
@@ -1,1089 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { ApiRegistry } from './api-registry';
3
- import type {
4
- ApiRegistryEntryInput,
5
- } from '@objectstack/spec/api';
6
- import type { Logger } from '@objectstack/spec/contracts';
7
-
8
- // Mock logger
9
- const createMockLogger = (): Logger => ({
10
- debug: vi.fn(),
11
- info: vi.fn(),
12
- warn: vi.fn(),
13
- error: vi.fn(),
14
- });
15
-
16
- describe('ApiRegistry', () => {
17
- let registry: ApiRegistry;
18
- let logger: Logger;
19
-
20
- beforeEach(() => {
21
- logger = createMockLogger();
22
- registry = new ApiRegistry(logger, 'error', '1.0.0');
23
- });
24
-
25
- describe('Constructor', () => {
26
- it('should create registry with default conflict resolution', () => {
27
- const reg = new ApiRegistry(logger);
28
- const snapshot = reg.getRegistry();
29
- expect(snapshot.conflictResolution).toBe('error');
30
- expect(snapshot.version).toBe('1.0.0');
31
- });
32
-
33
- it('should create registry with custom conflict resolution', () => {
34
- const reg = new ApiRegistry(logger, 'priority', '2.0.0');
35
- const snapshot = reg.getRegistry();
36
- expect(snapshot.conflictResolution).toBe('priority');
37
- expect(snapshot.version).toBe('2.0.0');
38
- });
39
- });
40
-
41
- describe('registerApi', () => {
42
- it('should register a simple REST API', () => {
43
- const api: ApiRegistryEntryInput = {
44
- id: 'customer_api',
45
- name: 'Customer API',
46
- type: 'rest',
47
- version: 'v1',
48
- basePath: '/api/v1/customers',
49
- endpoints: [
50
- {
51
- id: 'get_customer',
52
- method: 'GET',
53
- path: '/api/v1/customers/:id',
54
- summary: 'Get customer by ID',
55
- responses: [
56
- {
57
- statusCode: 200,
58
- description: 'Success',
59
- },
60
- ],
61
- },
62
- ],
63
- };
64
-
65
- registry.registerApi(api);
66
-
67
- const retrieved = registry.getApi('customer_api');
68
- expect(retrieved).toBeDefined();
69
- expect(retrieved?.name).toBe('Customer API');
70
- expect(retrieved?.endpoints.length).toBe(1);
71
- });
72
-
73
- it('should throw error when registering duplicate API', () => {
74
- const api: ApiRegistryEntryInput = {
75
- id: 'test_api',
76
- name: 'Test API',
77
- type: 'rest',
78
- version: 'v1',
79
- basePath: '/api/test',
80
- endpoints: [],
81
- };
82
-
83
- registry.registerApi(api);
84
-
85
- expect(() => registry.registerApi(api)).toThrow(
86
- "API 'test_api' already registered"
87
- );
88
- });
89
-
90
- it('should register API with multiple endpoints', () => {
91
- const api: ApiRegistryEntryInput = {
92
- id: 'crud_api',
93
- name: 'CRUD API',
94
- type: 'rest',
95
- version: 'v1',
96
- basePath: '/api/v1/data',
97
- endpoints: [
98
- {
99
- id: 'create',
100
- method: 'POST',
101
- path: '/api/v1/data',
102
- summary: 'Create record',
103
- responses: [],
104
- },
105
- {
106
- id: 'read',
107
- method: 'GET',
108
- path: '/api/v1/data/:id',
109
- summary: 'Read record',
110
- responses: [],
111
- },
112
- {
113
- id: 'update',
114
- method: 'PUT',
115
- path: '/api/v1/data/:id',
116
- summary: 'Update record',
117
- responses: [],
118
- },
119
- {
120
- id: 'delete',
121
- method: 'DELETE',
122
- path: '/api/v1/data/:id',
123
- summary: 'Delete record',
124
- responses: [],
125
- },
126
- ],
127
- };
128
-
129
- registry.registerApi(api);
130
-
131
- const stats = registry.getStats();
132
- expect(stats.totalApis).toBe(1);
133
- expect(stats.totalEndpoints).toBe(4);
134
- expect(stats.totalRoutes).toBe(4);
135
- });
136
-
137
- it('should register API with RBAC permissions', () => {
138
- const api: ApiRegistryEntryInput = {
139
- id: 'protected_api',
140
- name: 'Protected API',
141
- type: 'rest',
142
- version: 'v1',
143
- basePath: '/api/protected',
144
- endpoints: [
145
- {
146
- id: 'admin_only',
147
- method: 'POST',
148
- path: '/api/protected/admin',
149
- summary: 'Admin endpoint',
150
- requiredPermissions: ['admin.access', 'api_enabled'],
151
- responses: [],
152
- },
153
- ],
154
- };
155
-
156
- registry.registerApi(api);
157
-
158
- const endpoint = registry.getEndpoint('protected_api', 'admin_only');
159
- expect(endpoint?.requiredPermissions).toEqual(['admin.access', 'api_enabled']);
160
- });
161
- });
162
-
163
- describe('unregisterApi', () => {
164
- it('should unregister an API', () => {
165
- const api: ApiRegistryEntryInput = {
166
- id: 'temp_api',
167
- name: 'Temporary API',
168
- type: 'rest',
169
- version: 'v1',
170
- basePath: '/api/temp',
171
- endpoints: [
172
- {
173
- id: 'test',
174
- method: 'GET',
175
- path: '/api/temp/test',
176
- responses: [],
177
- },
178
- ],
179
- };
180
-
181
- registry.registerApi(api);
182
- expect(registry.getApi('temp_api')).toBeDefined();
183
-
184
- registry.unregisterApi('temp_api');
185
- expect(registry.getApi('temp_api')).toBeUndefined();
186
- });
187
-
188
- it('should throw error when unregistering non-existent API', () => {
189
- expect(() => registry.unregisterApi('nonexistent')).toThrow(
190
- "API 'nonexistent' not found"
191
- );
192
- });
193
- });
194
-
195
- describe('Route Conflict Detection', () => {
196
- describe('error strategy', () => {
197
- it('should throw error on route conflict', () => {
198
- const api1: ApiRegistryEntryInput = {
199
- id: 'api1',
200
- name: 'API 1',
201
- type: 'rest',
202
- version: 'v1',
203
- basePath: '/api/v1',
204
- endpoints: [
205
- {
206
- id: 'endpoint1',
207
- method: 'GET',
208
- path: '/api/v1/test',
209
- responses: [],
210
- },
211
- ],
212
- };
213
-
214
- const api2: ApiRegistryEntryInput = {
215
- id: 'api2',
216
- name: 'API 2',
217
- type: 'rest',
218
- version: 'v1',
219
- basePath: '/api/v1',
220
- endpoints: [
221
- {
222
- id: 'endpoint2',
223
- method: 'GET',
224
- path: '/api/v1/test', // Same route!
225
- responses: [],
226
- },
227
- ],
228
- };
229
-
230
- registry.registerApi(api1);
231
- expect(() => registry.registerApi(api2)).toThrow(/Route conflict detected/);
232
- });
233
-
234
- it('should allow same path with different methods', () => {
235
- const api: ApiRegistryEntryInput = {
236
- id: 'multi_method',
237
- name: 'Multi Method API',
238
- type: 'rest',
239
- version: 'v1',
240
- basePath: '/api/v1',
241
- endpoints: [
242
- {
243
- id: 'get',
244
- method: 'GET',
245
- path: '/api/v1/resource',
246
- responses: [],
247
- },
248
- {
249
- id: 'post',
250
- method: 'POST',
251
- path: '/api/v1/resource',
252
- responses: [],
253
- },
254
- {
255
- id: 'put',
256
- method: 'PUT',
257
- path: '/api/v1/resource',
258
- responses: [],
259
- },
260
- ],
261
- };
262
-
263
- expect(() => registry.registerApi(api)).not.toThrow();
264
- expect(registry.getStats().totalRoutes).toBe(3);
265
- });
266
- });
267
-
268
- describe('priority strategy', () => {
269
- beforeEach(() => {
270
- registry = new ApiRegistry(logger, 'priority');
271
- });
272
-
273
- it('should prefer higher priority endpoint', () => {
274
- const api1: ApiRegistryEntryInput = {
275
- id: 'low_priority',
276
- name: 'Low Priority API',
277
- type: 'rest',
278
- version: 'v1',
279
- basePath: '/api',
280
- endpoints: [
281
- {
282
- id: 'low',
283
- method: 'GET',
284
- path: '/api/test',
285
- priority: 100,
286
- responses: [],
287
- },
288
- ],
289
- };
290
-
291
- const api2: ApiRegistryEntryInput = {
292
- id: 'high_priority',
293
- name: 'High Priority API',
294
- type: 'rest',
295
- version: 'v1',
296
- basePath: '/api',
297
- endpoints: [
298
- {
299
- id: 'high',
300
- method: 'GET',
301
- path: '/api/test',
302
- priority: 500,
303
- responses: [],
304
- },
305
- ],
306
- };
307
-
308
- registry.registerApi(api1);
309
- registry.registerApi(api2); // Should replace low priority
310
-
311
- const result = registry.findEndpointByRoute('GET', '/api/test');
312
- expect(result?.api.id).toBe('high_priority');
313
- expect(result?.endpoint.id).toBe('high');
314
- });
315
-
316
- it('should keep higher priority when registering lower priority', () => {
317
- const api1: ApiRegistryEntryInput = {
318
- id: 'high_priority',
319
- name: 'High Priority API',
320
- type: 'rest',
321
- version: 'v1',
322
- basePath: '/api',
323
- endpoints: [
324
- {
325
- id: 'high',
326
- method: 'GET',
327
- path: '/api/test',
328
- priority: 900,
329
- responses: [],
330
- },
331
- ],
332
- };
333
-
334
- const api2: ApiRegistryEntryInput = {
335
- id: 'low_priority',
336
- name: 'Low Priority API',
337
- type: 'rest',
338
- version: 'v1',
339
- basePath: '/api',
340
- endpoints: [
341
- {
342
- id: 'low',
343
- method: 'GET',
344
- path: '/api/test',
345
- priority: 100,
346
- responses: [],
347
- },
348
- ],
349
- };
350
-
351
- registry.registerApi(api1);
352
- registry.registerApi(api2); // Should NOT replace
353
-
354
- const result = registry.findEndpointByRoute('GET', '/api/test');
355
- expect(result?.api.id).toBe('high_priority');
356
- expect(result?.endpoint.id).toBe('high');
357
- });
358
- });
359
-
360
- describe('first-wins strategy', () => {
361
- beforeEach(() => {
362
- registry = new ApiRegistry(logger, 'first-wins');
363
- });
364
-
365
- it('should keep first registered endpoint', () => {
366
- const api1: ApiRegistryEntryInput = {
367
- id: 'first',
368
- name: 'First API',
369
- type: 'rest',
370
- version: 'v1',
371
- basePath: '/api',
372
- endpoints: [
373
- {
374
- id: 'first_endpoint',
375
- method: 'GET',
376
- path: '/api/test',
377
- responses: [],
378
- },
379
- ],
380
- };
381
-
382
- const api2: ApiRegistryEntryInput = {
383
- id: 'second',
384
- name: 'Second API',
385
- type: 'rest',
386
- version: 'v1',
387
- basePath: '/api',
388
- endpoints: [
389
- {
390
- id: 'second_endpoint',
391
- method: 'GET',
392
- path: '/api/test',
393
- responses: [],
394
- },
395
- ],
396
- };
397
-
398
- registry.registerApi(api1);
399
- registry.registerApi(api2);
400
-
401
- const result = registry.findEndpointByRoute('GET', '/api/test');
402
- expect(result?.api.id).toBe('first');
403
- expect(result?.endpoint.id).toBe('first_endpoint');
404
- });
405
- });
406
-
407
- describe('last-wins strategy', () => {
408
- beforeEach(() => {
409
- registry = new ApiRegistry(logger, 'last-wins');
410
- });
411
-
412
- it('should use last registered endpoint', () => {
413
- const api1: ApiRegistryEntryInput = {
414
- id: 'first',
415
- name: 'First API',
416
- type: 'rest',
417
- version: 'v1',
418
- basePath: '/api',
419
- endpoints: [
420
- {
421
- id: 'first_endpoint',
422
- method: 'GET',
423
- path: '/api/test',
424
- responses: [],
425
- },
426
- ],
427
- };
428
-
429
- const api2: ApiRegistryEntryInput = {
430
- id: 'second',
431
- name: 'Second API',
432
- type: 'rest',
433
- version: 'v1',
434
- basePath: '/api',
435
- endpoints: [
436
- {
437
- id: 'second_endpoint',
438
- method: 'GET',
439
- path: '/api/test',
440
- responses: [],
441
- },
442
- ],
443
- };
444
-
445
- registry.registerApi(api1);
446
- registry.registerApi(api2);
447
-
448
- const result = registry.findEndpointByRoute('GET', '/api/test');
449
- expect(result?.api.id).toBe('second');
450
- expect(result?.endpoint.id).toBe('second_endpoint');
451
- });
452
- });
453
- });
454
-
455
- describe('findApis', () => {
456
- beforeEach(() => {
457
- // Register multiple APIs for testing
458
- registry.registerApi({
459
- id: 'rest_api',
460
- name: 'REST API',
461
- type: 'rest',
462
- version: 'v1',
463
- basePath: '/api/v1/rest',
464
- endpoints: [],
465
- metadata: {
466
- status: 'active',
467
- tags: ['data', 'crud'],
468
- },
469
- });
470
-
471
- registry.registerApi({
472
- id: 'graphql_api',
473
- name: 'GraphQL API',
474
- type: 'graphql',
475
- version: 'v1',
476
- basePath: '/graphql',
477
- endpoints: [],
478
- metadata: {
479
- status: 'active',
480
- tags: ['query', 'data'],
481
- },
482
- });
483
-
484
- registry.registerApi({
485
- id: 'deprecated_api',
486
- name: 'Deprecated API',
487
- type: 'rest',
488
- version: 'v0',
489
- basePath: '/api/v0/old',
490
- endpoints: [],
491
- metadata: {
492
- status: 'deprecated',
493
- tags: ['legacy'],
494
- },
495
- });
496
- });
497
-
498
- it('should find all APIs with empty query', () => {
499
- const result = registry.findApis({});
500
- expect(result.total).toBe(3);
501
- expect(result.apis.length).toBe(3);
502
- });
503
-
504
- it('should filter by type', () => {
505
- const result = registry.findApis({ type: 'rest' });
506
- expect(result.total).toBe(2);
507
- expect(result.apis.every((api) => api.type === 'rest')).toBe(true);
508
- });
509
-
510
- it('should filter by status', () => {
511
- const result = registry.findApis({ status: 'active' });
512
- expect(result.total).toBe(2);
513
- expect(result.apis.every((api) => api.metadata?.status === 'active')).toBe(true);
514
- });
515
-
516
- it('should filter by version', () => {
517
- const result = registry.findApis({ version: 'v1' });
518
- expect(result.total).toBe(2);
519
- expect(result.apis.every((api) => api.version === 'v1')).toBe(true);
520
- });
521
-
522
- it('should filter by tags (ANY match)', () => {
523
- const result = registry.findApis({ tags: ['data'] });
524
- expect(result.total).toBe(2);
525
- });
526
-
527
- it('should search in name and description', () => {
528
- const result = registry.findApis({ search: 'graphql' });
529
- expect(result.total).toBe(1);
530
- expect(result.apis[0].id).toBe('graphql_api');
531
- });
532
-
533
- it('should combine multiple filters', () => {
534
- const result = registry.findApis({
535
- type: 'rest',
536
- status: 'active',
537
- tags: ['crud'],
538
- });
539
- expect(result.total).toBe(1);
540
- expect(result.apis[0].id).toBe('rest_api');
541
- });
542
- });
543
-
544
- describe('getEndpoint', () => {
545
- it('should get endpoint by API and endpoint ID', () => {
546
- const api: ApiRegistryEntryInput = {
547
- id: 'test_api',
548
- name: 'Test API',
549
- type: 'rest',
550
- version: 'v1',
551
- basePath: '/api/test',
552
- endpoints: [
553
- {
554
- id: 'test_endpoint',
555
- method: 'GET',
556
- path: '/api/test/hello',
557
- summary: 'Test endpoint',
558
- responses: [],
559
- },
560
- ],
561
- };
562
-
563
- registry.registerApi(api);
564
-
565
- const endpoint = registry.getEndpoint('test_api', 'test_endpoint');
566
- expect(endpoint).toBeDefined();
567
- expect(endpoint?.summary).toBe('Test endpoint');
568
- });
569
-
570
- it('should return undefined for non-existent endpoint', () => {
571
- const endpoint = registry.getEndpoint('nonexistent', 'also_nonexistent');
572
- expect(endpoint).toBeUndefined();
573
- });
574
- });
575
-
576
- describe('findEndpointByRoute', () => {
577
- it('should find endpoint by method and path', () => {
578
- const api: ApiRegistryEntryInput = {
579
- id: 'route_api',
580
- name: 'Route API',
581
- type: 'rest',
582
- version: 'v1',
583
- basePath: '/api',
584
- endpoints: [
585
- {
586
- id: 'get_users',
587
- method: 'GET',
588
- path: '/api/users',
589
- responses: [],
590
- },
591
- ],
592
- };
593
-
594
- registry.registerApi(api);
595
-
596
- const result = registry.findEndpointByRoute('GET', '/api/users');
597
- expect(result).toBeDefined();
598
- expect(result?.api.id).toBe('route_api');
599
- expect(result?.endpoint.id).toBe('get_users');
600
- });
601
-
602
- it('should return undefined for non-existent route', () => {
603
- const result = registry.findEndpointByRoute('POST', '/nonexistent');
604
- expect(result).toBeUndefined();
605
- });
606
- });
607
-
608
- describe('getRegistry', () => {
609
- it('should return complete registry snapshot', () => {
610
- registry.registerApi({
611
- id: 'api1',
612
- name: 'API 1',
613
- type: 'rest',
614
- version: 'v1',
615
- basePath: '/api/v1',
616
- endpoints: [
617
- { id: 'e1', path: '/api/v1/test', responses: [] },
618
- ],
619
- });
620
-
621
- const snapshot = registry.getRegistry();
622
- expect(snapshot.version).toBe('1.0.0');
623
- expect(snapshot.conflictResolution).toBe('error');
624
- expect(snapshot.totalApis).toBe(1);
625
- expect(snapshot.totalEndpoints).toBe(1);
626
- expect(snapshot.byType).toBeDefined();
627
- expect(snapshot.byStatus).toBeDefined();
628
- expect(snapshot.updatedAt).toBeDefined();
629
- });
630
-
631
- it('should group APIs by type', () => {
632
- registry.registerApi({
633
- id: 'rest1',
634
- name: 'REST 1',
635
- type: 'rest',
636
- version: 'v1',
637
- basePath: '/api/rest1',
638
- endpoints: [],
639
- });
640
-
641
- registry.registerApi({
642
- id: 'rest2',
643
- name: 'REST 2',
644
- type: 'rest',
645
- version: 'v1',
646
- basePath: '/api/rest2',
647
- endpoints: [],
648
- });
649
-
650
- registry.registerApi({
651
- id: 'graphql1',
652
- name: 'GraphQL 1',
653
- type: 'graphql',
654
- version: 'v1',
655
- basePath: '/graphql',
656
- endpoints: [],
657
- });
658
-
659
- const snapshot = registry.getRegistry();
660
- expect(snapshot.byType?.rest?.length).toBe(2);
661
- expect(snapshot.byType?.graphql?.length).toBe(1);
662
- });
663
- });
664
-
665
- describe('clear', () => {
666
- it('should clear all registered APIs', () => {
667
- registry.registerApi({
668
- id: 'test',
669
- name: 'Test',
670
- type: 'rest',
671
- version: 'v1',
672
- basePath: '/test',
673
- endpoints: [{ id: 'e1', path: '/test', responses: [] }],
674
- });
675
-
676
- expect(registry.getStats().totalApis).toBe(1);
677
-
678
- registry.clear();
679
-
680
- expect(registry.getStats().totalApis).toBe(0);
681
- expect(registry.getStats().totalEndpoints).toBe(0);
682
- expect(registry.getStats().totalRoutes).toBe(0);
683
- });
684
- });
685
-
686
- describe('getStats', () => {
687
- it('should return accurate statistics', () => {
688
- registry.registerApi({
689
- id: 'api1',
690
- name: 'API 1',
691
- type: 'rest',
692
- version: 'v1',
693
- basePath: '/api1',
694
- endpoints: [
695
- { id: 'e1', path: '/api1/e1', responses: [] },
696
- { id: 'e2', path: '/api1/e2', responses: [] },
697
- ],
698
- });
699
-
700
- registry.registerApi({
701
- id: 'api2',
702
- name: 'API 2',
703
- type: 'graphql',
704
- version: 'v1',
705
- basePath: '/graphql',
706
- endpoints: [
707
- { id: 'query', path: '/graphql', responses: [] },
708
- ],
709
- });
710
-
711
- const stats = registry.getStats();
712
- expect(stats.totalApis).toBe(2);
713
- expect(stats.totalEndpoints).toBe(3);
714
- expect(stats.totalRoutes).toBe(3);
715
- expect(stats.apisByType.rest).toBe(1);
716
- expect(stats.apisByType.graphql).toBe(1);
717
- expect(stats.endpointsByApi.api1).toBe(2);
718
- expect(stats.endpointsByApi.api2).toBe(1);
719
- });
720
- });
721
-
722
- describe('Multi-protocol Support', () => {
723
- it('should register GraphQL API', () => {
724
- const api: ApiRegistryEntryInput = {
725
- id: 'graphql',
726
- name: 'GraphQL API',
727
- type: 'graphql',
728
- version: 'v1',
729
- basePath: '/graphql',
730
- endpoints: [
731
- {
732
- id: 'query',
733
- path: '/graphql',
734
- summary: 'GraphQL Query',
735
- responses: [],
736
- },
737
- ],
738
- };
739
-
740
- registry.registerApi(api);
741
- expect(registry.getApi('graphql')?.type).toBe('graphql');
742
- });
743
-
744
- it('should register WebSocket API', () => {
745
- const api: ApiRegistryEntryInput = {
746
- id: 'websocket',
747
- name: 'WebSocket API',
748
- type: 'websocket',
749
- version: 'v1',
750
- basePath: '/ws',
751
- endpoints: [
752
- {
753
- id: 'subscribe',
754
- path: '/ws/events',
755
- summary: 'Subscribe to events',
756
- protocolConfig: {
757
- subProtocol: 'websocket',
758
- eventName: 'data.updated',
759
- direction: 'server-to-client',
760
- },
761
- responses: [],
762
- },
763
- ],
764
- };
765
-
766
- registry.registerApi(api);
767
- const endpoint = registry.getEndpoint('websocket', 'subscribe');
768
- expect(endpoint?.protocolConfig?.subProtocol).toBe('websocket');
769
- });
770
-
771
- it('should register Plugin API', () => {
772
- const api: ApiRegistryEntryInput = {
773
- id: 'custom_plugin',
774
- name: 'Custom Plugin API',
775
- type: 'plugin',
776
- version: '1.0.0',
777
- basePath: '/plugins/custom',
778
- endpoints: [
779
- {
780
- id: 'custom_action',
781
- method: 'POST',
782
- path: '/plugins/custom/action',
783
- summary: 'Custom plugin action',
784
- responses: [],
785
- },
786
- ],
787
- metadata: {
788
- pluginSource: 'custom_plugin_package',
789
- status: 'active',
790
- },
791
- };
792
-
793
- registry.registerApi(api);
794
- const result = registry.findApis({ pluginSource: 'custom_plugin_package' });
795
- expect(result.total).toBe(1);
796
- });
797
- });
798
-
799
- describe('Performance Optimizations', () => {
800
- it('should use indices for fast type-based lookups', () => {
801
- // Register multiple APIs with different types
802
- registry.registerApi({
803
- id: 'rest_api_1',
804
- name: 'REST API 1',
805
- type: 'rest',
806
- version: 'v1',
807
- basePath: '/api/rest1',
808
- endpoints: [{ id: 'e1', path: '/api/rest1', responses: [] }],
809
- });
810
-
811
- registry.registerApi({
812
- id: 'rest_api_2',
813
- name: 'REST API 2',
814
- type: 'rest',
815
- version: 'v1',
816
- basePath: '/api/rest2',
817
- endpoints: [{ id: 'e2', path: '/api/rest2', responses: [] }],
818
- });
819
-
820
- registry.registerApi({
821
- id: 'graphql_api',
822
- name: 'GraphQL API',
823
- type: 'graphql',
824
- version: 'v1',
825
- basePath: '/graphql',
826
- endpoints: [{ id: 'e3', path: '/graphql', responses: [] }],
827
- });
828
-
829
- // Should efficiently find all REST APIs
830
- const restApis = registry.findApis({ type: 'rest' });
831
- expect(restApis.total).toBe(2);
832
- expect(restApis.apis.every(api => api.type === 'rest')).toBe(true);
833
-
834
- // Should efficiently find GraphQL APIs
835
- const graphqlApis = registry.findApis({ type: 'graphql' });
836
- expect(graphqlApis.total).toBe(1);
837
- expect(graphqlApis.apis[0].id).toBe('graphql_api');
838
- });
839
-
840
- it('should use indices for fast tag-based lookups', () => {
841
- registry.registerApi({
842
- id: 'api_1',
843
- name: 'API 1',
844
- type: 'rest',
845
- version: 'v1',
846
- basePath: '/api1',
847
- endpoints: [{ id: 'e1', path: '/api1', responses: [] }],
848
- metadata: { tags: ['customer', 'crm'] },
849
- });
850
-
851
- registry.registerApi({
852
- id: 'api_2',
853
- name: 'API 2',
854
- type: 'rest',
855
- version: 'v1',
856
- basePath: '/api2',
857
- endpoints: [{ id: 'e2', path: '/api2', responses: [] }],
858
- metadata: { tags: ['order', 'sales'] },
859
- });
860
-
861
- registry.registerApi({
862
- id: 'api_3',
863
- name: 'API 3',
864
- type: 'rest',
865
- version: 'v1',
866
- basePath: '/api3',
867
- endpoints: [{ id: 'e3', path: '/api3', responses: [] }],
868
- metadata: { tags: ['customer', 'analytics'] },
869
- });
870
-
871
- // Should efficiently find APIs by tag
872
- const customerApis = registry.findApis({ tags: ['customer'] });
873
- expect(customerApis.total).toBe(2);
874
- expect(customerApis.apis.map(a => a.id).sort()).toEqual(['api_1', 'api_3']);
875
-
876
- // Should support multiple tags (ANY match)
877
- const multiTagApis = registry.findApis({ tags: ['crm', 'sales'] });
878
- expect(multiTagApis.total).toBe(2);
879
- });
880
-
881
- it('should use indices for fast status-based lookups', () => {
882
- registry.registerApi({
883
- id: 'active_api',
884
- name: 'Active API',
885
- type: 'rest',
886
- version: 'v1',
887
- basePath: '/active',
888
- endpoints: [{ id: 'e1', path: '/active', responses: [] }],
889
- metadata: { status: 'active' },
890
- });
891
-
892
- registry.registerApi({
893
- id: 'beta_api',
894
- name: 'Beta API',
895
- type: 'rest',
896
- version: 'v1',
897
- basePath: '/beta',
898
- endpoints: [{ id: 'e2', path: '/beta', responses: [] }],
899
- metadata: { status: 'beta' },
900
- });
901
-
902
- registry.registerApi({
903
- id: 'deprecated_api',
904
- name: 'Deprecated API',
905
- type: 'rest',
906
- version: 'v1',
907
- basePath: '/deprecated',
908
- endpoints: [{ id: 'e3', path: '/deprecated', responses: [] }],
909
- metadata: { status: 'deprecated' },
910
- });
911
-
912
- // Should efficiently find by status
913
- const activeApis = registry.findApis({ status: 'active' });
914
- expect(activeApis.total).toBe(1);
915
- expect(activeApis.apis[0].id).toBe('active_api');
916
-
917
- const betaApis = registry.findApis({ status: 'beta' });
918
- expect(betaApis.total).toBe(1);
919
- });
920
-
921
- it('should combine multiple indexed filters efficiently', () => {
922
- registry.registerApi({
923
- id: 'rest_crm_active',
924
- name: 'REST CRM Active',
925
- type: 'rest',
926
- version: 'v1',
927
- basePath: '/crm',
928
- endpoints: [{ id: 'e1', path: '/crm', responses: [] }],
929
- metadata: { status: 'active', tags: ['crm', 'customer'] },
930
- });
931
-
932
- registry.registerApi({
933
- id: 'rest_crm_beta',
934
- name: 'REST CRM Beta',
935
- type: 'rest',
936
- version: 'v1',
937
- basePath: '/crm-beta',
938
- endpoints: [{ id: 'e2', path: '/crm-beta', responses: [] }],
939
- metadata: { status: 'beta', tags: ['crm'] },
940
- });
941
-
942
- registry.registerApi({
943
- id: 'graphql_crm_active',
944
- name: 'GraphQL CRM Active',
945
- type: 'graphql',
946
- version: 'v1',
947
- basePath: '/graphql',
948
- endpoints: [{ id: 'e3', path: '/graphql', responses: [] }],
949
- metadata: { status: 'active', tags: ['crm'] },
950
- });
951
-
952
- // Combine type + status + tags filters
953
- const result = registry.findApis({
954
- type: 'rest',
955
- status: 'active',
956
- tags: ['crm'],
957
- });
958
-
959
- expect(result.total).toBe(1);
960
- expect(result.apis[0].id).toBe('rest_crm_active');
961
- });
962
-
963
- it('should maintain indices when APIs are unregistered', () => {
964
- registry.registerApi({
965
- id: 'temp_api',
966
- name: 'Temporary API',
967
- type: 'rest',
968
- version: 'v1',
969
- basePath: '/temp',
970
- endpoints: [{ id: 'e1', path: '/temp', responses: [] }],
971
- metadata: { status: 'beta', tags: ['temp', 'test'] },
972
- });
973
-
974
- // Verify it's in indices
975
- expect(registry.findApis({ type: 'rest' }).total).toBe(1);
976
- expect(registry.findApis({ status: 'beta' }).total).toBe(1);
977
- expect(registry.findApis({ tags: ['temp'] }).total).toBe(1);
978
-
979
- // Unregister
980
- registry.unregisterApi('temp_api');
981
-
982
- // Verify removed from indices
983
- expect(registry.findApis({ type: 'rest' }).total).toBe(0);
984
- expect(registry.findApis({ status: 'beta' }).total).toBe(0);
985
- expect(registry.findApis({ tags: ['temp'] }).total).toBe(0);
986
- });
987
- });
988
-
989
- describe('Safety Guards', () => {
990
- it('should allow clear() in non-production environment', () => {
991
- const originalEnv = process.env.NODE_ENV;
992
- try {
993
- process.env.NODE_ENV = 'test';
994
-
995
- registry.registerApi({
996
- id: 'test_api',
997
- name: 'Test API',
998
- type: 'rest',
999
- version: 'v1',
1000
- basePath: '/test',
1001
- endpoints: [{ id: 'e1', path: '/test', responses: [] }],
1002
- });
1003
-
1004
- expect(registry.getStats().totalApis).toBe(1);
1005
-
1006
- // Should work without force flag in non-production
1007
- registry.clear();
1008
- expect(registry.getStats().totalApis).toBe(0);
1009
- } finally {
1010
- process.env.NODE_ENV = originalEnv;
1011
- }
1012
- });
1013
-
1014
- it('should prevent clear() in production without force flag', () => {
1015
- const originalEnv = process.env.NODE_ENV;
1016
- try {
1017
- process.env.NODE_ENV = 'production';
1018
-
1019
- registry.registerApi({
1020
- id: 'prod_api',
1021
- name: 'Production API',
1022
- type: 'rest',
1023
- version: 'v1',
1024
- basePath: '/prod',
1025
- endpoints: [{ id: 'e1', path: '/prod', responses: [] }],
1026
- });
1027
-
1028
- // Should throw error in production without force flag
1029
- expect(() => registry.clear()).toThrow(
1030
- 'Cannot clear registry in production environment without force flag'
1031
- );
1032
-
1033
- // API should still exist
1034
- expect(registry.getStats().totalApis).toBe(1);
1035
- } finally {
1036
- process.env.NODE_ENV = originalEnv;
1037
- }
1038
- });
1039
-
1040
- it('should allow clear() in production with force flag', () => {
1041
- const originalEnv = process.env.NODE_ENV;
1042
- try {
1043
- process.env.NODE_ENV = 'production';
1044
-
1045
- registry.registerApi({
1046
- id: 'prod_api',
1047
- name: 'Production API',
1048
- type: 'rest',
1049
- version: 'v1',
1050
- basePath: '/prod',
1051
- endpoints: [{ id: 'e1', path: '/prod', responses: [] }],
1052
- });
1053
-
1054
- expect(registry.getStats().totalApis).toBe(1);
1055
-
1056
- // Should work with force flag
1057
- registry.clear({ force: true });
1058
- expect(registry.getStats().totalApis).toBe(0);
1059
-
1060
- // Verify logger warned about forced clear
1061
- expect(logger.warn).toHaveBeenCalledWith(
1062
- 'API registry forcefully cleared in production',
1063
- { force: true }
1064
- );
1065
- } finally {
1066
- process.env.NODE_ENV = originalEnv;
1067
- }
1068
- });
1069
-
1070
- it('should clear all indices when clear() is called', () => {
1071
- registry.registerApi({
1072
- id: 'api_1',
1073
- name: 'API 1',
1074
- type: 'rest',
1075
- version: 'v1',
1076
- basePath: '/api1',
1077
- endpoints: [{ id: 'e1', path: '/api1', responses: [] }],
1078
- metadata: { status: 'active', tags: ['test'] },
1079
- });
1080
-
1081
- registry.clear();
1082
-
1083
- // All lookups should return empty
1084
- expect(registry.findApis({ type: 'rest' }).total).toBe(0);
1085
- expect(registry.findApis({ status: 'active' }).total).toBe(0);
1086
- expect(registry.findApis({ tags: ['test'] }).total).toBe(0);
1087
- });
1088
- });
1089
- });