@objectstack/client 3.3.0 → 4.0.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 +52 -0
- package/README.md +9 -6
- package/dist/index.d.mts +83 -13
- package/dist/index.d.ts +83 -13
- package/dist/index.js +74 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +74 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -11
- package/src/client.hono.test.ts +2 -2
- package/src/client.msw.test.ts +2 -2
- package/src/client.test.ts +70 -0
- package/src/index.ts +127 -40
- package/src/query-builder.ts +10 -1
- package/tsconfig.json +2 -1
package/src/client.hono.test.ts
CHANGED
|
@@ -52,14 +52,14 @@ describe('ObjectStackClient (with Hono Server)', () => {
|
|
|
52
52
|
if (protocol) {
|
|
53
53
|
return await protocol.findData({ object: params.object, query: params.query || params.filters });
|
|
54
54
|
}
|
|
55
|
-
const records = await ql.find(params.object, {
|
|
55
|
+
const records = await ql.find(params.object, { where: params.filters });
|
|
56
56
|
return { object: params.object, records, total: records.length };
|
|
57
57
|
}
|
|
58
58
|
if (method === 'find') {
|
|
59
59
|
if (protocol) {
|
|
60
60
|
return await protocol.findData({ object: params.object, query: params.query || params.filters });
|
|
61
61
|
}
|
|
62
|
-
const records = await ql.find(params.object, {
|
|
62
|
+
const records = await ql.find(params.object, { where: params.filters });
|
|
63
63
|
return { object: params.object, records, total: records.length };
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/client.msw.test.ts
CHANGED
|
@@ -57,7 +57,7 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
|
|
|
57
57
|
return await protocol.findData({ object: params.object, query: params.query });
|
|
58
58
|
}
|
|
59
59
|
const queryOpts = params.query || {};
|
|
60
|
-
const records = await ql.find(params.object, {
|
|
60
|
+
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
|
|
61
61
|
return { object: params.object, records, total: records.length };
|
|
62
62
|
}
|
|
63
63
|
if (method === 'find') {
|
|
@@ -65,7 +65,7 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
|
|
|
65
65
|
return await protocol.findData({ object: params.object, query: params.query });
|
|
66
66
|
}
|
|
67
67
|
const queryOpts = params.query || {};
|
|
68
|
-
const records = await ql.find(params.object, {
|
|
68
|
+
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
|
|
69
69
|
return { object: params.object, records, total: records.length };
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/client.test.ts
CHANGED
|
@@ -827,3 +827,73 @@ describe('ObjectStackClient.automation', () => {
|
|
|
827
827
|
expect(client.capabilities!.search).toBe(true);
|
|
828
828
|
});
|
|
829
829
|
});
|
|
830
|
+
|
|
831
|
+
// ==========================================
|
|
832
|
+
// QueryOptionsV2 (Canonical Query Syntax) Tests
|
|
833
|
+
// ==========================================
|
|
834
|
+
|
|
835
|
+
describe('QueryOptionsV2 — canonical find()', () => {
|
|
836
|
+
it('should accept canonical field names (where, fields, orderBy, limit, offset)', async () => {
|
|
837
|
+
const { client, fetchMock } = createMockClient({
|
|
838
|
+
success: true,
|
|
839
|
+
data: { object: 'account', records: [], total: 0 }
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
await client.data.find('account', {
|
|
843
|
+
where: { status: 'active' },
|
|
844
|
+
fields: ['name', 'email'],
|
|
845
|
+
orderBy: ['-created_at'],
|
|
846
|
+
limit: 10,
|
|
847
|
+
offset: 5,
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const url = fetchMock.mock.calls[0][0] as string;
|
|
851
|
+
// V2 canonical options are normalized to HTTP transport params
|
|
852
|
+
expect(url).toContain('top=10');
|
|
853
|
+
expect(url).toContain('skip=5');
|
|
854
|
+
expect(url).toContain('select=name%2Cemail');
|
|
855
|
+
expect(url).toContain('sort=-created_at');
|
|
856
|
+
// where → filter as JSON
|
|
857
|
+
expect(url).toContain('status=active');
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('should still accept legacy field names (filter, select, sort, top, skip)', async () => {
|
|
861
|
+
const { client, fetchMock } = createMockClient({
|
|
862
|
+
success: true,
|
|
863
|
+
data: { object: 'account', records: [], total: 0 }
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
await client.data.find('account', {
|
|
867
|
+
filter: { industry: 'Tech' },
|
|
868
|
+
select: ['name'],
|
|
869
|
+
sort: ['-revenue'],
|
|
870
|
+
top: 20,
|
|
871
|
+
skip: 0,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
const url = fetchMock.mock.calls[0][0] as string;
|
|
875
|
+
expect(url).toContain('top=20');
|
|
876
|
+
expect(url).toContain('select=name');
|
|
877
|
+
expect(url).toContain('sort=-revenue');
|
|
878
|
+
expect(url).toContain('industry=Tech');
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
describe('QueryBuilder — offset() alias', () => {
|
|
883
|
+
it('should set offset via .offset() method', () => {
|
|
884
|
+
const q = createQuery('task')
|
|
885
|
+
.limit(10)
|
|
886
|
+
.offset(20)
|
|
887
|
+
.build();
|
|
888
|
+
expect(q.limit).toBe(10);
|
|
889
|
+
expect(q.offset).toBe(20);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('should set offset via deprecated .skip() method', () => {
|
|
893
|
+
const q = createQuery('task')
|
|
894
|
+
.limit(10)
|
|
895
|
+
.skip(30)
|
|
896
|
+
.build();
|
|
897
|
+
expect(q.offset).toBe(30);
|
|
898
|
+
});
|
|
899
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -87,9 +87,17 @@ import {
|
|
|
87
87
|
SubscribeResponse,
|
|
88
88
|
UnsubscribeResponse,
|
|
89
89
|
WellKnownCapabilities,
|
|
90
|
+
ApiRoutes,
|
|
90
91
|
} from '@objectstack/spec/api';
|
|
91
92
|
import { Logger, createLogger } from '@objectstack/core';
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Route types that the client can resolve.
|
|
96
|
+
* Covers all keys from `ApiRoutes` (the discovery schema) plus
|
|
97
|
+
* client-specific virtual routes (`views`, `permissions`).
|
|
98
|
+
*/
|
|
99
|
+
export type ApiRouteType = keyof ApiRoutes | 'views' | 'permissions';
|
|
100
|
+
|
|
93
101
|
export interface ClientConfig {
|
|
94
102
|
baseUrl: string;
|
|
95
103
|
token?: string;
|
|
@@ -113,6 +121,16 @@ export interface ClientConfig {
|
|
|
113
121
|
*/
|
|
114
122
|
export type DiscoveryResult = GetDiscoveryResponse;
|
|
115
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @deprecated Use `data.query()` with standard QueryAST parameters instead.
|
|
126
|
+
* This interface uses legacy parameter names (filter/sort/top/skip) that
|
|
127
|
+
* require translation to QueryAST. Prefer QueryAST fields directly:
|
|
128
|
+
* - filter → where
|
|
129
|
+
* - select → fields
|
|
130
|
+
* - sort → orderBy
|
|
131
|
+
* - skip → offset
|
|
132
|
+
* - top → limit
|
|
133
|
+
*/
|
|
116
134
|
export interface QueryOptions {
|
|
117
135
|
select?: string[]; // Simplified Selection
|
|
118
136
|
/** @canonical Preferred filter parameter (singular). */
|
|
@@ -127,6 +145,37 @@ export interface QueryOptions {
|
|
|
127
145
|
groupBy?: string[];
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Canonical query options using Spec protocol field names.
|
|
150
|
+
* This is the recommended interface for `data.find()` queries.
|
|
151
|
+
*
|
|
152
|
+
* Canonical field mapping (QueryAST-aligned):
|
|
153
|
+
* - `where` — filter conditions (replaces legacy `filter`/`filters`)
|
|
154
|
+
* - `fields` — field selection (replaces legacy `select`)
|
|
155
|
+
* - `orderBy` — sort definition (replaces legacy `sort`)
|
|
156
|
+
* - `limit` — max records (replaces legacy `top`)
|
|
157
|
+
* - `offset` — skip records (replaces legacy `skip`)
|
|
158
|
+
* - `expand` — relation loading (replaces legacy `populate`)
|
|
159
|
+
*/
|
|
160
|
+
export interface QueryOptionsV2 {
|
|
161
|
+
/** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */
|
|
162
|
+
where?: Record<string, any> | unknown[];
|
|
163
|
+
/** Fields to retrieve (SELECT clause). */
|
|
164
|
+
fields?: string[];
|
|
165
|
+
/** Sort definition (ORDER BY clause). */
|
|
166
|
+
orderBy?: string | string[] | SortNode[];
|
|
167
|
+
/** Maximum number of records to return (LIMIT). */
|
|
168
|
+
limit?: number;
|
|
169
|
+
/** Number of records to skip (OFFSET). */
|
|
170
|
+
offset?: number;
|
|
171
|
+
/** Relations to expand (JOIN / eager-load). */
|
|
172
|
+
expand?: Record<string, any> | string[];
|
|
173
|
+
/** Aggregation functions. */
|
|
174
|
+
aggregations?: AggregationNode[];
|
|
175
|
+
/** Group by fields. */
|
|
176
|
+
groupBy?: string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
export interface PaginatedResult<T = any> {
|
|
131
180
|
/** Spec-compliant: array of matching records */
|
|
132
181
|
records: T[];
|
|
@@ -228,10 +277,10 @@ export class ObjectStackClient {
|
|
|
228
277
|
this.logger.debug('Standard discovery probe failed', { error: (e as Error).message });
|
|
229
278
|
}
|
|
230
279
|
|
|
231
|
-
// 2. Fallback to
|
|
280
|
+
// 2. Fallback to Protocol-standard Discovery Path /api/v1/discovery
|
|
232
281
|
if (!data) {
|
|
233
|
-
const fallbackUrl = `${this.baseUrl}/api/v1`;
|
|
234
|
-
this.logger.debug('Falling back to
|
|
282
|
+
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
283
|
+
this.logger.debug('Falling back to standard discovery endpoint', { url: fallbackUrl });
|
|
235
284
|
const res = await this.fetchImpl(fallbackUrl);
|
|
236
285
|
if (!res.ok) {
|
|
237
286
|
throw new Error(`Failed to connect to ${fallbackUrl}: ${res.statusText}`);
|
|
@@ -263,9 +312,20 @@ export class ObjectStackClient {
|
|
|
263
312
|
* Well-known capability flags discovered from the server.
|
|
264
313
|
* Returns undefined if the client has not yet connected or the server
|
|
265
314
|
* did not include capabilities in its discovery response.
|
|
315
|
+
*
|
|
316
|
+
* The server may return capabilities in hierarchical format
|
|
317
|
+
* `{ key: { enabled: boolean } }` or flat boolean format `{ key: boolean }`.
|
|
318
|
+
* This getter normalizes both to flat `WellKnownCapabilities`.
|
|
266
319
|
*/
|
|
267
320
|
get capabilities(): WellKnownCapabilities | undefined {
|
|
268
|
-
|
|
321
|
+
const raw = this.discoveryInfo?.capabilities;
|
|
322
|
+
if (!raw) return undefined;
|
|
323
|
+
// Normalize: hierarchical { enabled: boolean } → flat boolean
|
|
324
|
+
const result: Record<string, boolean> = {};
|
|
325
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
326
|
+
result[key] = typeof value === 'object' && value !== null ? !!(value as any).enabled : !!value;
|
|
327
|
+
}
|
|
328
|
+
return result as unknown as WellKnownCapabilities;
|
|
269
329
|
}
|
|
270
330
|
|
|
271
331
|
/**
|
|
@@ -1234,7 +1294,7 @@ export class ObjectStackClient {
|
|
|
1234
1294
|
* List feed items for a record
|
|
1235
1295
|
*/
|
|
1236
1296
|
list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
|
|
1237
|
-
const route = this.getRoute('
|
|
1297
|
+
const route = this.getRoute('data');
|
|
1238
1298
|
const params = new URLSearchParams();
|
|
1239
1299
|
if (options?.type) params.set('type', options.type);
|
|
1240
1300
|
if (options?.limit) params.set('limit', String(options.limit));
|
|
@@ -1248,7 +1308,7 @@ export class ObjectStackClient {
|
|
|
1248
1308
|
* Create a new feed item (comment, note, task, etc.)
|
|
1249
1309
|
*/
|
|
1250
1310
|
create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
|
|
1251
|
-
const route = this.getRoute('
|
|
1311
|
+
const route = this.getRoute('data');
|
|
1252
1312
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
|
|
1253
1313
|
method: 'POST',
|
|
1254
1314
|
body: JSON.stringify(data)
|
|
@@ -1260,7 +1320,7 @@ export class ObjectStackClient {
|
|
|
1260
1320
|
* Update an existing feed item
|
|
1261
1321
|
*/
|
|
1262
1322
|
update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
|
|
1263
|
-
const route = this.getRoute('
|
|
1323
|
+
const route = this.getRoute('data');
|
|
1264
1324
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
|
|
1265
1325
|
method: 'PUT',
|
|
1266
1326
|
body: JSON.stringify(data)
|
|
@@ -1272,7 +1332,7 @@ export class ObjectStackClient {
|
|
|
1272
1332
|
* Delete a feed item
|
|
1273
1333
|
*/
|
|
1274
1334
|
delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
|
|
1275
|
-
const route = this.getRoute('
|
|
1335
|
+
const route = this.getRoute('data');
|
|
1276
1336
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
|
|
1277
1337
|
method: 'DELETE'
|
|
1278
1338
|
});
|
|
@@ -1283,7 +1343,7 @@ export class ObjectStackClient {
|
|
|
1283
1343
|
* Add an emoji reaction to a feed item
|
|
1284
1344
|
*/
|
|
1285
1345
|
addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
|
|
1286
|
-
const route = this.getRoute('
|
|
1346
|
+
const route = this.getRoute('data');
|
|
1287
1347
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
|
|
1288
1348
|
method: 'POST',
|
|
1289
1349
|
body: JSON.stringify({ emoji })
|
|
@@ -1295,7 +1355,7 @@ export class ObjectStackClient {
|
|
|
1295
1355
|
* Remove an emoji reaction from a feed item
|
|
1296
1356
|
*/
|
|
1297
1357
|
removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
|
|
1298
|
-
const route = this.getRoute('
|
|
1358
|
+
const route = this.getRoute('data');
|
|
1299
1359
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
|
|
1300
1360
|
method: 'DELETE'
|
|
1301
1361
|
});
|
|
@@ -1306,7 +1366,7 @@ export class ObjectStackClient {
|
|
|
1306
1366
|
* Pin a feed item to the top of the timeline
|
|
1307
1367
|
*/
|
|
1308
1368
|
pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
|
|
1309
|
-
const route = this.getRoute('
|
|
1369
|
+
const route = this.getRoute('data');
|
|
1310
1370
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
|
|
1311
1371
|
method: 'POST'
|
|
1312
1372
|
});
|
|
@@ -1317,7 +1377,7 @@ export class ObjectStackClient {
|
|
|
1317
1377
|
* Unpin a feed item
|
|
1318
1378
|
*/
|
|
1319
1379
|
unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
|
|
1320
|
-
const route = this.getRoute('
|
|
1380
|
+
const route = this.getRoute('data');
|
|
1321
1381
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
|
|
1322
1382
|
method: 'DELETE'
|
|
1323
1383
|
});
|
|
@@ -1328,7 +1388,7 @@ export class ObjectStackClient {
|
|
|
1328
1388
|
* Star (bookmark) a feed item
|
|
1329
1389
|
*/
|
|
1330
1390
|
star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
|
|
1331
|
-
const route = this.getRoute('
|
|
1391
|
+
const route = this.getRoute('data');
|
|
1332
1392
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
|
|
1333
1393
|
method: 'POST'
|
|
1334
1394
|
});
|
|
@@ -1339,7 +1399,7 @@ export class ObjectStackClient {
|
|
|
1339
1399
|
* Unstar a feed item
|
|
1340
1400
|
*/
|
|
1341
1401
|
unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
|
|
1342
|
-
const route = this.getRoute('
|
|
1402
|
+
const route = this.getRoute('data');
|
|
1343
1403
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
|
|
1344
1404
|
method: 'DELETE'
|
|
1345
1405
|
});
|
|
@@ -1350,7 +1410,7 @@ export class ObjectStackClient {
|
|
|
1350
1410
|
* Search feed items
|
|
1351
1411
|
*/
|
|
1352
1412
|
search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
|
|
1353
|
-
const route = this.getRoute('
|
|
1413
|
+
const route = this.getRoute('data');
|
|
1354
1414
|
const params = new URLSearchParams();
|
|
1355
1415
|
params.set('query', query);
|
|
1356
1416
|
if (options?.type) params.set('type', options.type);
|
|
@@ -1367,7 +1427,7 @@ export class ObjectStackClient {
|
|
|
1367
1427
|
* Get field-level changelog for a record
|
|
1368
1428
|
*/
|
|
1369
1429
|
getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
|
|
1370
|
-
const route = this.getRoute('
|
|
1430
|
+
const route = this.getRoute('data');
|
|
1371
1431
|
const params = new URLSearchParams();
|
|
1372
1432
|
if (options?.field) params.set('field', options.field);
|
|
1373
1433
|
if (options?.actorId) params.set('actorId', options.actorId);
|
|
@@ -1384,7 +1444,7 @@ export class ObjectStackClient {
|
|
|
1384
1444
|
* Subscribe to record notifications
|
|
1385
1445
|
*/
|
|
1386
1446
|
subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
|
|
1387
|
-
const route = this.getRoute('
|
|
1447
|
+
const route = this.getRoute('data');
|
|
1388
1448
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
|
|
1389
1449
|
method: 'POST',
|
|
1390
1450
|
body: JSON.stringify(options || {})
|
|
@@ -1396,7 +1456,7 @@ export class ObjectStackClient {
|
|
|
1396
1456
|
* Unsubscribe from record notifications
|
|
1397
1457
|
*/
|
|
1398
1458
|
unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
|
|
1399
|
-
const route = this.getRoute('
|
|
1459
|
+
const route = this.getRoute('data');
|
|
1400
1460
|
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
|
|
1401
1461
|
method: 'DELETE'
|
|
1402
1462
|
});
|
|
@@ -1423,34 +1483,56 @@ export class ObjectStackClient {
|
|
|
1423
1483
|
return this.unwrapResponse<PaginatedResult<T>>(res);
|
|
1424
1484
|
},
|
|
1425
1485
|
|
|
1426
|
-
|
|
1486
|
+
/**
|
|
1487
|
+
* @deprecated Use `data.query()` with standard QueryAST parameters instead.
|
|
1488
|
+
* This method uses legacy parameter names. Internally adapts to HTTP GET params.
|
|
1489
|
+
*/
|
|
1490
|
+
find: async <T = any>(object: string, options: QueryOptions | QueryOptionsV2 = {}): Promise<PaginatedResult<T>> => {
|
|
1427
1491
|
const route = this.getRoute('data');
|
|
1428
1492
|
const queryParams = new URLSearchParams();
|
|
1429
|
-
|
|
1493
|
+
|
|
1494
|
+
// ── Normalize V2 canonical options → HTTP transport params ───
|
|
1495
|
+
// Detect V2 options by presence of canonical-only keys.
|
|
1496
|
+
const v2 = options as QueryOptionsV2;
|
|
1497
|
+
const normalizedOptions: QueryOptions = {} as QueryOptions;
|
|
1498
|
+
if ('where' in options || 'fields' in options || 'orderBy' in options || 'offset' in options) {
|
|
1499
|
+
// V2 canonical options detected — map to legacy HTTP transport keys
|
|
1500
|
+
if (v2.where) normalizedOptions.filter = v2.where as any;
|
|
1501
|
+
if (v2.fields) normalizedOptions.select = v2.fields;
|
|
1502
|
+
if (v2.orderBy) normalizedOptions.sort = v2.orderBy as any;
|
|
1503
|
+
if (v2.limit != null) normalizedOptions.top = v2.limit;
|
|
1504
|
+
if (v2.offset != null) normalizedOptions.skip = v2.offset;
|
|
1505
|
+
if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
|
|
1506
|
+
if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
|
|
1507
|
+
} else {
|
|
1508
|
+
// Legacy QueryOptions — pass through as-is
|
|
1509
|
+
Object.assign(normalizedOptions, options);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1430
1512
|
// 1. Handle Pagination
|
|
1431
|
-
if (
|
|
1432
|
-
if (
|
|
1513
|
+
if (normalizedOptions.top) queryParams.set('top', normalizedOptions.top.toString());
|
|
1514
|
+
if (normalizedOptions.skip) queryParams.set('skip', normalizedOptions.skip.toString());
|
|
1433
1515
|
|
|
1434
1516
|
// 2. Handle Sort
|
|
1435
|
-
if (
|
|
1517
|
+
if (normalizedOptions.sort) {
|
|
1436
1518
|
// Check if it's AST
|
|
1437
|
-
if (Array.isArray(
|
|
1438
|
-
queryParams.set('sort', JSON.stringify(
|
|
1519
|
+
if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === 'object') {
|
|
1520
|
+
queryParams.set('sort', JSON.stringify(normalizedOptions.sort));
|
|
1439
1521
|
} else {
|
|
1440
|
-
const sortVal = Array.isArray(
|
|
1522
|
+
const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(',') : normalizedOptions.sort;
|
|
1441
1523
|
queryParams.set('sort', sortVal as string);
|
|
1442
1524
|
}
|
|
1443
1525
|
}
|
|
1444
1526
|
|
|
1445
1527
|
// 3. Handle Select
|
|
1446
|
-
if (
|
|
1447
|
-
queryParams.set('select',
|
|
1528
|
+
if (normalizedOptions.select) {
|
|
1529
|
+
queryParams.set('select', normalizedOptions.select.join(','));
|
|
1448
1530
|
}
|
|
1449
1531
|
|
|
1450
1532
|
// 4. Handle Filters (Simple vs AST)
|
|
1451
1533
|
// Canonical HTTP param name: `filter` (singular). `filters` (plural) is accepted
|
|
1452
1534
|
// for backward compatibility but `filter` is the standard going forward.
|
|
1453
|
-
const filterValue =
|
|
1535
|
+
const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
|
|
1454
1536
|
if (filterValue) {
|
|
1455
1537
|
// Detect AST filter format vs simple key-value map. AST filters use an array structure
|
|
1456
1538
|
// with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
|
|
@@ -1469,11 +1551,11 @@ export class ObjectStackClient {
|
|
|
1469
1551
|
}
|
|
1470
1552
|
|
|
1471
1553
|
// 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
|
|
1472
|
-
if (
|
|
1473
|
-
queryParams.set('aggregations', JSON.stringify(
|
|
1554
|
+
if (normalizedOptions.aggregations) {
|
|
1555
|
+
queryParams.set('aggregations', JSON.stringify(normalizedOptions.aggregations));
|
|
1474
1556
|
}
|
|
1475
|
-
if (
|
|
1476
|
-
queryParams.set('groupBy',
|
|
1557
|
+
if (normalizedOptions.groupBy) {
|
|
1558
|
+
queryParams.set('groupBy', normalizedOptions.groupBy.join(','));
|
|
1477
1559
|
}
|
|
1478
1560
|
|
|
1479
1561
|
const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
|
|
@@ -1663,16 +1745,20 @@ export class ObjectStackClient {
|
|
|
1663
1745
|
* Get the conventional route path for a given API endpoint type
|
|
1664
1746
|
* ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
|
|
1665
1747
|
*/
|
|
1666
|
-
private getRoute(type:
|
|
1667
|
-
// 1. Use discovered routes if available
|
|
1668
|
-
|
|
1669
|
-
|
|
1748
|
+
private getRoute(type: ApiRouteType): string {
|
|
1749
|
+
// 1. Use discovered routes if available (only for ApiRoutes keys, not client-specific keys)
|
|
1750
|
+
const routes = this.discoveryInfo?.routes;
|
|
1751
|
+
if (routes) {
|
|
1752
|
+
const key = type as keyof ApiRoutes;
|
|
1753
|
+
const discovered = routes[key];
|
|
1754
|
+
if (discovered) return discovered;
|
|
1670
1755
|
}
|
|
1671
1756
|
|
|
1672
|
-
// 2. Fallback to conventions
|
|
1673
|
-
const routeMap: Record<
|
|
1757
|
+
// 2. Fallback to conventions (covers all ApiRoutes keys + client-specific virtual routes)
|
|
1758
|
+
const routeMap: Record<ApiRouteType, string> = {
|
|
1674
1759
|
data: '/api/v1/data',
|
|
1675
1760
|
metadata: '/api/v1/meta',
|
|
1761
|
+
discovery: '/api/v1/discovery',
|
|
1676
1762
|
ui: '/api/v1/ui',
|
|
1677
1763
|
auth: '/api/v1/auth',
|
|
1678
1764
|
analytics: '/api/v1/analytics',
|
|
@@ -1686,7 +1772,8 @@ export class ObjectStackClient {
|
|
|
1686
1772
|
notifications: '/api/v1/notifications',
|
|
1687
1773
|
ai: '/api/v1/ai',
|
|
1688
1774
|
i18n: '/api/v1/i18n',
|
|
1689
|
-
feed: '/api/v1/
|
|
1775
|
+
feed: '/api/v1/feed',
|
|
1776
|
+
graphql: '/graphql',
|
|
1690
1777
|
};
|
|
1691
1778
|
|
|
1692
1779
|
return routeMap[type] || `/api/v1/${type}`;
|
package/src/query-builder.ts
CHANGED
|
@@ -236,13 +236,22 @@ export class QueryBuilder<T = any> {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* Skip records (for pagination)
|
|
239
|
+
* Skip records (for pagination).
|
|
240
|
+
* @deprecated Prefer `.offset()` for alignment with Spec canonical field names.
|
|
240
241
|
*/
|
|
241
242
|
skip(count: number): this {
|
|
242
243
|
this.query.offset = count;
|
|
243
244
|
return this;
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Offset records (for pagination) — canonical alias for `.skip()`
|
|
249
|
+
*/
|
|
250
|
+
offset(count: number): this {
|
|
251
|
+
this.query.offset = count;
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
|
|
246
255
|
/**
|
|
247
256
|
* Paginate results
|
|
248
257
|
*/
|
package/tsconfig.json
CHANGED