@objectstack/client 1.0.10 → 1.0.12

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.
@@ -33,23 +33,23 @@ describe('ObjectStackClient (with Hono Server)', () => {
33
33
  const ql = kernel.getService<any>('objectql'); // Use 'objectql' service name for clarity
34
34
  if (method === 'create') {
35
35
  const res = await ql.insert(params.object, params.data);
36
- // Ensure we return the full object including input data + ID
37
- return { ...params.data, ...res };
36
+ const record = { ...params.data, ...res };
37
+ return { object: params.object, id: record.id || record._id, record };
38
38
  }
39
39
  // Params from HttpDispatcher: { object, id, ...query }
40
40
  if (method === 'get') {
41
- // Ensure we search by 'id' explicitly for InMemoryDriver
42
- return ql.findOne(params.object, { where: { id: params.id } });
41
+ const record = await ql.findOne(params.object, { where: { id: params.id } });
42
+ return record ? { object: params.object, id: params.id, record } : null;
43
43
  }
44
44
  // Params from HttpDispatcher: { object, filters }
45
45
  if (method === 'query') {
46
- // HttpDispatcher passes filters as simple object (map)
47
- // ObjectQL expects { filter, select, sort, ... }
48
- const data = await ql.find(params.object, { filter: params.filters });
49
- // HttpDispatcher expects { data, count }
50
- return { data, count: data.length };
46
+ const records = await ql.find(params.object, { filter: params.filters });
47
+ return { object: params.object, records, total: records.length };
48
+ }
49
+ if (method === 'find') {
50
+ const records = await ql.find(params.object, { filter: params.filters });
51
+ return { object: params.object, records, total: records.length };
51
52
  }
52
- if (method === 'find') return ql.find(params.object, { filter: params.filters });
53
53
  }
54
54
 
55
55
  if (service === 'metadata') {
@@ -101,44 +101,49 @@ describe('ObjectStackClient (with Hono Server)', () => {
101
101
  if (kernel) await kernel.shutdown();
102
102
  });
103
103
 
104
- it('should connect to hono server', async () => {
104
+ it('should connect to hono server and discover endpoints', async () => {
105
105
  const client = new ObjectStackClient({ baseUrl });
106
106
  await client.connect();
107
107
 
108
- // Client creates URL like ${baseUrl}/api/v1
109
- expect(client).toBeDefined();
108
+ // Client should have populated discovery info
109
+ expect(client['discoveryInfo']).toBeDefined();
110
+
111
+ // Verify endpoints from valid discovery response
112
+ // Standard: /api/v1/data, /api/v1/meta, etc.
113
+ const endpoints = client['discoveryInfo']!.routes;
114
+ expect(endpoints.data).toContain('/api/v1/data');
115
+ expect(endpoints.metadata).toContain('/api/v1/meta');
116
+ expect(endpoints.auth).toContain('/api/v1/auth');
110
117
  });
111
118
 
112
119
  it('should create and retrieve data via hono', async () => {
113
120
  const client = new ObjectStackClient({ baseUrl });
114
121
  await client.connect();
115
122
 
116
- // Create
123
+ // Create — Spec: CreateDataResponse = { object, id, record }
117
124
  const createdResponse = await client.data.create('customer', {
118
125
  name: 'Hono User',
119
126
  email: 'hono@example.com'
120
127
  });
121
128
 
122
- expect(createdResponse.success).toBe(true);
123
- expect(createdResponse.data.name).toBe('Hono User');
124
- expect(createdResponse.data.id).toBeDefined();
125
-
126
- // Retrieve
127
- const retrievedResponse = await client.data.get('customer', createdResponse.data.id);
128
- expect(retrievedResponse.success).toBe(true);
129
- expect(retrievedResponse.data.name).toBe('Hono User');
129
+ expect(createdResponse.record.name).toBe('Hono User');
130
+ expect(createdResponse.id).toBeDefined();
131
+
132
+ // Retrieve — Spec: GetDataResponse = { object, id, record }
133
+ const retrievedResponse = await client.data.get('customer', createdResponse.id);
134
+ expect(retrievedResponse.record.name).toBe('Hono User');
130
135
  });
131
136
 
132
137
  it('should find data via hono', async () => {
133
138
  const client = new ObjectStackClient({ baseUrl });
134
139
  await client.connect();
135
140
 
141
+ // Spec: FindDataResponse = { object, records, total? }
136
142
  const resultsResponse = await client.data.find('customer', {
137
143
  where: { name: 'Hono User' }
138
144
  });
139
145
 
140
- expect(resultsResponse.success).toBe(true);
141
- expect(resultsResponse.data.length).toBeGreaterThan(0);
142
- expect(resultsResponse.data[0].name).toBe('Hono User');
146
+ expect(resultsResponse.records.length).toBeGreaterThan(0);
147
+ expect(resultsResponse.records[0].name).toBe('Hono User');
143
148
  });
144
149
  });
@@ -39,17 +39,22 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
39
39
  const ql = kernel.getService<any>('objectql');
40
40
  if (method === 'create') {
41
41
  const res = await ql.insert(params.object, params.data);
42
- return { ...params.data, ...res };
42
+ const record = { ...params.data, ...res };
43
+ return { object: params.object, id: record.id || record._id, record };
43
44
  }
44
45
  if (method === 'get') {
45
46
  // Ensure we search by 'id' explicitly for InMemoryDriver
46
- return ql.findOne(params.object, { where: { id: params.id } });
47
+ const record = await ql.findOne(params.object, { where: { id: params.id } });
48
+ return record ? { object: params.object, id: params.id, record } : null;
47
49
  }
48
50
  if (method === 'query') {
49
- const data = await ql.find(params.object, { filter: params.filters });
50
- return { data, count: data.length };
51
+ const records = await ql.find(params.object, { filter: params.filters });
52
+ return { object: params.object, records, total: records.length };
53
+ }
54
+ if (method === 'find') {
55
+ const records = await ql.find(params.object, { filter: params.filters });
56
+ return { object: params.object, records, total: records.length };
51
57
  }
52
- if (method === 'find') return ql.find(params.object, { filter: params.filters });
53
58
  }
54
59
 
55
60
  if (service === 'metadata') {
@@ -91,7 +96,7 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
91
96
  version: '1.0.0',
92
97
  routes: {
93
98
  data: '/api/v1/data',
94
- metadata: '/api/v1/metadata',
99
+ metadata: '/api/v1/meta',
95
100
  auth: '/api/v1/auth'
96
101
  },
97
102
  capabilities: ['data', 'metadata'],
@@ -99,7 +104,7 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
99
104
  });
