@objectstack/client 0.6.1 → 0.7.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @objectstack/client
2
2
 
3
+ ## 0.7.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fb41cc0: Patch release: Updated documentation and JSON schemas
8
+ - Updated dependencies [fb41cc0]
9
+ - @objectstack/spec@0.7.2
10
+ - @objectstack/core@0.7.2
11
+
12
+ ## 0.7.1
13
+
14
+ ### Patch Changes
15
+
16
+ - Patch release for maintenance and stability improvements
17
+ - Updated dependencies
18
+ - @objectstack/spec@0.7.1
19
+ - @objectstack/core@0.7.1
20
+
3
21
  ## 0.6.1
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -6,7 +6,11 @@ The official TypeScript client for ObjectStack.
6
6
 
7
7
  - **Auto-Discovery**: Connects to your ObjectStack server and automatically configures API endpoints.
8
8
  - **Typed Metadata**: Retrieve Object and View definitions with full type support.
9
+ - **Metadata Caching**: ETag-based conditional requests for efficient metadata caching.
9
10
  - **Unified Data Access**: Simple CRUD operations for any object in your schema.
11
+ - **Batch Operations**: Efficient bulk create/update/delete with transaction support.
12
+ - **View Storage**: Save, load, and share custom UI view configurations.
13
+ - **Standardized Errors**: Machine-readable error codes with retry guidance.
10
14
 
11
15
  ## Installation
12
16
 
@@ -47,8 +51,48 @@ async function main() {
47
51
  priority: 1
48
52
  });
49
53
 
50
- // 6. Batch Operations
51
- await client.data.deleteMany('todo_task', ['id1', 'id2']);
54
+ // 6. Batch Operations (New!)
55
+ const batchResult = await client.data.batch('todo_task', {
56
+ operation: 'update',
57
+ records: [
58
+ { id: '1', data: { status: 'active' } },
59
+ { id: '2', data: { status: 'active' } }
60
+ ],
61
+ options: {
62
+ atomic: true, // Rollback on any failure
63
+ returnRecords: true // Include full records in response
64
+ }
65
+ });
66
+ console.log(`Updated ${batchResult.succeeded} records`);
67
+
68
+ // 7. Metadata Caching (New!)
69
+ const cachedObject = await client.meta.getCached('todo_task', {
70
+ ifNoneMatch: '"686897696a7c876b7e"' // ETag from previous request
71
+ });
72
+ if (cachedObject.notModified) {
73
+ console.log('Using cached metadata');
74
+ }
75
+
76
+ // 8. View Storage (New!)
77
+ const view = await client.views.create({
78
+ name: 'active_tasks',
79
+ label: 'Active Tasks',
80
+ object: 'todo_task',
81
+ type: 'list',
82
+ visibility: 'public',
83
+ query: {
84
+ object: 'todo_task',
85
+ where: { status: 'active' },
86
+ orderBy: [{ field: 'priority', order: 'desc' }],
87
+ limit: 50
88
+ },
89
+ layout: {
90
+ columns: [
91
+ { field: 'subject', label: 'Task', width: 200 },
92
+ { field: 'priority', label: 'Priority', width: 100 }
93
+ ]
94
+ }
95
+ });
52
96
  }
53
97
  ```
54
98
 
@@ -59,6 +103,7 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`.
59
103
 
60
104
  ### `client.meta`
61
105
  - `getObject(name: string)`: Get object schema.
106
+ - `getCached(name: string, options?)`: Get object schema with ETag-based caching.
62
107
  - `getView(name: string)`: Get view configuration.
63
108
 
64
109
  ### `client.data`
