@objectql/sdk 3.0.1 → 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/README.md +273 -1
- package/dist/index.d.ts +223 -5
- package/dist/index.js +445 -7
- package/dist/index.js.map +1 -1
- package/jest.config.js +8 -0
- package/package.json +3 -2
- package/src/index.ts +552 -7
- package/test/remote-driver.test.ts +454 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* @objectql/sdk - Universal HTTP Client for ObjectQL
|
|
3
11
|
*
|
|
@@ -40,8 +48,65 @@ import {
|
|
|
40
48
|
MetadataApiResponse,
|
|
41
49
|
ObjectQLError,
|
|
42
50
|
ApiErrorCode,
|
|
43
|
-
|
|
51
|
+
Filter
|
|
44
52
|
} from '@objectql/types';
|
|
53
|
+
import { DriverInterface, QueryAST } from '@objectstack/spec';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Command interface for executeCommand method
|
|
57
|
+
* Defines the structure of mutation commands sent to the remote API
|
|
58
|
+
*/
|
|
59
|
+
export interface Command {
|
|
60
|
+
type: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
|
|
61
|
+
object: string;
|
|
62
|
+
data?: any;
|
|
63
|
+
id?: string | number;
|
|
64
|
+
ids?: Array<string | number>;
|
|
65
|
+
records?: any[];
|
|
66
|
+
updates?: Array<{id: string | number, data: any}>;
|
|
67
|
+
options?: any;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Command result interface
|
|
72
|
+
* Standard response format for command execution
|
|
73
|
+
*/
|
|
74
|
+
export interface CommandResult {
|
|
75
|
+
success: boolean;
|
|
76
|
+
data?: any;
|
|
77
|
+
affected: number;
|
|
78
|
+
error?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* SDK Configuration for the RemoteDriver
|
|
83
|
+
*/
|
|
84
|
+
export interface SdkConfig {
|
|
85
|
+
/** Base URL of the remote ObjectQL server */
|
|
86
|
+
baseUrl: string;
|
|
87
|
+
/** RPC endpoint path (default: /api/objectql) */
|
|
88
|
+
rpcPath?: string;
|
|
89
|
+
/** Query endpoint path (default: /api/query) */
|
|
90
|
+
queryPath?: string;
|
|
91
|
+
/** Command endpoint path (default: /api/command) */
|
|
92
|
+
commandPath?: string;
|
|
93
|
+
/** Custom execute endpoint path (default: /api/execute) */
|
|
94
|
+
executePath?: string;
|
|
95
|
+
/** Authentication token */
|
|
96
|
+
token?: string;
|
|
97
|
+
/** API key for authentication */
|
|
98
|
+
apiKey?: string;
|
|
99
|
+
/** Custom headers */
|
|
100
|
+
headers?: Record<string, string>;
|
|
101
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
102
|
+
timeout?: number;
|
|
103
|
+
/** Enable retry on failure (default: false) */
|
|
104
|
+
enableRetry?: boolean;
|
|
105
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
106
|
+
maxRetries?: number;
|
|
107
|
+
/** Enable request/response logging (default: false) */
|
|
108
|
+
enableLogging?: boolean;
|
|
109
|
+
}
|
|
45
110
|
|
|
46
111
|
/**
|
|
47
112
|
* Polyfill for AbortSignal.timeout if not available (for older browsers)
|
|
@@ -70,12 +135,457 @@ function createTimeoutSignal(ms: number): AbortSignal {
|
|
|
70
135
|
|
|
71
136
|
/**
|
|
72
137
|
* Legacy Driver implementation that uses JSON-RPC style API
|
|
138
|
+
*
|
|
139
|
+
* Implements both the legacy Driver interface from @objectql/types and
|
|
140
|
+
* the standard DriverInterface from @objectstack/spec for compatibility
|
|
141
|
+
* with the new kernel-based plugin system.
|
|
142
|
+
*
|
|
143
|
+
* @version 4.0.0 - DriverInterface compliant
|
|
73
144
|
*/
|
|
74
|
-
export class RemoteDriver implements Driver {
|
|
145
|
+
export class RemoteDriver implements Driver, DriverInterface {
|
|
146
|
+
// Driver metadata (ObjectStack-compatible)
|
|
147
|
+
public readonly name = 'RemoteDriver';
|
|
148
|
+
public readonly version = '4.0.0';
|
|
149
|
+
public readonly supports = {
|
|
150
|
+
transactions: false,
|
|
151
|
+
joins: false,
|
|
152
|
+
fullTextSearch: false,
|
|
153
|
+
jsonFields: true,
|
|
154
|
+
arrayFields: true
|
|
155
|
+
};
|
|
156
|
+
|
|
75
157
|
private rpcPath: string;
|
|
158
|
+
private queryPath: string;
|
|
159
|
+
private commandPath: string;
|
|
160
|
+
private executePath: string;
|
|
161
|
+
private baseUrl: string;
|
|
162
|
+
private token?: string;
|
|
163
|
+
private apiKey?: string;
|
|
164
|
+
private headers: Record<string, string>;
|
|
165
|
+
private timeout: number;
|
|
166
|
+
private enableRetry: boolean;
|
|
167
|
+
private maxRetries: number;
|
|
168
|
+
private enableLogging: boolean;
|
|
76
169
|
|
|
77
|
-
constructor(
|
|
78
|
-
|
|
170
|
+
constructor(baseUrlOrConfig: string | SdkConfig, rpcPath?: string) {
|
|
171
|
+
if (typeof baseUrlOrConfig === 'string') {
|
|
172
|
+
// Legacy constructor signature
|
|
173
|
+
this.baseUrl = baseUrlOrConfig;
|
|
174
|
+
this.rpcPath = rpcPath || '/api/objectql';
|
|
175
|
+
this.queryPath = '/api/query';
|
|
176
|
+
this.commandPath = '/api/command';
|
|
177
|
+
this.executePath = '/api/execute';
|
|
178
|
+
this.headers = {};
|
|
179
|
+
this.timeout = 30000;
|
|
180
|
+
this.enableRetry = false;
|
|
181
|
+
this.maxRetries = 3;
|
|
182
|
+
this.enableLogging = false;
|
|
183
|
+
} else {
|
|
184
|
+
// New config-based constructor
|
|
185
|
+
const config = baseUrlOrConfig;
|
|
186
|
+
this.baseUrl = config.baseUrl;
|
|
187
|
+
this.rpcPath = config.rpcPath || '/api/objectql';
|
|
188
|
+
this.queryPath = config.queryPath || '/api/query';
|
|
189
|
+
this.commandPath = config.commandPath || '/api/command';
|
|
190
|
+
this.executePath = config.executePath || '/api/execute';
|
|
191
|
+
this.token = config.token;
|
|
192
|
+
this.apiKey = config.apiKey;
|
|
193
|
+
this.headers = config.headers || {};
|
|
194
|
+
this.timeout = config.timeout || 30000;
|
|
195
|
+
this.enableRetry = config.enableRetry || false;
|
|
196
|
+
this.maxRetries = config.maxRetries || 3;
|
|
197
|
+
this.enableLogging = config.enableLogging || false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build full endpoint URL
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
private buildEndpoint(path: string): string {
|
|
206
|
+
return `${this.baseUrl.replace(/\/$/, '')}${path}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get authentication headers
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
private getAuthHeaders(): Record<string, string> {
|
|
214
|
+
const authHeaders: Record<string, string> = {};
|
|
215
|
+
|
|
216
|
+
if (this.token) {
|
|
217
|
+
authHeaders['Authorization'] = `Bearer ${this.token}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (this.apiKey) {
|
|
221
|
+
authHeaders['X-API-Key'] = this.apiKey;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return authHeaders;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handle HTTP errors and convert to ObjectQLError
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
private async handleHttpError(response: Response): Promise<never> {
|
|
232
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
233
|
+
let errorCode: ApiErrorCode = ApiErrorCode.INTERNAL_ERROR;
|
|
234
|
+
let errorDetails: any = undefined;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const json = await response.json();
|
|
238
|
+
if (json.error) {
|
|
239
|
+
errorMessage = json.error.message || errorMessage;
|
|
240
|
+
errorCode = json.error.code || errorCode;
|
|
241
|
+
errorDetails = json.error.details;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Could not parse JSON, use default error message
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Map HTTP status codes to ObjectQL error codes
|
|
248
|
+
if (response.status === 401) {
|
|
249
|
+
errorCode = ApiErrorCode.UNAUTHORIZED;
|
|
250
|
+
} else if (response.status === 403) {
|
|
251
|
+
errorCode = ApiErrorCode.FORBIDDEN;
|
|
252
|
+
} else if (response.status === 404) {
|
|
253
|
+
errorCode = ApiErrorCode.NOT_FOUND;
|
|
254
|
+
} else if (response.status === 400) {
|
|
255
|
+
errorCode = ApiErrorCode.VALIDATION_ERROR;
|
|
256
|
+
} else if (response.status >= 500) {
|
|
257
|
+
errorCode = ApiErrorCode.INTERNAL_ERROR;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new ObjectQLError({
|
|
261
|
+
code: errorCode,
|
|
262
|
+
message: errorMessage,
|
|
263
|
+
details: errorDetails
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Retry logic with exponential backoff
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
private async retryWithBackoff<T>(
|
|
272
|
+
fn: () => Promise<T>,
|
|
273
|
+
attempt: number = 0
|
|
274
|
+
): Promise<T> {
|
|
275
|
+
let currentAttempt = attempt;
|
|
276
|
+
|
|
277
|
+
while (true) {
|
|
278
|
+
try {
|
|
279
|
+
return await fn();
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
// Don't retry on client errors (4xx) or if retries are disabled
|
|
282
|
+
if (!this.enableRetry || currentAttempt >= this.maxRetries) {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Don't retry on validation or auth errors
|
|
287
|
+
if (error instanceof ObjectQLError) {
|
|
288
|
+
const nonRetryableCodes = [
|
|
289
|
+
ApiErrorCode.VALIDATION_ERROR,
|
|
290
|
+
ApiErrorCode.UNAUTHORIZED,
|
|
291
|
+
ApiErrorCode.FORBIDDEN,
|
|
292
|
+
ApiErrorCode.NOT_FOUND
|
|
293
|
+
];
|
|
294
|
+
if (nonRetryableCodes.includes(error.code as ApiErrorCode)) {
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Calculate exponential backoff delay
|
|
300
|
+
const delay = Math.min(1000 * Math.pow(2, currentAttempt), 10000);
|
|
301
|
+
|
|
302
|
+
if (this.enableLogging) {
|
|
303
|
+
console.log(`Retry attempt ${currentAttempt + 1}/${this.maxRetries} after ${delay}ms delay`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
307
|
+
currentAttempt++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Log request/response if logging is enabled
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
private log(message: string, data?: any): void {
|
|
317
|
+
if (this.enableLogging) {
|
|
318
|
+
console.log(`[RemoteDriver] ${message}`, data || '');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Connect to the remote server (for DriverInterface compatibility)
|
|
324
|
+
*/
|
|
325
|
+
async connect(): Promise<void> {
|
|
326
|
+
// Test connection with a simple health check
|
|
327
|
+
try {
|
|
328
|
+
await this.checkHealth();
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(`Failed to connect to remote server: ${(error as Error).message}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check remote server connection health
|
|
336
|
+
*/
|
|
337
|
+
async checkHealth(): Promise<boolean> {
|
|
338
|
+
try {
|
|
339
|
+
const endpoint = `${this.baseUrl.replace(/\/$/, '')}${this.rpcPath}`;
|
|
340
|
+
const res = await fetch(endpoint, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: {
|
|
343
|
+
'Content-Type': 'application/json'
|
|
344
|
+
},
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
op: 'ping',
|
|
347
|
+
object: '_health',
|
|
348
|
+
args: {}
|
|
349
|
+
})
|
|
350
|
+
});
|
|
351
|
+
return res.ok;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Execute a query using QueryAST format (DriverInterface v4.0)
|
|
359
|
+
*
|
|
360
|
+
* Sends a QueryAST to the remote server's /api/query endpoint
|
|
361
|
+
* and returns the query results.
|
|
362
|
+
*
|
|
363
|
+
* @param ast - The QueryAST to execute
|
|
364
|
+
* @param options - Optional execution options
|
|
365
|
+
* @returns Query result with value array and optional count
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const result = await driver.executeQuery({
|
|
370
|
+
* object: 'users',
|
|
371
|
+
* fields: ['name', 'email'],
|
|
372
|
+
* filters: {
|
|
373
|
+
* type: 'comparison',
|
|
374
|
+
* field: 'status',
|
|
375
|
+
* operator: '=',
|
|
376
|
+
* value: 'active'
|
|
377
|
+
* },
|
|
378
|
+
* sort: [{ field: 'created_at', order: 'desc' }],
|
|
379
|
+
* top: 10
|
|
380
|
+
* });
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
|
|
384
|
+
return this.retryWithBackoff(async () => {
|
|
385
|
+
const endpoint = this.buildEndpoint(this.queryPath);
|
|
386
|
+
this.log('executeQuery', { endpoint, ast });
|
|
387
|
+
|
|
388
|
+
const headers: Record<string, string> = {
|
|
389
|
+
'Content-Type': 'application/json',
|
|
390
|
+
...this.getAuthHeaders(),
|
|
391
|
+
...this.headers
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const response = await fetch(endpoint, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers,
|
|
397
|
+
body: JSON.stringify(ast),
|
|
398
|
+
signal: createTimeoutSignal(this.timeout)
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
await this.handleHttpError(response);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const json = await response.json();
|
|
406
|
+
this.log('executeQuery response', json);
|
|
407
|
+
|
|
408
|
+
// Handle both direct data response and wrapped response formats
|
|
409
|
+
if (json.error) {
|
|
410
|
+
throw new ObjectQLError({
|
|
411
|
+
code: json.error.code || ApiErrorCode.INTERNAL_ERROR,
|
|
412
|
+
message: json.error.message,
|
|
413
|
+
details: json.error.details
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Support multiple response formats
|
|
418
|
+
if (json.value !== undefined) {
|
|
419
|
+
return {
|
|
420
|
+
value: Array.isArray(json.value) ? json.value : [json.value],
|
|
421
|
+
count: json.count
|
|
422
|
+
};
|
|
423
|
+
} else if (json.data !== undefined) {
|
|
424
|
+
return {
|
|
425
|
+
value: Array.isArray(json.data) ? json.data : [json.data],
|
|
426
|
+
count: json.count || (Array.isArray(json.data) ? json.data.length : 1)
|
|
427
|
+
};
|
|
428
|
+
} else if (Array.isArray(json)) {
|
|
429
|
+
return {
|
|
430
|
+
value: json,
|
|
431
|
+
count: json.length
|
|
432
|
+
};
|
|
433
|
+
} else {
|
|
434
|
+
return {
|
|
435
|
+
value: [json],
|
|
436
|
+
count: 1
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Execute a command using Command format (DriverInterface v4.0)
|
|
444
|
+
*
|
|
445
|
+
* Sends a Command to the remote server's /api/command endpoint
|
|
446
|
+
* for mutation operations (create, update, delete, bulk operations).
|
|
447
|
+
*
|
|
448
|
+
* @param command - The command to execute
|
|
449
|
+
* @param options - Optional execution options
|
|
450
|
+
* @returns Command execution result
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```typescript
|
|
454
|
+
* // Create a record
|
|
455
|
+
* const result = await driver.executeCommand({
|
|
456
|
+
* type: 'create',
|
|
457
|
+
* object: 'users',
|
|
458
|
+
* data: { name: 'Alice', email: 'alice@example.com' }
|
|
459
|
+
* });
|
|
460
|
+
*
|
|
461
|
+
* // Bulk update
|
|
462
|
+
* const bulkResult = await driver.executeCommand({
|
|
463
|
+
* type: 'bulkUpdate',
|
|
464
|
+
* object: 'users',
|
|
465
|
+
* updates: [
|
|
466
|
+
* { id: '1', data: { status: 'active' } },
|
|
467
|
+
* { id: '2', data: { status: 'inactive' } }
|
|
468
|
+
* ]
|
|
469
|
+
* });
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
async executeCommand(command: Command, options?: any): Promise<CommandResult> {
|
|
473
|
+
return this.retryWithBackoff(async () => {
|
|
474
|
+
const endpoint = this.buildEndpoint(this.commandPath);
|
|
475
|
+
this.log('executeCommand', { endpoint, command });
|
|
476
|
+
|
|
477
|
+
const headers: Record<string, string> = {
|
|
478
|
+
'Content-Type': 'application/json',
|
|
479
|
+
...this.getAuthHeaders(),
|
|
480
|
+
...this.headers
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const response = await fetch(endpoint, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers,
|
|
486
|
+
body: JSON.stringify(command),
|
|
487
|
+
signal: createTimeoutSignal(this.timeout)
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
await this.handleHttpError(response);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const json = await response.json();
|
|
495
|
+
this.log('executeCommand response', json);
|
|
496
|
+
|
|
497
|
+
// Handle error response
|
|
498
|
+
if (json.error) {
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
error: json.error.message || 'Command execution failed',
|
|
502
|
+
affected: 0
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Handle standard CommandResult format
|
|
507
|
+
if (json.success !== undefined) {
|
|
508
|
+
return {
|
|
509
|
+
success: json.success,
|
|
510
|
+
data: json.data,
|
|
511
|
+
affected: json.affected || 0,
|
|
512
|
+
error: json.error
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Handle legacy response formats
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
data: json.data || json,
|
|
520
|
+
affected: json.affected || 1
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Execute a custom operation on the remote server
|
|
527
|
+
*
|
|
528
|
+
* Allows calling custom HTTP endpoints with flexible parameters.
|
|
529
|
+
* Useful for custom actions, workflows, or specialized operations.
|
|
530
|
+
*
|
|
531
|
+
* @param endpoint - Optional custom endpoint path (defaults to /api/execute)
|
|
532
|
+
* @param payload - Request payload
|
|
533
|
+
* @param options - Optional execution options
|
|
534
|
+
* @returns Execution result
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```typescript
|
|
538
|
+
* // Execute a custom workflow
|
|
539
|
+
* const result = await driver.execute('/api/workflows/approve', {
|
|
540
|
+
* workflowId: 'wf_123',
|
|
541
|
+
* comment: 'Approved'
|
|
542
|
+
* });
|
|
543
|
+
*
|
|
544
|
+
* // Use default execute endpoint
|
|
545
|
+
* const result = await driver.execute(undefined, {
|
|
546
|
+
* action: 'calculateMetrics',
|
|
547
|
+
* params: { year: 2024 }
|
|
548
|
+
* });
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
async execute(endpoint?: string, payload?: any, options?: any): Promise<any> {
|
|
552
|
+
return this.retryWithBackoff(async () => {
|
|
553
|
+
const targetEndpoint = endpoint
|
|
554
|
+
? this.buildEndpoint(endpoint)
|
|
555
|
+
: this.buildEndpoint(this.executePath);
|
|
556
|
+
|
|
557
|
+
this.log('execute', { endpoint: targetEndpoint, payload });
|
|
558
|
+
|
|
559
|
+
const headers: Record<string, string> = {
|
|
560
|
+
'Content-Type': 'application/json',
|
|
561
|
+
...this.getAuthHeaders(),
|
|
562
|
+
...this.headers
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const response = await fetch(targetEndpoint, {
|
|
566
|
+
method: 'POST',
|
|
567
|
+
headers,
|
|
568
|
+
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
|
569
|
+
signal: createTimeoutSignal(this.timeout)
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (!response.ok) {
|
|
573
|
+
await this.handleHttpError(response);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const json = await response.json();
|
|
577
|
+
this.log('execute response', json);
|
|
578
|
+
|
|
579
|
+
if (json.error) {
|
|
580
|
+
throw new ObjectQLError({
|
|
581
|
+
code: json.error.code || ApiErrorCode.INTERNAL_ERROR,
|
|
582
|
+
message: json.error.message,
|
|
583
|
+
details: json.error.details
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return json;
|
|
588
|
+
});
|
|
79
589
|
}
|
|
80
590
|
|
|
81
591
|
private async request(op: string, objectName: string, args: any) {
|
|
@@ -104,12 +614,47 @@ export class RemoteDriver implements Driver {
|
|
|
104
614
|
return json.data;
|
|
105
615
|
}
|
|
106
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
|
|
619
|
+
* This ensures backward compatibility while supporting the new @objectstack/spec interface.
|
|
620
|
+
*
|
|
621
|
+
* QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
|
|
622
|
+
* QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
|
|
623
|
+
*/
|
|
624
|
+
private normalizeQuery(query: any): any {
|
|
625
|
+
if (!query) return {};
|
|
626
|
+
|
|
627
|
+
const normalized: any = { ...query };
|
|
628
|
+
|
|
629
|
+
// Normalize limit/top
|
|
630
|
+
if (normalized.top !== undefined && normalized.limit === undefined) {
|
|
631
|
+
normalized.limit = normalized.top;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Normalize sort format
|
|
635
|
+
if (normalized.sort && Array.isArray(normalized.sort)) {
|
|
636
|
+
// Check if it's already in the array format [field, order]
|
|
637
|
+
const firstSort = normalized.sort[0];
|
|
638
|
+
if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
|
|
639
|
+
// Convert from QueryAST format {field, order} to internal format [field, order]
|
|
640
|
+
normalized.sort = normalized.sort.map((item: any) => [
|
|
641
|
+
item.field,
|
|
642
|
+
item.order || item.direction || item.dir || 'asc'
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return normalized;
|
|
648
|
+
}
|
|
649
|
+
|
|
107
650
|
async find(objectName: string, query: any, options?: any): Promise<any[]> {
|
|
108
|
-
|
|
651
|
+
const normalizedQuery = this.normalizeQuery(query);
|
|
652
|
+
return this.request('find', objectName, normalizedQuery);
|
|
109
653
|
}
|
|
110
654
|
|
|
111
655
|
async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise<any> {
|
|
112
|
-
|
|
656
|
+
const normalizedQuery = query ? this.normalizeQuery(query) : undefined;
|
|
657
|
+
return this.request('findOne', objectName, { id, query: normalizedQuery }); // Note: args format must match server expectation
|
|
113
658
|
}
|
|
114
659
|
|
|
115
660
|
async create(objectName: string, data: any, options?: any): Promise<any> {
|
|
@@ -296,7 +841,7 @@ export class DataApiClient implements IDataApiClient {
|
|
|
296
841
|
);
|
|
297
842
|
}
|
|
298
843
|
|
|
299
|
-
async count(objectName: string, filters?:
|
|
844
|
+
async count(objectName: string, filters?: Filter): Promise<DataApiCountResponse> {
|
|
300
845
|
return this.request<DataApiCountResponse>(
|
|
301
846
|
'GET',
|
|
302
847
|
`${this.dataPath}/${objectName}`,
|