100
105
  }),
101
106
 
102
- http.get('http://127.0.0.1/api/v1/metadata/object/:name', async ({ params }) => {
107
+ http.get('http://127.0.0.1/api/v1/meta/object/:name', async ({ params }) => {
103
108
  try {
104
109
  const res = await (kernel as any).broker.call('metadata.getObject', { objectName: params.name });
105
110
  return HttpResponse.json({ success: true, data: res });
@@ -162,37 +167,44 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
162
167
  const client = new ObjectStackClient({ baseUrl: BASE_URL });
163
168
  await client.connect();
164
169
 
165
- const customerRes = await client.meta.getItem('object', 'customer');
166
- expect(customerRes.success).toBe(true);
167
- expect(customerRes.data.name).toBe('customer');
170
+ // Spec: GetMetaItemResponse = { type, name, item }
171
+ const customerRes: any = await client.meta.getItem('object', 'customer');
172
+ expect(customerRes).toBeDefined();
173
+ // After unwrapResponse, we get the protocol-level response
174
+ // The manual handler wraps as { success, data: schema }, so unwrap yields the schema
175
+ const schema = customerRes.item || customerRes;
176
+ expect(schema.name).toBe('customer');
168
177
  });
169
178
 
170
179
  it('should find data records', async () => {
171
180
  const client = new ObjectStackClient({ baseUrl: BASE_URL });
172
181
  await client.connect();
173
182
 
183
+ // Spec: FindDataResponse = { object, records, total? }
174
184
  const resultsRes = await client.data.find('customer');
175
- expect(resultsRes.success).toBe(true);
176
- expect(resultsRes.data.length).toBe(2);
177
- expect(resultsRes.data[0].name).toBe('Alice');
185
+ expect(resultsRes.records).toBeDefined();
186
+ expect(resultsRes.records.length).toBe(2);
187
+ expect(resultsRes.records[0].name).toBe('Alice');
178
188
  });
179
189
 
180
190
  it('should get single record', async () => {
181
191
  const client = new ObjectStackClient({ baseUrl: BASE_URL });
182
192
  await client.connect();
183
193
 
194
+ // Spec: GetDataResponse = { object, id, record }
184
195
  const recordRes = await client.data.get('customer', '101');
185
- expect(recordRes.success).toBe(true);
186
- expect(recordRes.data.name).toBe('Alice');
196
+ expect(recordRes.record).toBeDefined();
197
+ expect(recordRes.record.name).toBe('Alice');
187
198
  });
188
199
 
189
200
  it('should create record', async () => {
190
201
  const client = new ObjectStackClient({ baseUrl: BASE_URL });
191
202
  await client.connect();
192
203
 
204
+ // Spec: CreateDataResponse = { object, id, record }
193
205
  const newRecordRes = await client.data.create('customer', { name: 'Charlie' });
194
- expect(newRecordRes.success).toBe(true);
195
- expect(newRecordRes.data.name).toBe('Charlie');
196
- expect(newRecordRes.data.id).toBeDefined();
206
+ expect(newRecordRes.record).toBeDefined();
207
+ expect(newRecordRes.record.name).toBe('Charlie');
208
+ expect(newRecordRes.id).toBeDefined();
197
209
  });
198
210
  });
@@ -46,7 +46,7 @@ describe('ObjectStackClient', () => {
46
46
  });
47
47
 
48
48
  const result = await client.meta.getTypes();
49
- expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata', expect.any(Object));
49
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta', expect.any(Object));
50
50
  expect(result.types).toEqual(['object', 'plugin', 'view']);
51
51
  });
