@objectstack/core 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/API_REGISTRY.md +392 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +36 -0
  4. package/dist/api-registry-plugin.d.ts +54 -0
  5. package/dist/api-registry-plugin.d.ts.map +1 -0
  6. package/dist/api-registry-plugin.js +53 -0
  7. package/dist/api-registry-plugin.test.d.ts +2 -0
  8. package/dist/api-registry-plugin.test.d.ts.map +1 -0
  9. package/dist/api-registry-plugin.test.js +332 -0
  10. package/dist/api-registry.d.ts +259 -0
  11. package/dist/api-registry.d.ts.map +1 -0
  12. package/dist/api-registry.js +599 -0
  13. package/dist/api-registry.test.d.ts +2 -0
  14. package/dist/api-registry.test.d.ts.map +1 -0
  15. package/dist/api-registry.test.js +957 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/logger.d.ts +1 -0
  20. package/dist/logger.d.ts.map +1 -1
  21. package/dist/logger.js +35 -11
  22. package/dist/plugin-loader.d.ts +3 -2
  23. package/dist/plugin-loader.d.ts.map +1 -1
  24. package/dist/plugin-loader.js +13 -11
  25. package/dist/qa/adapter.d.ts +14 -0
  26. package/dist/qa/adapter.d.ts.map +1 -0
  27. package/dist/qa/adapter.js +1 -0
  28. package/dist/qa/http-adapter.d.ts +16 -0
  29. package/dist/qa/http-adapter.d.ts.map +1 -0
  30. package/dist/qa/http-adapter.js +107 -0
  31. package/dist/qa/index.d.ts +4 -0
  32. package/dist/qa/index.d.ts.map +1 -0
  33. package/dist/qa/index.js +3 -0
  34. package/dist/qa/runner.d.ts +27 -0
  35. package/dist/qa/runner.d.ts.map +1 -0
  36. package/dist/qa/runner.js +157 -0
  37. package/dist/security/index.d.ts +14 -0
  38. package/dist/security/index.d.ts.map +1 -0
  39. package/dist/security/index.js +13 -0
  40. package/dist/security/plugin-config-validator.d.ts +79 -0
  41. package/dist/security/plugin-config-validator.d.ts.map +1 -0
  42. package/dist/security/plugin-config-validator.js +166 -0
  43. package/dist/security/plugin-config-validator.test.d.ts +2 -0
  44. package/dist/security/plugin-config-validator.test.d.ts.map +1 -0
  45. package/dist/security/plugin-config-validator.test.js +223 -0
  46. package/dist/security/plugin-permission-enforcer.d.ts +154 -0
  47. package/dist/security/plugin-permission-enforcer.d.ts.map +1 -0
  48. package/dist/security/plugin-permission-enforcer.js +323 -0
  49. package/dist/security/plugin-permission-enforcer.test.d.ts +2 -0
  50. package/dist/security/plugin-permission-enforcer.test.d.ts.map +1 -0
  51. package/dist/security/plugin-permission-enforcer.test.js +205 -0
  52. package/dist/security/plugin-signature-verifier.d.ts +96 -0
  53. package/dist/security/plugin-signature-verifier.d.ts.map +1 -0
  54. package/dist/security/plugin-signature-verifier.js +250 -0
  55. package/examples/api-registry-example.ts +557 -0
  56. package/package.json +2 -2
  57. package/src/api-registry-plugin.test.ts +391 -0
  58. package/src/api-registry-plugin.ts +86 -0
  59. package/src/api-registry.test.ts +1089 -0
  60. package/src/api-registry.ts +736 -0
  61. package/src/index.ts +6 -0
  62. package/src/logger.ts +36 -11
  63. package/src/plugin-loader.ts +17 -13
  64. package/src/qa/adapter.ts +14 -0
  65. package/src/qa/http-adapter.ts +114 -0
  66. package/src/qa/index.ts +3 -0
  67. package/src/qa/runner.ts +179 -0
  68. package/src/security/index.ts +29 -0
  69. package/src/security/plugin-config-validator.test.ts +276 -0
  70. package/src/security/plugin-config-validator.ts +191 -0
  71. package/src/security/plugin-permission-enforcer.test.ts +251 -0
  72. package/src/security/plugin-permission-enforcer.ts +408 -0
  73. package/src/security/plugin-signature-verifier.ts +359 -0
