@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,391 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ObjectKernel } from './kernel';
3
+ import { createApiRegistryPlugin } from './api-registry-plugin';
4
+ import { ApiRegistry } from './api-registry';
5
+ import type { Plugin } from './types';
6
+ import type { ApiRegistryEntryInput } from '@objectstack/spec/api';
7
+
8
+ describe('API Registry Plugin', () => {
9
+ let kernel: ObjectKernel;
10
+
11
+ beforeEach(() => {
12
+ kernel = new ObjectKernel();
13
+ });
14
+
15
+ describe('Plugin Registration', () => {
16
+ it('should register API Registry as a service', async () => {
17
+ kernel.use(createApiRegistryPlugin());
18
+ await kernel.bootstrap();
19
+
20
+ const registry = kernel.getService<ApiRegistry>('api-registry');
21
+ expect(registry).toBeDefined();
22
+ expect(registry).toBeInstanceOf(ApiRegistry);
23
+
24
+ await kernel.shutdown();
25
+ });
26
+
27
+ it('should register with custom conflict resolution', async () => {
28
+ kernel.use(createApiRegistryPlugin({
29
+ conflictResolution: 'priority',
30
+ version: '2.0.0',
31
+ }));
32
+ await kernel.bootstrap();
33
+
34
+ const registry = kernel.getService<ApiRegistry>('api-registry');
35
+ const snapshot = registry.getRegistry();
36
+ expect(snapshot.conflictResolution).toBe('priority');
37
+ expect(snapshot.version).toBe('2.0.0');
38
+
39
+ await kernel.shutdown();
40
+ });
41
+ });
42
+
43
+ describe('Integration with Plugins', () => {
44
+ it('should allow plugins to register APIs', async () => {
45
+ kernel.use(createApiRegistryPlugin());
46
+
47
+ const testPlugin: Plugin = {
48
+ name: 'test-plugin',
49
+ init: async (ctx) => {
50
+ const registry = ctx.getService<ApiRegistry>('api-registry');
51
+
52
+ const api: ApiRegistryEntryInput = {
53
+ id: 'test_api',
54
+ name: 'Test API',
55
+ type: 'rest',
56
+ version: 'v1',
57
+ basePath: '/api/test',
58
+ endpoints: [
59
+ {
60
+ id: 'get_test',
61
+ method: 'GET',
62
+ path: '/api/test/hello',
63
+ summary: 'Test endpoint',
64
+ responses: [
65
+ {
66
+ statusCode: 200,
67
+ description: 'Success',
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ };
73
+
74
+ registry.registerApi(api);
75
+ },
76
+ };
77
+
78
+ kernel.use(testPlugin);
79
+ await kernel.bootstrap();
80
+
81
+ const registry = kernel.getService<ApiRegistry>('api-registry');
82
+ const api = registry.getApi('test_api');
83
+ expect(api).toBeDefined();
84
+ expect(api?.name).toBe('Test API');
85
+ expect(api?.endpoints.length).toBe(1);
86
+
87
+ await kernel.shutdown();
88
+ });
89
+
90
+ it('should allow multiple plugins to register APIs', async () => {
91
+ kernel.use(createApiRegistryPlugin());
92
+
93
+ const plugin1: Plugin = {
94
+ name: 'plugin-1',
95
+ init: async (ctx) => {
96
+ const registry = ctx.getService<ApiRegistry>('api-registry');
97
+ registry.registerApi({
98
+ id: 'api1',
99
+ name: 'API 1',
100
+ type: 'rest',
101
+ version: 'v1',
102
+ basePath: '/api/plugin1',
103
+ endpoints: [
104
+ {
105
+ id: 'endpoint1',
106
+ method: 'GET',
107
+ path: '/api/plugin1/data',
108
+ responses: [],
109
+ },
110
+ ],
111
+ });
112
+ },
113
+ };
114
+
115
+ const plugin2: Plugin = {
116
+ name: 'plugin-2',
117
+ init: async (ctx) => {
118
+ const registry = ctx.getService<ApiRegistry>('api-registry');
119
+ registry.registerApi({
120
+ id: 'api2',
121
+ name: 'API 2',
122
+ type: 'graphql',
123
+ version: 'v1',
124
+ basePath: '/graphql',
125
+ endpoints: [
126
+ {
127
+ id: 'query',
128
+ path: '/graphql',
129
+ responses: [],
130
+ },
131
+ ],
132
+ });
133
+ },
134
+ };
135
+
136
+ kernel.use(plugin1);
137
+ kernel.use(plugin2);
138
+ await kernel.bootstrap();
139
+
140
+ const registry = kernel.getService<ApiRegistry>('api-registry');
141
+ const stats = registry.getStats();
142
+ expect(stats.totalApis).toBe(2);
143
+ expect(stats.apisByType.rest).toBe(1);
144
+ expect(stats.apisByType.graphql).toBe(1);
145
+
146
+ await kernel.shutdown();
147
+ });
148
+
149
+ it('should support API discovery across plugins', async () => {
150
+ kernel.use(createApiRegistryPlugin());
151
+
152
+ const dataPlugin: Plugin = {
153
+ name: 'data-plugin',
154
+ init: async (ctx) => {
155
+ const registry = ctx.getService<ApiRegistry>('api-registry');
156
+ registry.registerApi({
157
+ id: 'customer_api',
158
+ name: 'Customer API',
159
+ type: 'rest',
160
+ version: 'v1',
161
+ basePath: '/api/v1/customers',
162
+ endpoints: [],
163
+ metadata: {
164
+ status: 'active',
165
+ tags: ['crm', 'data'],
166
+ },
167
+ });
168
+
169
+ registry.registerApi({
170
+ id: 'product_api',
171
+ name: 'Product API',
172
+ type: 'rest',
173
+ version: 'v1',
174
+ basePath: '/api/v1/products',
175
+ endpoints: [],
176
+ metadata: {
177
+ status: 'active',
178
+ tags: ['inventory', 'data'],
179
+ },
180
+ });
181
+ },
182
+ };
183
+
184
+ const analyticsPlugin: Plugin = {
185
+ name: 'analytics-plugin',
186
+ init: async (ctx) => {
187
+ const registry = ctx.getService<ApiRegistry>('api-registry');
188
+ registry.registerApi({
189
+ id: 'analytics_api',
190
+ name: 'Analytics API',
191
+ type: 'rest',
192
+ version: 'v1',
193
+ basePath: '/api/v1/analytics',
194
+ endpoints: [],
195
+ metadata: {
196
+ status: 'beta',
197
+ tags: ['analytics', 'reporting'],
198
+ },
199
+ });
200
+ },
201
+ };
202
+
203
+ kernel.use(dataPlugin);
204
+ kernel.use(analyticsPlugin);
205
+ await kernel.bootstrap();
206
+
207
+ const registry = kernel.getService<ApiRegistry>('api-registry');
208
+
209
+ // Find all data APIs
210
+ const dataApis = registry.findApis({ tags: ['data'] });
211
+ expect(dataApis.total).toBe(2);
212
+
213
+ // Find active APIs
214
+ const activeApis = registry.findApis({ status: 'active' });
215
+ expect(activeApis.total).toBe(2);
216
+
217
+ // Find CRM APIs
218
+ const crmApis = registry.findApis({ tags: ['crm'] });
219
+ expect(crmApis.total).toBe(1);
220
+ expect(crmApis.apis[0].id).toBe('customer_api');
221
+
222
+ await kernel.shutdown();
223
+ });
224
+
225
+ it('should handle route conflicts based on strategy', async () => {
226
+ kernel.use(createApiRegistryPlugin({
227
+ conflictResolution: 'priority',
228
+ }));
229
+
230
+ const corePlugin: Plugin = {
231
+ name: 'core-plugin',
232
+ init: async (ctx) => {
233
+ const registry = ctx.getService<ApiRegistry>('api-registry');
234
+ registry.registerApi({
235
+ id: 'core_api',
236
+ name: 'Core API',
237
+ type: 'rest',
238
+ version: 'v1',
239
+ basePath: '/api',
240
+ endpoints: [
241
+ {
242
+ id: 'core_endpoint',
243
+ method: 'GET',
244
+ path: '/api/data/:object',
245
+ priority: 900, // High priority
246
+ summary: 'Core data endpoint',
247
+ responses: [],
248
+ },
249
+ ],
250
+ });
251
+ },
252
+ };
253
+
254
+ const pluginOverride: Plugin = {
255
+ name: 'plugin-override',
256
+ init: async (ctx) => {
257
+ const registry = ctx.getService<ApiRegistry>('api-registry');
258
+ registry.registerApi({
259
+ id: 'plugin_api',
260
+ name: 'Plugin API',
261
+ type: 'rest',
262
+ version: 'v1',
263
+ basePath: '/api',
264
+ endpoints: [
265
+ {
266
+ id: 'plugin_endpoint',
267
+ method: 'GET',
268
+ path: '/api/data/:object',
269
+ priority: 300, // Lower priority
270
+ summary: 'Plugin data endpoint',
271
+ responses: [],
272
+ },
273
+ ],
274
+ });
275
+ },
276
+ };
277
+
278
+ kernel.use(corePlugin);
279
+ kernel.use(pluginOverride);
280
+ await kernel.bootstrap();
281
+
282
+ const registry = kernel.getService<ApiRegistry>('api-registry');
283
+ const result = registry.findEndpointByRoute('GET', '/api/data/:object');
284
+
285
+ // Core API should win due to higher priority
286
+ expect(result?.api.id).toBe('core_api');
287
+ expect(result?.endpoint.id).toBe('core_endpoint');
288
+
289
+ await kernel.shutdown();
290
+ });
291
+
292
+ it('should support cleanup on plugin unload', async () => {
293
+ kernel.use(createApiRegistryPlugin());
294
+
295
+ const dynamicPlugin: Plugin = {
296
+ name: 'dynamic-plugin',
297
+ init: async (ctx) => {
298
+ const registry = ctx.getService<ApiRegistry>('api-registry');
299
+ registry.registerApi({
300
+ id: 'dynamic_api',
301
+ name: 'Dynamic API',
302
+ type: 'rest',
303
+ version: 'v1',
304
+ basePath: '/api/dynamic',
305
+ endpoints: [
306
+ {
307
+ id: 'test',
308
+ method: 'GET',
309
+ path: '/api/dynamic/test',
310
+ responses: [],
311
+ },
312
+ ],
313
+ });
314
+ },
315
+ destroy: async () => {
316
+ // In a real scenario, this would use ctx to access registry
317
+ // For now, we'll test the registry's unregister capability
318
+ },
319
+ };
320
+
321
+ kernel.use(dynamicPlugin);
322
+ await kernel.bootstrap();
323
+
324
+ const registry = kernel.getService<ApiRegistry>('api-registry');
325
+ expect(registry.getApi('dynamic_api')).toBeDefined();
326
+
327
+ // Unregister the API
328
+ registry.unregisterApi('dynamic_api');
329
+ expect(registry.getApi('dynamic_api')).toBeUndefined();
330
+
331
+ await kernel.shutdown();
332
+ });
333
+ });
334
+
335
+ describe('API Registry Lifecycle', () => {
336
+ it('should be available during plugin start phase', async () => {
337
+ kernel.use(createApiRegistryPlugin());
338
+
339
+ let registryAvailable = false;
340
+
341
+ const testPlugin: Plugin = {
342
+ name: 'test-plugin',
343
+ init: async () => {
344
+ // Init phase
345
+ },
346
+ start: async (ctx) => {
347
+ // Start phase - registry should be available
348
+ const registry = ctx.getService<ApiRegistry>('api-registry');
349
+ registryAvailable = registry !== undefined;
350
+ },
351
+ };
352
+
353
+ kernel.use(testPlugin);
354
+ await kernel.bootstrap();
355
+
356
+ expect(registryAvailable).toBe(true);
357
+
358
+ await kernel.shutdown();
359
+ });
360
+
361
+ it('should provide consistent registry across all plugins', async () => {
362
+ kernel.use(createApiRegistryPlugin());
363
+
364
+ let registry1: ApiRegistry | undefined;
365
+ let registry2: ApiRegistry | undefined;
366
+
367
+ const plugin1: Plugin = {
368
+ name: 'plugin-1',
369
+ init: async (ctx) => {
370
+ registry1 = ctx.getService<ApiRegistry>('api-registry');
371
+ },
372
+ };
373
+
374
+ const plugin2: Plugin = {
375
+ name: 'plugin-2',
376
+ init: async (ctx) => {
377
+ registry2 = ctx.getService<ApiRegistry>('api-registry');
378
+ },
379
+ };
380
+
381
+ kernel.use(plugin1);
382
+ kernel.use(plugin2);
383
+ await kernel.bootstrap();
384
+
385
+ // Same registry instance should be shared
386
+ expect(registry1).toBe(registry2);
387
+
388
+ await kernel.shutdown();
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,86 @@
1
+ import type { Plugin, PluginContext } from './types.js';
2
+ import { ApiRegistry } from './api-registry.js';
3
+ import type { ConflictResolutionStrategy } from '@objectstack/spec/api';
4
+
5
+ /**
6
+ * API Registry Plugin Configuration
7
+ */
8
+ export interface ApiRegistryPluginConfig {
9
+ /**
10
+ * Conflict resolution strategy for route conflicts
11
+ * @default 'error'
12
+ */
13
+ conflictResolution?: ConflictResolutionStrategy;
14
+
15
+ /**
16
+ * Registry version
17
+ * @default '1.0.0'
18
+ */
19
+ version?: string;
20
+ }
21
+
22
+ /**
23
+ * API Registry Plugin
24
+ *
25
+ * Registers the API Registry service in the kernel, making it available
26
+ * to all plugins for endpoint registration and discovery.
27
+ *
28
+ * **Usage:**
29
+ * ```typescript
30
+ * const kernel = new ObjectKernel();
31
+ *
32
+ * // Register API Registry Plugin
33
+ * kernel.use(createApiRegistryPlugin({ conflictResolution: 'priority' }));
34
+ *
35
+ * // In other plugins, access the API Registry
36
+ * const plugin: Plugin = {
37
+ * name: 'my-plugin',
38
+ * init: async (ctx) => {
39
+ * const registry = ctx.getService<ApiRegistry>('api-registry');
40
+ *
41
+ * // Register plugin APIs
42
+ * registry.registerApi({
43
+ * id: 'my_plugin_api',
44
+ * name: 'My Plugin API',
45
+ * type: 'rest',
46
+ * version: 'v1',
47
+ * basePath: '/api/v1/my-plugin',
48
+ * endpoints: [...]
49
+ * });
50
+ * }
51
+ * };
52
+ * ```
53
+ *
54
+ * @param config - Plugin configuration
55
+ * @returns Plugin instance
56
+ */
57
+ export function createApiRegistryPlugin(
58
+ config: ApiRegistryPluginConfig = {}
59
+ ): Plugin {
60
+ const {
61
+ conflictResolution = 'error',
62
+ version = '1.0.0',
63
+ } = config;
64
+
65
+ return {
66
+ name: 'com.objectstack.core.api-registry',
67
+ version: '1.0.0',
68
+
69
+ init: async (ctx: PluginContext) => {
70
+ // Create API Registry instance
71
+ const registry = new ApiRegistry(
72
+ ctx.logger,
73
+ conflictResolution,
74
+ version
75
+ );
76
+
77
+ // Register as a service
78
+ ctx.registerService('api-registry', registry);
79
+
80
+ ctx.logger.info('API Registry plugin initialized', {
81
+ conflictResolution,
82
+ version,
83
+ });
84
+ },
85
+ };
86
+ }