52
52
 
@@ -65,7 +65,7 @@ describe('ObjectStackClient', () => {
65
65
  });
66
66
 
67
67
  const result = await client.meta.getItems('object');
68
- expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata/object', expect.any(Object));
68
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object', expect.any(Object));
69
69
  expect(result.type).toBe('object');
70
70
  expect(result.items).toHaveLength(2);
71
71
  });
@@ -85,7 +85,7 @@ describe('ObjectStackClient', () => {
85
85
  });
86
86
 
87
87
  const result = await client.meta.getItem('object', 'customer');
88
- expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata/object/customer', expect.any(Object));
88
+ expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object/customer', expect.any(Object));
89
89
  expect(result.name).toBe('customer');
90
90
  });
91
91
  });
package/src/index.ts CHANGED
@@ -56,8 +56,46 @@ export interface QueryOptions {
56
56
  }
57
57
 
58
58
  export interface PaginatedResult<T = any> {
59
- value: T[];
60
- count: number;
59
+ /** @deprecated Use `records` — aligned with FindDataResponseSchema */
60
+ value?: T[];
61
+ /** Spec-compliant: array of matching records */
62
+ records: T[];
63
+ /** @deprecated Use `total` — aligned with FindDataResponseSchema */
64
+ count?: number;
65
+ /** Total number of matching records (if requested) */
66
+ total?: number;
67
+ /** The object name */
68
+ object?: string;
69
+ /** Whether more records are available */
70
+ hasMore?: boolean;
71
+ }
72
+
73
+ /** Spec: GetDataResponseSchema */
74
+ export interface GetDataResult<T = any> {
75
+ object: string;
76
+ id: string;
77
+ record: T;
78
+ }
79
+
80
+ /** Spec: CreateDataResponseSchema */
81
+ export interface CreateDataResult<T = any> {
82
+ object: string;
83
+ id: string;
84
+ record: T;
85
+ }
86
+
87
+ /** Spec: UpdateDataResponseSchema */
88
+ export interface UpdateDataResult<T = any> {
89
+ object: string;
90
+ id: string;
91
+ record: T;
92
+ }
93
+
94
+ /** Spec: DeleteDataResponseSchema */
95
+ export interface DeleteDataResult {
96
+ object: string;
97
+ id: string;
98
+ deleted: boolean;
61
99
  }
62
100
 