@@ -0,0 +1,599 @@
1
+ import { ApiRegistryEntrySchema } from '@objectstack/spec/api';
2
+ /**
3
+ * API Registry Service
4
+ *
5
+ * Central registry for managing API endpoints across different protocols.
6
+ * Provides endpoint registration, discovery, and conflict resolution.
7
+ *
8
+ * **Features:**
9
+ * - Multi-protocol support (REST, GraphQL, OData, WebSocket, etc.)
10
+ * - Route conflict detection with configurable resolution strategies
11
+ * - RBAC permission integration
12
+ * - Dynamic schema linking with ObjectQL references
13
+ * - Plugin API registration
14
+ *
15
+ * **Architecture Alignment:**
16
+ * - Kubernetes: Service Discovery & API Server
17
+ * - AWS API Gateway: Unified API Management
18
+ * - Kong Gateway: Plugin-based API Management
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const registry = new ApiRegistry(logger, 'priority');
23
+ *
24
+ * // Register an API
25
+ * registry.registerApi({
26
+ * id: 'customer_api',
27
+ * name: 'Customer API',
28
+ * type: 'rest',
29
+ * version: 'v1',
30
+ * basePath: '/api/v1/customers',
31
+ * endpoints: [...]
32
+ * });
33
+ *
34
+ * // Discover APIs
35
+ * const apis = registry.findApis({ type: 'rest', status: 'active' });
36
+ *
37
+ * // Get registry snapshot
38
+ * const snapshot = registry.getRegistry();
39
+ * ```
40
+ */
41
+ export class ApiRegistry {
42
+ constructor(logger, conflictResolution = 'error', version = '1.0.0') {
43
+ this.apis = new Map();
44
+ this.endpoints = new Map();
45
+ this.routes = new Map();
46
+ // Performance optimization: Auxiliary indices for O(1) lookups
47
+ this.apisByType = new Map();
48
+ this.apisByTag = new Map();
49
+ this.apisByStatus = new Map();
50
+ this.logger = logger;
51
+ this.conflictResolution = conflictResolution;
52
+ this.version = version;
53
+ this.updatedAt = new Date().toISOString();
54
+ }
55
+ /**
56
+ * Register an API with its endpoints
57
+ *
58
+ * @param api - API registry entry
59
+ * @throws Error if API already registered or route conflicts detected
60
+ */
61
+ registerApi(api) {
62
+ // Check if API already exists
63
+ if (this.apis.has(api.id)) {
64
+ throw new Error(`[ApiRegistry] API '${api.id}' already registered`);
65
+ }
66
+ // Parse and validate the input using Zod schema
67
+ const fullApi = ApiRegistryEntrySchema.parse(api);
68
+ // Validate and register endpoints
69
+ for (const endpoint of fullApi.endpoints) {
70
+ this.validateEndpoint(endpoint, fullApi.id);
71
+ }
72
+ // Register the API
73
+ this.apis.set(fullApi.id, fullApi);
74
+ // Register endpoints
75
+ for (const endpoint of fullApi.endpoints) {
76
+ this.registerEndpoint(fullApi.id, endpoint);
77
+ }
78
+ // Update auxiliary indices for performance optimization
79
+ this.updateIndices(fullApi);
80
+ this.updatedAt = new Date().toISOString();
81
+ this.logger.info(`API registered: ${fullApi.id}`, {
82
+ api: fullApi.id,
83
+ type: fullApi.type,
84
+ endpointCount: fullApi.endpoints.length,
85
+ });
86
+ }
87
+ /**
88
+ * Unregister an API and all its endpoints
89
+ *
90
+ * @param apiId - API identifier
91
+ */
92
+ unregisterApi(apiId) {
93
+ const api = this.apis.get(apiId);
94
+ if (!api) {
95
+ throw new Error(`[ApiRegistry] API '${apiId}' not found`);
96
+ }
97
+ // Remove all endpoints
98
+ for (const endpoint of api.endpoints) {
99
+ this.unregisterEndpoint(apiId, endpoint.id);
100
+ }
101
+ // Remove from auxiliary indices
102
+ this.removeFromIndices(api);
103
+ // Remove the API
104
+ this.apis.delete(apiId);
105
+ this.updatedAt = new Date().toISOString();
106
+ this.logger.info(`API unregistered: ${apiId}`);
107
+ }
108
+ /**
109
+ * Register a single endpoint
110
+ *
111
+ * @param apiId - API identifier
112
+ * @param endpoint - Endpoint registration
113
+ * @throws Error if route conflict detected
114
+ */
115
+ registerEndpoint(apiId, endpoint) {
116
+ const endpointKey = `${apiId}:${endpoint.id}`;
117
+ // Check if endpoint already registered
118
+ if (this.endpoints.has(endpointKey)) {
119
+ throw new Error(`[ApiRegistry] Endpoint '${endpoint.id}' already registered for API '${apiId}'`);
120
+ }
121
+ // Register endpoint
122
+ this.endpoints.set(endpointKey, { api: apiId, endpoint });
123
+ // Register route if path is defined
124
+ if (endpoint.path) {
125
+ this.registerRoute(apiId, endpoint);
126
+ }
127
+ }
128
+ /**
129
+ * Unregister a single endpoint
130
+ *
131
+ * @param apiId - API identifier
132
+ * @param endpointId - Endpoint identifier
133
+ */
134
+ unregisterEndpoint(apiId, endpointId) {
135
+ const endpointKey = `${apiId}:${endpointId}`;
136
+ const entry = this.endpoints.get(endpointKey);
137
+ if (!entry) {
138
+ return; // Already unregistered
139
+ }
140
+ // Unregister route
141
+ if (entry.endpoint.path) {
142
+ const routeKey = this.getRouteKey(entry.endpoint);
143
+ this.routes.delete(routeKey);
144
+ }
145
+ // Unregister endpoint
146
+ this.endpoints.delete(endpointKey);
147
+ }
148
+ /**
149
+ * Register a route with conflict detection
150
+ *
151
+ * @param apiId - API identifier
152
+ * @param endpoint - Endpoint registration
153
+ * @throws Error if route conflict detected (based on strategy)
154
+ */
155
+ registerRoute(apiId, endpoint) {
156
+ const routeKey = this.getRouteKey(endpoint);
157
+ const priority = endpoint.priority ?? 100;
158
+ const existingRoute = this.routes.get(routeKey);
159
+ if (existingRoute) {
160
+ // Route conflict detected
161
+ this.handleRouteConflict(routeKey, apiId, endpoint, existingRoute, priority);
162
+ return;
163
+ }
164
+ // Register route
165
+ this.routes.set(routeKey, {
166
+ api: apiId,
167
+ endpointId: endpoint.id,
168
+ priority,
169
+ });
170
+ }
171
+ /**
172
+ * Handle route conflict based on resolution strategy
173
+ *
174
+ * @param routeKey - Route key
175
+ * @param apiId - New API identifier
176
+ * @param endpoint - New endpoint
177
+ * @param existingRoute - Existing route registration
178
+ * @param newPriority - New endpoint priority
179
+ * @throws Error if strategy is 'error'
180
+ */
181
+ handleRouteConflict(routeKey, apiId, endpoint, existingRoute, newPriority) {
182
+ const strategy = this.conflictResolution;
183
+ switch (strategy) {
184
+ case 'error':
185
+ throw new Error(`[ApiRegistry] Route conflict detected: '${routeKey}' is already registered by API '${existingRoute.api}' endpoint '${existingRoute.endpointId}'`);
186
+ case 'priority':
187
+ if (newPriority > existingRoute.priority) {
188
+ // New endpoint has higher priority, replace
189
+ this.logger.warn(`Route conflict: replacing '${routeKey}' (priority ${existingRoute.priority} -> ${newPriority})`, {
190
+ oldApi: existingRoute.api,
191
+ oldEndpoint: existingRoute.endpointId,
192
+ newApi: apiId,
193
+ newEndpoint: endpoint.id,
194
+ });
195
+ this.routes.set(routeKey, {
196
+ api: apiId,
197
+ endpointId: endpoint.id,
198
+ priority: newPriority,
199
+ });
200
+ }
201
+ else {
202
+ // Existing endpoint has higher priority, keep it
203
+ this.logger.warn(`Route conflict: keeping existing '${routeKey}' (priority ${existingRoute.priority} >= ${newPriority})`, {
204
+ existingApi: existingRoute.api,
205
+ existingEndpoint: existingRoute.endpointId,
206
+ newApi: apiId,
207
+ newEndpoint: endpoint.id,
208
+ });
209
+ }
210
+ break;
211
+ case 'first-wins':
212
+ // Keep existing route
213
+ this.logger.warn(`Route conflict: keeping first registered '${routeKey}'`, {
214
+ existingApi: existingRoute.api,
215
+ newApi: apiId,
216
+ });
217
+ break;
218
+ case 'last-wins':
219
+ // Replace with new route
220
+ this.logger.warn(`Route conflict: replacing with last registered '${routeKey}'`, {
221
+ oldApi: existingRoute.api,
222
+ newApi: apiId,
223
+ });
224
+ this.routes.set(routeKey, {
225
+ api: apiId,
226
+ endpointId: endpoint.id,
227
+ priority: newPriority,
228
+ });
229
+ break;
230
+ default:
231
+ throw new Error(`[ApiRegistry] Unknown conflict resolution strategy: ${strategy}`);
232
+ }
233
+ }
234
+ /**
235
+ * Generate a unique route key for conflict detection
236
+ *
237
+ * NOTE: This implementation uses exact string matching for route conflict detection.
238
+ * It works well for static paths but has limitations with parameterized routes.
239
+ * For example, `/api/users/:id` and `/api/users/:userId` will NOT be detected as conflicts
240
+ * even though they are semantically identical parameterized patterns. Similarly,
241
+ * `/api/:resource/list` and `/api/:entity/list` would also not be detected as conflicting.
242
+ *
243
+ * For more advanced conflict detection (e.g., path-to-regexp pattern matching),
244
+ * consider integrating with your routing library's conflict detection mechanism.
245
+ *
246
+ * @param endpoint - Endpoint registration
247
+ * @returns Route key (e.g., "GET:/api/v1/customers/:id")
248
+ */
249
+ getRouteKey(endpoint) {
250
+ const method = endpoint.method || 'ANY';
251
+ return `${method}:${endpoint.path}`;
252
+ }
253
+ /**
254
+ * Validate endpoint registration
255
+ *
256
+ * @param endpoint - Endpoint to validate
257
+ * @param apiId - API identifier (for error messages)
258
+ * @throws Error if endpoint is invalid
259
+ */
260
+ validateEndpoint(endpoint, apiId) {
261
+ if (!endpoint.id) {
262
+ throw new Error(`[ApiRegistry] Endpoint in API '${apiId}' missing 'id' field`);
263
+ }
264
+ if (!endpoint.path) {
265
+ throw new Error(`[ApiRegistry] Endpoint '${endpoint.id}' in API '${apiId}' missing 'path' field`);
266
+ }
267
+ }
268
+ /**
269
+ * Get an API by ID
270
+ *
271
+ * @param apiId - API identifier
272
+ * @returns API registry entry or undefined
273
+ */
274
+ getApi(apiId) {
275
+ return this.apis.get(apiId);
276
+ }
277
+ /**
278
+ * Get all registered APIs
279
+ *
280
+ * @returns Array of all APIs
281
+ */
282
+ getAllApis() {
283
+ return Array.from(this.apis.values());
284
+ }
285
+ /**
286
+ * Find APIs matching query criteria
287
+ *
288
+ * Performance optimized with auxiliary indices for O(1) lookups on type, tags, and status.
289
+ *
290
+ * @param query - Discovery query parameters
291
+ * @returns Matching APIs
292
+ */
293
+ findApis(query) {
294
+ let resultIds;
295
+ // Use indices for performance-optimized filtering
296
+ // Start with the most restrictive filter to minimize subsequent filtering
297
+ // Filter by type (using index for O(1) lookup)
298
+ if (query.type) {
299
+ const typeIds = this.apisByType.get(query.type);
300
+ if (!typeIds || typeIds.size === 0) {
301
+ return { apis: [], total: 0, filters: query };
302
+ }
303
+ resultIds = new Set(typeIds);
304
+ }
305
+ // Filter by status (using index for O(1) lookup)
306
+ if (query.status) {
307
+ const statusIds = this.apisByStatus.get(query.status);
308
+ if (!statusIds || statusIds.size === 0) {
309
+ return { apis: [], total: 0, filters: query };
310
+ }
311
+ if (resultIds) {
312
+ // Intersect with previous results
313
+ resultIds = new Set([...resultIds].filter(id => statusIds.has(id)));
314
+ }
315
+ else {
316
+ resultIds = new Set(statusIds);
317
+ }
318
+ if (resultIds.size === 0) {
319
+ return { apis: [], total: 0, filters: query };
320
+ }
321
+ }
322
+ // Filter by tags (using index for O(M) lookup where M is number of tags)
323
+ if (query.tags && query.tags.length > 0) {
324
+ const tagMatches = new Set();
325
+ for (const tag of query.tags) {
326
+ const tagIds = this.apisByTag.get(tag);
327
+ if (tagIds) {
328
+ tagIds.forEach(id => tagMatches.add(id));
329
+ }
330
+ }
331
+ if (tagMatches.size === 0) {
332
+ return { apis: [], total: 0, filters: query };
333
+ }
334
+ if (resultIds) {
335
+ // Intersect with previous results
336
+ resultIds = new Set([...resultIds].filter(id => tagMatches.has(id)));
337
+ }
338
+ else {
339
+ resultIds = tagMatches;
340
+ }
341
+ if (resultIds.size === 0) {
342
+ return { apis: [], total: 0, filters: query };
343
+ }
344
+ }
345
+ // Get the actual API objects
346
+ let results;
347
+ if (resultIds) {
348
+ results = Array.from(resultIds)
349
+ .map(id => this.apis.get(id))
350
+ .filter((api) => api !== undefined);
351
+ }
352
+ else {
353
+ results = Array.from(this.apis.values());
354
+ }
355
+ // Apply remaining filters that don't have indices (less common filters)
356
+ // Filter by plugin source
357
+ if (query.pluginSource) {
358
+ results = results.filter((api) => api.metadata?.pluginSource === query.pluginSource);
359
+ }
360
+ // Filter by version
361
+ if (query.version) {
362
+ results = results.filter((api) => api.version === query.version);
363
+ }
364
+ // Search in name/description
365
+ if (query.search) {
366
+ const searchLower = query.search.toLowerCase();
367
+ results = results.filter((api) => api.name.toLowerCase().includes(searchLower) ||
368
+ (api.description && api.description.toLowerCase().includes(searchLower)));
369
+ }
370
+ return {
371
+ apis: results,
372
+ total: results.length,
373
+ filters: query,
374
+ };
375
+ }
376
+ /**
377
+ * Get endpoint by API ID and endpoint ID
378
+ *
379
+ * @param apiId - API identifier
380
+ * @param endpointId - Endpoint identifier
381
+ * @returns Endpoint registration or undefined
382
+ */
383
+ getEndpoint(apiId, endpointId) {
384
+ const key = `${apiId}:${endpointId}`;
385
+ return this.endpoints.get(key)?.endpoint;
386
+ }
387
+ /**
388
+ * Find endpoint by route (method + path)
389
+ *
390
+ * @param method - HTTP method
391
+ * @param path - URL path
392
+ * @returns Endpoint registration or undefined
393
+ */
394
+ findEndpointByRoute(method, path) {
395
+ const routeKey = `${method}:${path}`;
396
+ const route = this.routes.get(routeKey);
397
+ if (!route) {
398
+ return undefined;
399
+ }
400
+ const api = this.apis.get(route.api);
401
+ const endpoint = this.getEndpoint(route.api, route.endpointId);
402
+ if (!api || !endpoint) {
403
+ return undefined;
404
+ }
405
+ return { api, endpoint };
406
+ }
407
+ /**
408
+ * Get complete registry snapshot
409
+ *
410
+ * @returns Current registry state
411
+ */
412
+ getRegistry() {
413
+ const apis = Array.from(this.apis.values());
414
+ // Group by type
415
+ const byType = {};
416
+ for (const api of apis) {
417
+ if (!byType[api.type]) {
418
+ byType[api.type] = [];
419
+ }
420
+ byType[api.type].push(api);
421
+ }
422
+ // Group by status
423
+ const byStatus = {};
424
+ for (const api of apis) {
425
+ const status = api.metadata?.status || 'active';
426
+ if (!byStatus[status]) {
427
+ byStatus[status] = [];
428
+ }
429
+ byStatus[status].push(api);
430
+ }
431
+ // Count total endpoints
432
+ const totalEndpoints = apis.reduce((sum, api) => sum + api.endpoints.length, 0);
433
+ return {
434
+ version: this.version,
435
+ conflictResolution: this.conflictResolution,
436
+ apis,
437
+ totalApis: apis.length,
438
+ totalEndpoints,
439
+ byType,
440
+ byStatus,
441
+ updatedAt: this.updatedAt,
442
+ };
443
+ }
444
+ /**
445
+ * Clear all registered APIs
446
+ *
447
+ * **⚠️ SAFETY WARNING:**
448
+ * This method clears all registered APIs and should be used with caution.
449
+ *
450
+ * **Usage Restrictions:**
451
+ * - In production environments (NODE_ENV=production), a `force: true` parameter is required
452
+ * - Primarily intended for testing and development hot-reload scenarios
453
+ *
454
+ * @param options - Clear options
455
+ * @param options.force - Force clear in production environment (default: false)
456
+ * @throws Error if called in production without force flag
457
+ *
458
+ * @example Safe usage in tests
459
+ * ```typescript
460
+ * beforeEach(() => {
461
+ * registry.clear(); // OK in test environment
462
+ * });
463
+ * ```
464
+ *
465
+ * @example Usage in production (requires explicit force)
466
+ * ```typescript
467
+ * // In production, explicit force is required
468
+ * registry.clear({ force: true });
469
+ * ```
470
+ */
471
+ clear(options = {}) {
472
+ const isProduction = this.isProductionEnvironment();
473
+ if (isProduction && !options.force) {
474
+ throw new Error('[ApiRegistry] Cannot clear registry in production environment without force flag. ' +
475
+ 'Use clear({ force: true }) if you really want to clear the registry.');
476
+ }
477
+ this.apis.clear();
478
+ this.endpoints.clear();
479
+ this.routes.clear();
480
+ // Clear auxiliary indices
481
+ this.apisByType.clear();
482
+ this.apisByTag.clear();
483
+ this.apisByStatus.clear();
484
+ this.updatedAt = new Date().toISOString();
485
+ if (isProduction) {
486
+ this.logger.warn('API registry forcefully cleared in production', { force: options.force });
487
+ }
488
+ else {
489
+ this.logger.info('API registry cleared');
490
+ }
491
+ }
492
+ /**
493
+ * Get registry statistics
494
+ *
495
+ * @returns Registry statistics
496
+ */
497
+ getStats() {
498
+ const apis = Array.from(this.apis.values());
499
+ const apisByType = {};
500
+ for (const api of apis) {
501
+ apisByType[api.type] = (apisByType[api.type] || 0) + 1;
502
+ }
503
+ const endpointsByApi = {};
504
+ for (const api of apis) {
505
+ endpointsByApi[api.id] = api.endpoints.length;
506
+ }
507
+ return {
508
+ totalApis: this.apis.size,
509
+ totalEndpoints: this.endpoints.size,
510
+ totalRoutes: this.routes.size,
511
+ apisByType,
512
+ endpointsByApi,
513
+ };
514
+ }
515
+ /**
516
+ * Update auxiliary indices when an API is registered
517
+ *
518
+ * @param api - API entry to index
519
+ * @private
520
+ * @internal
521
+ */
522
+ updateIndices(api) {
523
+ // Index by type
524
+ this.ensureIndexSet(this.apisByType, api.type).add(api.id);
525
+ // Index by status
526
+ const status = api.metadata?.status || 'active';
527
+ this.ensureIndexSet(this.apisByStatus, status).add(api.id);
528
+ // Index by tags
529
+ const tags = api.metadata?.tags || [];
530
+ for (const tag of tags) {
531
+ this.ensureIndexSet(this.apisByTag, tag).add(api.id);
532
+ }
533
+ }
534
+ /**
535
+ * Remove API from auxiliary indices when unregistered
536
+ *
537
+ * @param api - API entry to remove from indices
538
+ * @private
539
+ * @internal
540
+ */
541
+ removeFromIndices(api) {
542
+ // Remove from type index
543
+ this.removeFromIndexSet(this.apisByType, api.type, api.id);
544
+ // Remove from status index
545
+ const status = api.metadata?.status || 'active';
546
+ this.removeFromIndexSet(this.apisByStatus, status, api.id);
547
+ // Remove from tag indices
548
+ const tags = api.metadata?.tags || [];
549
+ for (const tag of tags) {
550
+ this.removeFromIndexSet(this.apisByTag, tag, api.id);
551
+ }
552
+ }
553
+ /**
554
+ * Helper to ensure an index set exists and return it
555
+ *
556
+ * @param map - Index map
557
+ * @param key - Index key
558
+ * @returns The Set for this key (created if needed)
559
+ * @private
560
+ * @internal
561
+ */
562
+ ensureIndexSet(map, key) {
563
+ let set = map.get(key);
564
+ if (!set) {
565
+ set = new Set();
566
+ map.set(key, set);
567
+ }
568
+ return set;
569
+ }
570
+ /**
571
+ * Helper to remove an ID from an index set and clean up empty sets
572
+ *
573
+ * @param map - Index map
574
+ * @param key - Index key
575
+ * @param id - API ID to remove
576
+ * @private
577
+ * @internal
578
+ */
579
+ removeFromIndexSet(map, key, id) {
580
+ const set = map.get(key);
581
+ if (set) {
582
+ set.delete(id);
583
+ // Clean up empty sets to avoid memory leaks
584
+ if (set.size === 0) {
585
+ map.delete(key);
586
+ }
587
+ }
588
+ }
589
+ /**
590
+ * Check if running in production environment
591
+ *
592
+ * @returns true if NODE_ENV is 'production'
593
+ * @private
594
+ * @internal
595
+ */
596
+ isProductionEnvironment() {
597
+ return process.env.NODE_ENV === 'production';
598
+ }
599
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api-registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-registry.test.d.ts","sourceRoot":"","sources":["../src/api-registry.test.ts"],"names":[],"mappings":""}