@@ -66,11 +111,21 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`.
66
111
  - `get<T>(object, id)`: Get single record by ID.
67
112
  - `query<T>(object, ast)`: Execute complex query using full AST.
68
113
  - `create<T>(object, data)`: Create record.
69
- - `createMany<T>(object, data[])`: Batch create records.
114
+ - `batch(object, request)`: **Recommended** - Execute batch operations (create/update/upsert/delete) with full control.
115
+ - `createMany<T>(object, data[])`: Batch create records (convenience method).
70
116
  - `update<T>(object, id, data)`: Update record.
71
- - `updateMany<T>(object, ids[], data)`: Batch update records.
117
+ - `updateMany<T>(object, records[], options?)`: Batch update records (convenience method).
72
118
  - `delete(object, id)`: Delete record.
73
- - `deleteMany(object, ids[])`: Batch delete records.
119
+ - `deleteMany(object, ids[], options?)`: Batch delete records (convenience method).
120
+
121
+ ### `client.views` (New!)
122
+ - `create(request)`: Create a new saved view.
123
+ - `get(id)`: Get a saved view by ID.
124
+ - `list(request?)`: List saved views with optional filters.
125
+ - `update(request)`: Update an existing view.
126
+ - `delete(id)`: Delete a saved view.
127
+ - `share(id, userIds[])`: Share a view with users/teams.
128
+ - `setDefault(id, object)`: Set a view as default for an object.
74
129
 
75
130
  ### Query Options
76
131
  The `find` method accepts an options object:
@@ -80,3 +135,32 @@ The `find` method accepts an options object:
80
135
  - `top`: Limit number of records.
81
136
  - `skip`: Offset for pagination.
82
137
 
138
+ ### Batch Options
139
+ Batch operations support the following options:
140
+ - `atomic`: If true, rollback entire batch on any failure (default: true).
141
+ - `returnRecords`: If true, return full record data in response (default: false).
142
+ - `continueOnError`: If true (and atomic=false), continue processing remaining records after errors.
143
+ - `validateOnly`: If true, validate records without persisting changes (dry-run mode).
144
+
145
+ ### Error Handling
146
+ The client provides standardized error handling with machine-readable error codes:
147
+
148
+ ```typescript
149
+ try {
150
+ await client.data.create('todo_task', { subject: '' });
151
+ } catch (error) {
152
+ console.error('Error code:', error.code); // e.g., 'validation_error'
153
+ console.error('Category:', error.category); // e.g., 'validation'
154
+ console.error('HTTP status:', error.httpStatus); // e.g., 400
155
+ console.error('Retryable:', error.retryable); // e.g., false
156
+ console.error('Details:', error.details); // Additional error info
157
+ }
158
+ ```
159
+
160
+ Common error codes:
161
+ - `validation_error`: Input validation failed
162
+ - `unauthenticated`: Authentication required
163
+ - `permission_denied`: Insufficient permissions
164
+ - `resource_not_found`: Resource does not exist
165
+ - `rate_limit_exceeded`: Too many requests
166
+
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { QueryAST, SortNode, AggregationNode } from '@objectstack/spec/data';
2
+ import { BatchUpdateRequest, BatchUpdateResponse, BatchOptions, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, CreateViewRequest, UpdateViewRequest, ListViewsRequest, ListViewsResponse, ViewResponse } from '@objectstack/spec/api';
3
+ import { Logger } from '@objectstack/core';
2
4
  export interface ClientConfig {
3
5
  baseUrl: string;
4
6
  token?: string;
@@ -6,6 +8,14 @@ export interface ClientConfig {
6
8
  * Custom fetch implementation (e.g. node-fetch or for Next.js caching)
7
9
  */
8
10
  fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
11
+ /**
12
+ * Logger instance for debugging
13
+ */
14
+ logger?: Logger;
15
+ /**
16
+ * Enable debug logging
17
+ */
18
+ debug?: boolean;
9
19
  }
10
20
  export interface DiscoveryResult {
11
21
  routes: {
@@ -30,11 +40,20 @@ export interface PaginatedResult<T = any> {
30
40
  value: T[];
31
41
  count: number;
32
42
  }
43
+ export interface StandardError {
44
+ code: StandardErrorCode;
45
+ message: string;
46
+ category: ErrorCategory;
47
+ httpStatus: number;
48
+ retryable: boolean;
49
+ details?: Record<string, any>;
50
+ }
33
51
  export declare class ObjectStackClient {
34
52
  private baseUrl;
35
53
  private token?;
36
54
  private fetchImpl;
37
55
  private routes?;
56
+ private logger;
38
57
  constructor(config: ClientConfig);
39
58
  /**
40
59
  * Initialize the client by discovering server capabilities and routes.
@@ -45,6 +64,11 @@ export declare class ObjectStackClient {
45
64
  */
46
65
  meta: {
47
66
  getObject: (name: string) => Promise<any>;
67
+ /**
68
+ * Get object metadata with cache support
69
+ * Supports ETag-based conditional requests for efficient caching
70
+ */
71
+ getCached: (name: string, cacheOptions?: MetadataCacheRequest) => Promise<MetadataCacheResponse>;
48
72
  getView: (object: string, type?: "list" | "form") => Promise<any>;
49
73
  };
50
74
  /**
@@ -61,13 +85,62 @@ export declare class ObjectStackClient {
61
85
  create: <T = any>(object: string, data: Partial<T>) => Promise<T>;
62
86
  createMany: <T = any>(object: string, data: Partial<T>[]) => Promise<T[]>;
63
87
  update: <T = any>(object: string, id: string, data: Partial<T>) => Promise<T>;
64
- updateMany: <T = any>(object: string, data: Partial<T>, filters?: Record<string, any> | any[]) => Promise<number>;
88
+ /**
89
+ * Batch update multiple records
90
+ * Uses the new BatchUpdateRequest schema with full control over options
91
+ */
92
+ batch: (object: string, request: BatchUpdateRequest) => Promise<BatchUpdateResponse>;
93
+ /**
94
+ * Update multiple records (simplified batch update)
95
+ * Convenience method for batch updates without full BatchUpdateRequest
96
+ */
97
+ updateMany: <T = any>(object: string, records: Array<{
98
+ id: string;
99
+ data: Partial<T>;
100
+ }>, options?: BatchOptions) => Promise<BatchUpdateResponse>;
65
101
  delete: (object: string, id: string) => Promise<{
66
102
  success: boolean;
67
103
  }>;
68
- deleteMany: (object: string, filters?: Record<string, any> | any[]) => Promise<{
69
- count: number;
104
+ /**
105
+ * Delete multiple records by IDs
106
+ */
107
+ deleteMany: (object: string, ids: string[], options?: BatchOptions) => Promise<BatchUpdateResponse>;
108
+ };
109
+ /**
110
+ * View Storage Operations
111
+ * Save, load, and manage UI view configurations
112
+ */
113
+ views: {
114
+ /**
115
+ * Create a new saved view
116
+ */
117
+ create: (request: CreateViewRequest) => Promise<ViewResponse>;
118
+ /**
119
+ * Get a saved view by ID
120
+ */
121
+ get: (id: string) => Promise<ViewResponse>;
122
+ /**
123
+ * List saved views with optional filters
124
+ */
125
+ list: (request?: ListViewsRequest) => Promise<ListViewsResponse>;
126
+ /**
127
+ * Update an existing view
128
+ */
129
+ update: (request: UpdateViewRequest) => Promise<ViewResponse>;
130
+ /**
131
+ * Delete a saved view
132
+ */
133
+ delete: (id: string) => Promise<{
134
+ success: boolean;
70
135
  }>;
136
+ /**
137
+ * Share a view with users/teams
138
+ */
139
+ share: (id: string, userIds: string[]) => Promise<ViewResponse>;
140
+ /**
141
+ * Set a view as default for an object
142
+ */
143
+ setDefault: (id: string, object: string) => Promise<ViewResponse>;
71
144
  };
72
145
  /**
73
146
  * Private Helpers
@@ -76,3 +149,5 @@ export declare class ObjectStackClient {
76
149
  private fetch;
77
150
  private getRoute;
78
151
  }
152
+ export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
153
+ export type { BatchUpdateRequest, BatchUpdateResponse, UpdateManyRequest, DeleteManyRequest, BatchOptions, BatchRecord, BatchOperationResult, MetadataCacheRequest, MetadataCacheResponse, StandardErrorCode, ErrorCategory, CreateViewRequest, UpdateViewRequest, ListViewsRequest, SavedView, ViewResponse, ListViewsResponse, ViewType, ViewVisibility, ViewColumn, ViewLayout } from '@objectstack/spec/api';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createLogger } from '@objectstack/core';
1
2
  export class ObjectStackClient {
2
3
  constructor(config) {
3
4
  /**
@@ -9,6 +10,45 @@ export class ObjectStackClient {
9
10
  const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
10
11
  return res.json();
11
12
  },
13
+ /**
14
+ * Get object metadata with cache support
15
+ * Supports ETag-based conditional requests for efficient caching
16
+ */
17
+ getCached: async (name, cacheOptions) => {
18
+ const route = this.getRoute('metadata');
19
+ const headers = {};
20
+ if (cacheOptions?.ifNoneMatch) {
21
+ headers['If-None-Match'] = cacheOptions.ifNoneMatch;
22
+ }
23
+ if (cacheOptions?.ifModifiedSince) {
24
+ headers['If-Modified-Since'] = cacheOptions.ifModifiedSince;
25
+ }
26
+ const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`, {
27
+ headers
28
+ });
29
+ // Check for 304 Not Modified
30
+ if (res.status === 304) {
31
+ return {
32
+ notModified: true,
33
+ etag: cacheOptions?.ifNoneMatch ? {
34
+ value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''),
35
+ weak: cacheOptions.ifNoneMatch.startsWith('W/')
36
+ } : undefined
37
+ };
38
+ }
39
+ const data = await res.json();
40
+ const etag = res.headers.get('ETag');
41
+ const lastModified = res.headers.get('Last-Modified');
42
+ return {
43
+ data,
44
+ etag: etag ? {
45
+ value: etag.replace(/^W\/|"/g, ''),
46
+ weak: etag.startsWith('W/')
47
+ } : undefined,
48
+ lastModified: lastModified || undefined,
49
+ notModified: false
50
+ };
51
+ },
12
52
  getView: async (object, type = 'list') => {
13
53
  const route = this.getRoute('ui');
14
54
  const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
@@ -96,7 +136,7 @@ export class ObjectStackClient {
96
136
  },
97
137
  createMany: async (object, data) => {
98
138
  const route = this.getRoute('data');
99
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
139
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
100
140
  method: 'POST',
101
141
  body: JSON.stringify(data)
102
142
  });
