@objectstack/runtime 3.0.11 → 3.1.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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +59 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/dispatcher-plugin.ts +18 -0
- package/src/http-dispatcher.test.ts +120 -0
- package/src/http-dispatcher.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "ObjectStack Core Runtime & Query Engine",
|
|
6
6
|
"type": "module",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"zod": "^4.3.6",
|
|
18
|
-
"@objectstack/core": "3.
|
|
19
|
-
"@objectstack/rest": "3.
|
|
20
|
-
"@objectstack/spec": "3.
|
|
21
|
-
"@objectstack/types": "3.
|
|
18
|
+
"@objectstack/core": "3.1.1",
|
|
19
|
+
"@objectstack/rest": "3.1.1",
|
|
20
|
+
"@objectstack/spec": "3.1.1",
|
|
21
|
+
"@objectstack/types": "3.1.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"typescript": "^5.0.0",
|
package/src/dispatcher-plugin.ts
CHANGED
|
@@ -199,6 +199,24 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
server.post(`${prefix}/packages/:id/publish`, async (req: any, res: any) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, 'POST', req.body, {}, { request: req });
|
|
205
|
+
sendResult(result, res);
|
|
206
|
+
} catch (err: any) {
|
|
207
|
+
errorResponse(err, res);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
server.post(`${prefix}/packages/:id/revert`, async (req: any, res: any) => {
|
|
212
|
+
try {
|
|
213
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, 'POST', req.body, {}, { request: req });
|
|
214
|
+
sendResult(result, res);
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
errorResponse(err, res);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
202
220
|
// ── Storage ─────────────────────────────────────────────────
|
|
203
221
|
server.post(`${prefix}/storage/upload`, async (req: any, res: any) => {
|
|
204
222
|
try {
|
|
@@ -469,4 +469,124 @@ describe('HttpDispatcher', () => {
|
|
|
469
469
|
).rejects.toThrow('Disk full');
|
|
470
470
|
});
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
// ═══════════════════════════════════════════════════════════════
|
|
474
|
+
// Package Publish / Revert Endpoints
|
|
475
|
+
// ═══════════════════════════════════════════════════════════════
|
|
476
|
+
|
|
477
|
+
describe('Package publish/revert endpoints', () => {
|
|
478
|
+
it('should handle POST /packages/:id/publish via metadata service', async () => {
|
|
479
|
+
const mockMetadata = {
|
|
480
|
+
publishPackage: vi.fn().mockResolvedValue({
|
|
481
|
+
success: true,
|
|
482
|
+
packageId: 'com.acme.crm',
|
|
483
|
+
version: 2,
|
|
484
|
+
publishedAt: '2025-06-01T00:00:00Z',
|
|
485
|
+
itemsPublished: 3,
|
|
486
|
+
}),
|
|
487
|
+
};
|
|
488
|
+
const mockRegistry = {
|
|
489
|
+
getAllPackages: vi.fn().mockReturnValue([]),
|
|
490
|
+
enablePackage: vi.fn(),
|
|
491
|
+
disablePackage: vi.fn(),
|
|
492
|
+
};
|
|
493
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
494
|
+
if (name === 'metadata') return Promise.resolve(mockMetadata);
|
|
495
|
+
if (name === 'objectql') return Promise.resolve({ registry: mockRegistry });
|
|
496
|
+
return null;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const result = await dispatcher.handlePackages('/com.acme.crm/publish', 'POST', { publishedBy: 'admin' }, {}, { request: {} });
|
|
500
|
+
expect(result.handled).toBe(true);
|
|
501
|
+
expect(result.response?.status).toBe(200);
|
|
502
|
+
expect(mockMetadata.publishPackage).toHaveBeenCalledWith('com.acme.crm', { publishedBy: 'admin' });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should handle POST /packages/:id/revert via metadata service', async () => {
|
|
506
|
+
const mockMetadata = {
|
|
507
|
+
revertPackage: vi.fn().mockResolvedValue(undefined),
|
|
508
|
+
};
|
|
509
|
+
const mockRegistry = {
|
|
510
|
+
getAllPackages: vi.fn().mockReturnValue([]),
|
|
511
|
+
enablePackage: vi.fn(),
|
|
512
|
+
disablePackage: vi.fn(),
|
|
513
|
+
};
|
|
514
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
515
|
+
if (name === 'metadata') return Promise.resolve(mockMetadata);
|
|
516
|
+
if (name === 'objectql') return Promise.resolve({ registry: mockRegistry });
|
|
517
|
+
return null;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const result = await dispatcher.handlePackages('/com.acme.crm/revert', 'POST', {}, {}, { request: {} });
|
|
521
|
+
expect(result.handled).toBe(true);
|
|
522
|
+
expect(result.response?.status).toBe(200);
|
|
523
|
+
expect(mockMetadata.revertPackage).toHaveBeenCalledWith('com.acme.crm');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should fallback to broker for publish when metadata service unavailable', async () => {
|
|
527
|
+
const mockRegistry = {
|
|
528
|
+
getAllPackages: vi.fn().mockReturnValue([]),
|
|
529
|
+
};
|
|
530
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
531
|
+
if (name === 'metadata') return Promise.resolve(null);
|
|
532
|
+
if (name === 'objectql') return Promise.resolve({ registry: mockRegistry });
|
|
533
|
+
return null;
|
|
534
|
+
});
|
|
535
|
+
mockBroker.call.mockResolvedValue({ success: true, packageId: 'crm', version: 1, publishedAt: '2025-01-01T00:00:00Z', itemsPublished: 2 });
|
|
536
|
+
|
|
537
|
+
const result = await dispatcher.handlePackages('/crm/publish', 'POST', {}, {}, { request: {} });
|
|
538
|
+
expect(result.handled).toBe(true);
|
|
539
|
+
expect(mockBroker.call).toHaveBeenCalledWith('metadata.publishPackage', { packageId: 'crm' }, { request: {} });
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════
|
|
544
|
+
// Metadata getPublished Endpoint
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
describe('Metadata getPublished endpoint', () => {
|
|
548
|
+
it('should handle GET /metadata/:type/:name/published via metadata service', async () => {
|
|
549
|
+
const mockMetadata = {
|
|
550
|
+
getPublished: vi.fn().mockResolvedValue({ name: 'account', label: 'Account' }),
|
|
551
|
+
};
|
|
552
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
553
|
+
if (name === 'metadata') return Promise.resolve(mockMetadata);
|
|
554
|
+
return null;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const result = await dispatcher.handleMetadata('/object/account/published', { request: {} }, 'GET');
|
|
558
|
+
expect(result.handled).toBe(true);
|
|
559
|
+
expect(result.response?.status).toBe(200);
|
|
560
|
+
expect(result.response?.body?.data).toEqual({ name: 'account', label: 'Account' });
|
|
561
|
+
expect(mockMetadata.getPublished).toHaveBeenCalledWith('object', 'account');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should return 404 when published item not found', async () => {
|
|
565
|
+
const mockMetadata = {
|
|
566
|
+
getPublished: vi.fn().mockResolvedValue(undefined),
|
|
567
|
+
};
|
|
568
|
+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
|
|
569
|
+
if (name === 'metadata') return Promise.resolve(mockMetadata);
|
|
570
|
+
return null;
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const result = await dispatcher.handleMetadata('/object/nonexistent/published', { request: {} }, 'GET');
|
|
574
|
+
expect(result.handled).toBe(true);
|
|
575
|
+
expect(result.response?.status).toBe(404);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should fallback to broker for getPublished when metadata service unavailable', async () => {
|
|
579
|
+
(kernel as any).getService = vi.fn().mockResolvedValue(null);
|
|
580
|
+
mockBroker.call.mockResolvedValue({ name: 'account', fields: ['name'] });
|
|
581
|
+
|
|
582
|
+
const result = await dispatcher.handleMetadata('/object/account/published', { request: {} }, 'GET');
|
|
583
|
+
expect(result.handled).toBe(true);
|
|
584
|
+
expect(result.response?.status).toBe(200);
|
|
585
|
+
expect(mockBroker.call).toHaveBeenCalledWith(
|
|
586
|
+
'metadata.getPublished',
|
|
587
|
+
{ type: 'object', name: 'account' },
|
|
588
|
+
{ request: {} }
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
472
592
|
});
|
package/src/http-dispatcher.ts
CHANGED
|
@@ -217,6 +217,24 @@ export class HttpDispatcher {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// GET /metadata/:type/:name/published → get published version
|
|
221
|
+
if (parts.length === 3 && parts[2] === 'published' && (!method || method === 'GET')) {
|
|
222
|
+
const [type, name] = parts;
|
|
223
|
+
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
224
|
+
if (metadataService && typeof (metadataService as any).getPublished === 'function') {
|
|
225
|
+
const data = await (metadataService as any).getPublished(type, name);
|
|
226
|
+
if (data === undefined) return { handled: true, response: this.error('Not found', 404) };
|
|
227
|
+
return { handled: true, response: this.success(data) };
|
|
228
|
+
}
|
|
229
|
+
// Broker fallback
|
|
230
|
+
try {
|
|
231
|
+
const data = await broker.call('metadata.getPublished', { type, name }, { request: context.request });
|
|
232
|
+
return { handled: true, response: this.success(data) };
|
|
233
|
+
} catch (e: any) {
|
|
234
|
+
return { handled: true, response: this.error(e.message, 404) };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
220
238
|
// /metadata/:type/:name
|
|
221
239
|
if (parts.length === 2) {
|
|
222
240
|
const [type, name] = parts;
|
|
@@ -467,6 +485,8 @@ export class HttpDispatcher {
|
|
|
467
485
|
* - DELETE /packages/:id → uninstall a package
|
|
468
486
|
* - PATCH /packages/:id/enable → enable a package
|
|
469
487
|
* - PATCH /packages/:id/disable → disable a package
|
|
488
|
+
* - POST /packages/:id/publish → publish a package (metadata snapshot)
|
|
489
|
+
* - POST /packages/:id/revert → revert a package to last published state
|
|
470
490
|
*
|
|
471
491
|
* Uses ObjectQL SchemaRegistry directly (via the 'objectql' service)
|
|
472
492
|
* with broker fallback for backward compatibility.
|
|
@@ -525,6 +545,38 @@ export class HttpDispatcher {
|
|
|
525
545
|
return { handled: true, response: this.success(pkg) };
|
|
526
546
|
}
|
|
527
547
|
|
|
548
|
+
// POST /packages/:id/publish → publish package metadata
|
|
549
|
+
if (parts.length === 2 && parts[1] === 'publish' && m === 'POST') {
|
|
550
|
+
const id = decodeURIComponent(parts[0]);
|
|
551
|
+
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
552
|
+
if (metadataService && typeof (metadataService as any).publishPackage === 'function') {
|
|
553
|
+
const result = await (metadataService as any).publishPackage(id, body || {});
|
|
554
|
+
return { handled: true, response: this.success(result) };
|
|
555
|
+
}
|
|
556
|
+
// Broker fallback
|
|
557
|
+
if (this.kernel.broker) {
|
|
558
|
+
const result = await this.kernel.broker.call('metadata.publishPackage', { packageId: id, ...body }, { request: context.request });
|
|
559
|
+
return { handled: true, response: this.success(result) };
|
|
560
|
+
}
|
|
561
|
+
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// POST /packages/:id/revert → revert package to last published state
|
|
565
|
+
if (parts.length === 2 && parts[1] === 'revert' && m === 'POST') {
|
|
566
|
+
const id = decodeURIComponent(parts[0]);
|
|
567
|
+
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
568
|
+
if (metadataService && typeof (metadataService as any).revertPackage === 'function') {
|
|
569
|
+
await (metadataService as any).revertPackage(id);
|
|
570
|
+
return { handled: true, response: this.success({ success: true }) };
|
|
571
|
+
}
|
|
572
|
+
// Broker fallback
|
|
573
|
+
if (this.kernel.broker) {
|
|
574
|
+
await this.kernel.broker.call('metadata.revertPackage', { packageId: id }, { request: context.request });
|
|
575
|
+
return { handled: true, response: this.success({ success: true }) };
|
|
576
|
+
}
|
|
577
|
+
return { handled: true, response: this.error('Metadata service not available', 503) };
|
|
578
|
+
}
|
|
579
|
+
|
|
528
580
|
// GET /packages/:id → get package
|
|
529
581
|
if (parts.length === 1 && m === 'GET') {
|
|
530
582
|
const id = decodeURIComponent(parts[0]);
|