63
101
  export interface StandardError {
@@ -114,7 +152,8 @@ export class ObjectStackClient {
114
152
  this.logger.debug('Probing .well-known discovery', { url: wellKnownUrl });
115
153
  const res = await this.fetchImpl(wellKnownUrl);
116
154
  if (res.ok) {
117
- data = await res.json();
155
+ const body = await res.json();
156
+ data = body.data || body;
118
157
  this.logger.debug('Discovered via .well-known');
119
158
  }
120
159
  } catch (e) {
@@ -129,7 +168,8 @@ export class ObjectStackClient {
129
168
  if (!res.ok) {
130
169
  throw new Error(`Failed to connect to ${fallbackUrl}: ${res.statusText}`);
131
170
  }
132
- data = await res.json();
171
+ const body = await res.json();
172
+ data = body.data || body;
133
173
  }
134
174
 
135
175
  if (!data) {
@@ -162,17 +202,22 @@ export class ObjectStackClient {
162
202
  getTypes: async (): Promise<GetMetaTypesResponse> => {
163
203
  const route = this.getRoute('metadata');
164
204
  const res = await this.fetch(`${this.baseUrl}${route}`);
165
- return res.json();
205
+ return this.unwrapResponse<GetMetaTypesResponse>(res);
166
206
  },
167
207
 
168
208
  /**
169
209
  * Get all items of a specific metadata type
170
210
  * @param type - Metadata type name (e.g., 'object', 'plugin')
211
+ * @param options - Optional filters (e.g., packageId to scope by package)
171
212
  */
172
- getItems: async (type: string): Promise<GetMetaItemsResponse> => {
213
+ getItems: async (type: string, options?: { packageId?: string }): Promise<GetMetaItemsResponse> => {
173
214
  const route = this.getRoute('metadata');
174
- const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
175
- return res.json();
215
+ const params = new URLSearchParams();
216
+ if (options?.packageId) params.set('package', options.packageId);
217
+ const qs = params.toString();
218
+ const url = `${this.baseUrl}${route}/${type}${qs ? `?${qs}` : ''}`;
219
+ const res = await this.fetch(url);
220
+ return this.unwrapResponse<GetMetaItemsResponse>(res);
176
221
  },
177
222
 
178
223
  /**
@@ -183,7 +228,7 @@ export class ObjectStackClient {
183
228
  getObject: async (name: string) => {
184
229
  const route = this.getRoute('metadata');
185
230
  const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
186
- return res.json();
231
+ return this.unwrapResponse(res);
187
232
  },
188
233
 
189
234
  /**
@@ -194,7 +239,7 @@ export class ObjectStackClient {
194
239
  getItem: async (type: string, name: string) => {
195
240
  const route = this.getRoute('metadata');
196
241
  const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
197
- return res.json();
242
+ return this.unwrapResponse(res);
198
243
  },
199
244
 
200
245
  /**
@@ -209,7 +254,7 @@ export class ObjectStackClient {
209
254
  method: 'PUT',
210
255
  body: JSON.stringify(item)
211
256
  });
212
- return res.json();
257
+ return this.unwrapResponse(res);
213
258
  },
214
259
 
215
260
  /**
@@ -260,7 +305,7 @@ export class ObjectStackClient {
260
305
  getView: async (object: string, type: 'list' | 'form' = 'list') => {
261
306
  const route = this.getRoute('ui');
262
307
  const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
263
- return res.json();
308
+ return this.unwrapResponse(res);
264
309
  }
265
310
  };
266
311
 
@@ -322,6 +367,97 @@ export class ObjectStackClient {
322
367
  }
323
368
  };
324
369
 
370
+ /**
371
+ * Package Management Services
372
+ *
373
+ * Manages the lifecycle of installed packages.
374
+ * A package (ManifestSchema) is the unit of installation.
375
+ * An app (AppSchema) is a UI navigation definition within a package.
376
+ * A package may contain 0, 1, or many apps, or be a pure functionality plugin.
377
+ *
378
+ * Endpoints:
379
+ * - GET /packages → list installed packages
380
+ * - GET /packages/:id → get package details
381
+ * - POST /packages → install a package
382
+ * - DELETE /packages/:id → uninstall a package
383
+ * - PATCH /packages/:id/enable → enable a package
384
+ * - PATCH /packages/:id/disable → disable a package
385
+ */
386
+ packages = {
387
+ /**
388
+ * List all installed packages with optional filters.
389
+ */
390
+ list: async (filters?: { status?: string; type?: string; enabled?: boolean }) => {
391
+ const route = this.getRoute('packages');
392
+ const params = new URLSearchParams();
393
+ if (filters?.status) params.set('status', filters.status);
394
+ if (filters?.type) params.set('type', filters.type);
395
+ if (filters?.enabled !== undefined) params.set('enabled', String(filters.enabled));
396
+ const qs = params.toString();
397
+ const url = `${this.baseUrl}${route}${qs ? '?' + qs : ''}`;
398
+ const res = await this.fetch(url);
399
+ return this.unwrapResponse<{ packages: any[]; total: number }>(res);
400
+ },
401
+
402
+ /**
403
+ * Get a specific installed package by its ID (reverse domain identifier).
404
+ */
405
+ get: async (id: string) => {
406
+ const route = this.getRoute('packages');
407
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}`);
408
+ return this.unwrapResponse<{ package: any }>(res);
409
+ },
410
+
411
+ /**
412
+ * Install a new package from its manifest.
413
+ */
414
+ install: async (manifest: any, options?: { settings?: Record<string, any>; enableOnInstall?: boolean }) => {
415
+ const route = this.getRoute('packages');
416
+ const res = await this.fetch(`${this.baseUrl}${route}`, {
417
+ method: 'POST',
418
+ body: JSON.stringify({
419
+ manifest,
420
+ settings: options?.settings,
421
+ enableOnInstall: options?.enableOnInstall,
422
+ }),
423
+ });
424
+ return this.unwrapResponse<{ package: any; message?: string }>(res);
425
+ },
426
+
427
+ /**
428
+ * Uninstall a package by its ID.
429
+ */
430
+ uninstall: async (id: string) => {
431
+ const route = this.getRoute('packages');
432
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}`, {
433
+ method: 'DELETE',
434
+ });
435
+ return this.unwrapResponse<{ id: string; success: boolean; message?: string }>(res);
436
+ },
437
+
438
+ /**
439
+ * Enable a disabled package.
440
+ */
441
+ enable: async (id: string) => {
442
+ const route = this.getRoute('packages');
443
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/enable`, {
444
+ method: 'PATCH',
445
+ });
446
+ return this.unwrapResponse<{ package: any; message?: string }>(res);
447
+ },
448
+
449
+ /**
450
+ * Disable an installed package.
451
+ */
452
+ disable: async (id: string) => {
453
+ const route = this.getRoute('packages');
454
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/disable`, {
455
+ method: 'PATCH',
456
+ });
457
+ return this.unwrapResponse<{ package: any; message?: string }>(res);
458
+ },
459
+ };
460
+
325
461
  /**
326
462
  * Authentication Services
327
463
  */
@@ -435,7 +571,7 @@ export class ObjectStackClient {
435
571
  method: 'POST',
436
572
  body: JSON.stringify(query)
437
573
  });
438
- return res.json();
574
+ return this.unwrapResponse<PaginatedResult<T>>(res);
439
575
  },