@@ -110,13 +150,33 @@ export class ObjectStackClient {
110
150
  });
111
151
  return res.json();
112
152
  },
113
- updateMany: async (object, data, filters) => {
153
+ /**
154
+ * Batch update multiple records
155
+ * Uses the new BatchUpdateRequest schema with full control over options
156
+ */
157
+ batch: async (object, request) => {
114
158
  const route = this.getRoute('data');
115
159
  const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
116
- method: 'PATCH',
117
- body: JSON.stringify({ data, filters })
160
+ method: 'POST',
161
+ body: JSON.stringify(request)
118
162
  });
119
- return res.json(); // Returns count
163
+ return res.json();
164
+ },
165
+ /**
166
+ * Update multiple records (simplified batch update)
167
+ * Convenience method for batch updates without full BatchUpdateRequest
168
+ */
169
+ updateMany: async (object, records, options) => {
170
+ const route = this.getRoute('data');
171
+ const request = {
172
+ records,
173
+ options
174
+ };
175
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
176
+ method: 'POST',
177
+ body: JSON.stringify(request)
178
+ });
179
+ return res.json();
120
180
  },
121
181
  delete: async (object, id) => {
122
182
  const route = this.getRoute('data');
@@ -125,11 +185,113 @@ export class ObjectStackClient {
125
185
  });
