@objectstack/client 1.0.11 → 1.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.d.mts +125 -14
- package/dist/index.d.ts +125 -14
- package/dist/index.js +128 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +128 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -9
- package/src/client.hono.test.ts +30 -25
- package/src/client.msw.test.ts +28 -16
- package/src/index.ts +182 -28
package/src/client.hono.test.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
return {
|
|
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
|
-
|
|
42
|
-
return
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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.
|
|
123
|
-
expect(createdResponse.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
141
|
-
expect(resultsResponse.
|
|
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
|
});
|
package/src/client.msw.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
return {
|
|
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') {
|
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
expect(customerRes
|
|
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.
|
|
176
|
-
expect(resultsRes.
|
|
177
|
-
expect(resultsRes.
|
|
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.
|
|
186
|
-
expect(recordRes.
|
|
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.
|
|
195
|
-
expect(newRecordRes.
|
|
196
|
-
expect(newRecordRes.
|
|
206
|
+
expect(newRecordRes.record).toBeDefined();
|
|
207
|
+
expect(newRecordRes.record.name).toBe('Charlie');
|
|
208
|
+
expect(newRecordRes.id).toBeDefined();
|
|
197
209
|
});
|
|
198
210
|
});
|
package/src/index.ts
CHANGED
|
@@ -56,8 +56,46 @@ export interface QueryOptions {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export interface PaginatedResult<T = any> {
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
175
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
692
|
+
return this.unwrapResponse<BatchUpdateResponse>(res);
|
|
557
693
|
},
|
|
558
694
|
|
|
559
|
-
delete: async (object: string, id: string): Promise<
|
|
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
|
|
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
|
|
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',
|
|
@@ -656,7 +809,7 @@ export class ObjectStackClient {
|
|
|
656
809
|
* Get the conventional route path for a given API endpoint type
|
|
657
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]) {
|
|
@@ -672,7 +825,8 @@ export class ObjectStackClient {
|
|
|
672
825
|
analytics: '/api/v1/analytics',
|
|
673
826
|
hub: '/api/v1/hub',
|
|
674
827
|
storage: '/api/v1/storage',
|
|
675
|
-
automation: '/api/v1/automation'
|
|
828
|
+
automation: '/api/v1/automation',
|
|
829
|
+
packages: '/api/v1/packages',
|
|
676
830
|
};
|
|
677
831
|
|
|
678
832
|
return routeMap[type] || `/api/v1/${type}`;
|