440
576
 
441
577
  find: async <T = any>(object: string, options: QueryOptions = {}): Promise<PaginatedResult<T>> => {
@@ -486,22 +622,22 @@ export class ObjectStackClient {
486
622
  }
487
623
 
488
624
  const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
489
- return res.json();
625
+ return this.unwrapResponse<PaginatedResult<T>>(res);
490
626
  },
491
627
 
492
- get: async <T = any>(object: string, id: string): Promise<T> => {
628
+ get: async <T = any>(object: string, id: string): Promise<GetDataResult<T>> => {
493
629
  const route = this.getRoute('data');
494
630
  const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
495
- return res.json();
631
+ return this.unwrapResponse<GetDataResult<T>>(res);
496
632
  },
497
633
 
498
- create: async <T = any>(object: string, data: Partial<T>): Promise<T> => {
634
+ create: async <T = any>(object: string, data: Partial<T>): Promise<CreateDataResult<T>> => {
499
635
  const route = this.getRoute('data');
500
636
  const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
501
637
  method: 'POST',
502
638
  body: JSON.stringify(data)
503
639
  });
504
- return res.json();
640
+ return this.unwrapResponse<CreateDataResult<T>>(res);
505
641
  },
506
642
 
507
643
  createMany: async <T = any>(object: string, data: Partial<T>[]): Promise<T[]> => {
@@ -510,16 +646,16 @@ export class ObjectStackClient {
510
646
  method: 'POST',
511
647
  body: JSON.stringify(data)
512
648
  });
513
- return res.json();
649
+ return this.unwrapResponse<T[]>(res);
514
650
  },
515
651
 
