@objectstack/runtime 3.0.11 → 3.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/runtime",
3
- "version": "3.0.11",
3
+ "version": "3.1.0",
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.0.11",
19
- "@objectstack/rest": "3.0.11",
20
- "@objectstack/spec": "3.0.11",
21
- "@objectstack/types": "3.0.11"
18
+ "@objectstack/core": "3.1.0",
19
+ "@objectstack/rest": "3.1.0",
20
+ "@objectstack/spec": "3.1.0",
21
+ "@objectstack/types": "3.1.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^5.0.0",
@@ -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
  });
@@ -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]);