126
186
  return res.json();
127
187
  },
128
- deleteMany: async (object, filters) => {
188
+ /**
189
+ * Delete multiple records by IDs
190
+ */
191
+ deleteMany: async (object, ids, options) => {
129
192
  const route = this.getRoute('data');
130
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
131
- method: 'DELETE',
132
- body: JSON.stringify({ filters })
193
+ const request = {
194
+ ids,
195
+ options
196
+ };
197
+ const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
198
+ method: 'POST',
199
+ body: JSON.stringify(request)
200
+ });
201
+ return res.json();
202
+ }
203
+ };
204
+ /**
205
+ * View Storage Operations
206
+ * Save, load, and manage UI view configurations
207
+ */
208
+ this.views = {
209
+ /**
210
+ * Create a new saved view
211
+ */
212
+ create: async (request) => {
213
+ const route = this.getRoute('ui');
214
+ const res = await this.fetch(`${this.baseUrl}${route}/views`, {
215
+ method: 'POST',
216
+ body: JSON.stringify(request)
217
+ });
218
+ return res.json();
219
+ },
220
+ /**
221
+ * Get a saved view by ID
222
+ */
223
+ get: async (id) => {
224
+ const route = this.getRoute('ui');
225
+ const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`);
226
+ return res.json();
227
+ },
228
+ /**
229
+ * List saved views with optional filters
230
+ */
231
+ list: async (request) => {
232
+ const route = this.getRoute('ui');
233
+ const queryParams = new URLSearchParams();
234
+ if (request?.object)
235
+ queryParams.set('object', request.object);
236
+ if (request?.type)
237
+ queryParams.set('type', request.type);
238
+ if (request?.visibility)
239
+ queryParams.set('visibility', request.visibility);
240
+ if (request?.createdBy)
241
+ queryParams.set('createdBy', request.createdBy);
242
+ if (request?.isDefault !== undefined)
243
+ queryParams.set('isDefault', String(request.isDefault));
244
+ if (request?.limit)
245
+ queryParams.set('limit', String(request.limit));
246
+ if (request?.offset)
247
+ queryParams.set('offset', String(request.offset));
248
+ const url = queryParams.toString()
249
+ ? `${this.baseUrl}${route}/views?${queryParams.toString()}`
250
+ : `${this.baseUrl}${route}/views`;
251
+ const res = await this.fetch(url);
252
+ return res.json();
253
+ },
254
+ /**
255
+ * Update an existing view
256
+ */
257
+ update: async (request) => {
258
+ const route = this.getRoute('ui');
259
+ const { id, ...updateData } = request;
260
+ const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`, {
261
+ method: 'PATCH',
262
+ body: JSON.stringify(updateData)
263
+ });
264
+ return res.json();
265
+ },
266
+ /**
267
+ * Delete a saved view
268
+ */
269
+ delete: async (id) => {
270
+ const route = this.getRoute('ui');
271
+ const res = await this.fetch(`${this.baseUrl}${route}/views/${id}`, {
272
+ method: 'DELETE'
273
+ });
274
+ return res.json();
275
+ },
276
+ /**
277
+ * Share a view with users/teams
278
+ */
279
+ share: async (id, userIds) => {
280
+ const route = this.getRoute('ui');
281
+ const res = await this.fetch(`${this.baseUrl}${route}/views/${id}/share`, {
282
+ method: 'POST',
283
+ body: JSON.stringify({ sharedWith: userIds })
284
+ });
285
+ return res.json();
286
+ },
287
+ /**
288
+ * Set a view as default for an object
289
+ */
290
+ setDefault: async (id, object) => {
291
+ const route = this.getRoute('ui');
292
+ const res = await this.fetch(`${this.baseUrl}${route}/views/${id}/set-default`, {
293
+ method: 'POST',
294
+ body: JSON.stringify({ object })
133
295
  });
134
296
  return res.json();
135
297
  }