516
- update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<T> => {
652
+ update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<UpdateDataResult<T>> => {
517
653
  const route = this.getRoute('data');
518
654
  const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
519
655
  method: 'PATCH',
520
656
  body: JSON.stringify(data)
521
657
  });
522
- return res.json();
658
+ return this.unwrapResponse<UpdateDataResult<T>>(res);
523
659
  },
524
660
 
525
661
  /**
@@ -532,7 +668,7 @@ export class ObjectStackClient {
532
668
  method: 'POST',
533
669
  body: JSON.stringify(request)
534
670
  });
535
- return res.json();
671
+ return this.unwrapResponse<BatchUpdateResponse>(res);
536
672
  },
537
673
 
538
674
  /**
@@ -553,15 +689,15 @@ export class ObjectStackClient {
553
689
  method: 'POST',
554
690
  body: JSON.stringify(request)
555
691
  });
556
- return res.json();
692
+ return this.unwrapResponse<BatchUpdateResponse>(res);
557
693
  },
558
694
 
559
- delete: async (object: string, id: string): Promise<{ success: boolean }> => {
695
+ delete: async (object: string, id: string): Promise<DeleteDataResult> => {
560
696
  const route = this.getRoute('data');
561
697
  const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
562
698
  method: 'DELETE'
563
699
  });
564
- return res.json();
700
+ return this.unwrapResponse<DeleteDataResult>(res);
565
701
  },
566
702
 
567
703
  /**
@@ -577,7 +713,7 @@ export class ObjectStackClient {
577
713
  method: 'POST',
578
714
  body: JSON.stringify(request)
579
715
  });
580
- return res.json();
716
+ return this.unwrapResponse<BatchUpdateResponse>(res);
581
717
  }
582
718
  };
583
719
 
@@ -594,6 +730,23 @@ export class ObjectStackClient {
594
730
  return Array.isArray(filter);
595
731
  }
596
732
 
733
+ /**
734
+ * Unwrap the standard REST API response envelope.
735
+ * The HTTP layer wraps responses as `{ success: boolean, data: T, meta? }`
736
+ * (see BaseResponseSchema in contract.zod.ts).
737
+ * This method strips the envelope and returns the inner `data` payload
738
+ * so callers receive the spec-level type (e.g. GetMetaTypesResponse).
739
+ */
740
+ private async unwrapResponse<T>(res: Response): Promise<T> {
741
+ const body = await res.json();
742
+ // If the body has a `success` flag it's a BaseResponse envelope
743
+ if (body && typeof body.success === 'boolean' && 'data' in body) {
744
+ return body.data as T;
745
+ }
746
+ // Already unwrapped or non-standard
747
+ return body as T;
748
+ }
749
+
597
750
  private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
598
751
  this.logger.debug('HTTP request', {
599
752
  method: options.method || 'GET',
@@ -654,9 +807,9 @@ export class ObjectStackClient {
654
807
 
655
808
  /**
656
809
  * Get the conventional route path for a given API endpoint type
657
- * ObjectStack uses standard conventions: /api/v1/data, /api/v1/metadata, /api/v1/ui
810
+ * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
658
811
  */
659
- private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'hub' | 'storage' | 'automation'): string {
812
+ private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'hub' | 'storage' | 'automation' | 'packages'): string {
660
813
  // 1. Use discovered routes if available
661
814
  // Note: Spec uses 'endpoints', mapped dynamically
662
815
  if (this.discoveryInfo?.endpoints && (this.discoveryInfo.endpoints as any)[type]) {
@@ -664,16 +817,16 @@ export class ObjectStackClient {
664
817
  }
665
818
 
666
819
  // 2. Fallback to conventions
667
- // Note: HttpDispatcher expects /metadata, not /meta
668
820
  const routeMap: Record<string, string> = {
669
821
  data: '/api/v1/data',
670
- metadata: '/api/v1/metadata',
822
+ metadata: '/api/v1/meta',
671
823
  ui: '/api/v1/ui',
672
824
  auth: '/api/v1/auth',
673
825
  analytics: '/api/v1/analytics',
674
826
  hub: '/api/v1/hub',
675
827
  storage: '/api/v1/storage',
676
- automation: '/api/v1/automation'
828
+ automation: '/api/v1/automation',
829
+ packages: '/api/v1/packages',
677
830
  };
678
831
 
679
832
  return routeMap[type] || `/api/v1/${type}`;