@@ -137,21 +299,32 @@ export class ObjectStackClient {
137
299
  this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
138
300
  this.token = config.token;
139
301
  this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
302
+ // Initialize logger
303
+ this.logger = config.logger || createLogger({
304
+ level: config.debug ? 'debug' : 'info',
305
+ format: 'pretty'
306
+ });
307
+ this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
140
308
  }
141
309
  /**
142
310
  * Initialize the client by discovering server capabilities and routes.
143
311
  */
144
312
  async connect() {
313
+ this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
145
314
  try {
146
315
  // Connect to the discovery endpoint
147
316
  // During boot, we might not know routes, so we check convention /api/v1 first
148
317
  const res = await this.fetch(`${this.baseUrl}/api/v1`);
149
318
  const data = await res.json();
150
319
  this.routes = data.routes;
320
+ this.logger.info('Connected to ObjectStack server', {
321
+ routes: Object.keys(data.routes || {}),
322
+ capabilities: data.capabilities
323
+ });
151
324
  return data;
152
325
  }
153
326
  catch (e) {
154
- console.error('Failed to connect to ObjectStack Server', e);
327
+ this.logger.error('Failed to connect to ObjectStack server', e, { baseUrl: this.baseUrl });
155
328
  throw e;
156
329
  }
157
330
  }
@@ -165,6 +338,11 @@ export class ObjectStackClient {
165
338
  return Array.isArray(filter);
166
339
  }
167
340
  async fetch(url, options = {}) {
341
+ this.logger.debug('HTTP request', {
342
+ method: options.method || 'GET',
343
+ url,
344
+ hasBody: !!options.body
345
+ });
168
346
  const headers = {
169
347
  'Content-Type': 'application/json',
170
348
  ...(options.headers || {}),
@@ -173,6 +351,12 @@ export class ObjectStackClient {
173
351
  headers['Authorization'] = `Bearer ${this.token}`;
174
352
  }
175
353
  const res = await this.fetchImpl(url, { ...options, headers });
354
+ this.logger.debug('HTTP response', {
355
+ method: options.method || 'GET',
356
+ url,
357
+ status: res.status,
358
+ ok: res.ok
359
+ });
176
360
  if (!res.ok) {
177
361
  let errorBody;
178
362
  try {
@@ -181,16 +365,37 @@ export class ObjectStackClient {
181
365
  catch {
182
366
  errorBody = { message: res.statusText };
183
367
  }
184
- throw new Error(`[ObjectStack] Request failed: ${res.status} ${JSON.stringify(errorBody)}`);
368
+ this.logger.error('HTTP request failed', undefined, {
369
+ method: options.method || 'GET',
370
+ url,
371
+ status: res.status,
372
+ error: errorBody
373
+ });
374
+ // Create a standardized error if the response includes error details
375
+ const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
376
+ const errorCode = errorBody?.code || errorBody?.error?.code;
377
+ const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ''}${errorMessage}`);
378
+ // Attach error details for programmatic access
379
+ error.code = errorCode;
380
+ error.category = errorBody?.category;
381
+ error.httpStatus = res.status;
382
+ error.retryable = errorBody?.retryable;
383
+ error.details = errorBody?.details || errorBody;
384
+ throw error;
185
385
  }
186
386
  return res;
187
387
  }
188
388
  getRoute(key) {
189
389
  if (!this.routes) {
190
390
  // Fallback for strictness, but we allow bootstrapping
191
- console.warn(`[ObjectStackClient] Accessing ${key} route before connect(). Using default /api/v1/${key}`);
391
+ this.logger.warn('Accessing route before connect()', {
392
+ route: key,
393
+ fallback: `/api/v1/${key}`
394
+ });
192
395
  return `/api/v1/${key}`;
193
396
  }
194
397
  return this.routes[key] || `/api/v1/${key}`;
195
398
  }
196
399
  }
400
+ // Re-export type-safe query builder
401
+ export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';