@object-ui/data-objectstack 0.3.1 → 2.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 +317 -1
- package/dist/index.cjs +814 -39
- package/dist/index.d.cts +337 -8
- package/dist/index.d.ts +337 -8
- package/dist/index.js +804 -38
- package/package.json +5 -5
- package/src/cache/MetadataCache.test.ts +426 -0
- package/src/cache/MetadataCache.ts +229 -0
- package/src/connection.test.ts +141 -0
- package/src/errors.test.ts +426 -0
- package/src/errors.ts +275 -0
- package/src/index.ts +679 -48
- package/src/upload.test.ts +112 -0
package/dist/index.js
CHANGED
|
@@ -5,11 +5,355 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { ObjectStackClient } from "@objectstack/client";
|
|
7
7
|
import { convertFiltersToAST } from "@object-ui/core";
|
|
8
|
+
|
|
9
|
+
// src/cache/MetadataCache.ts
|
|
10
|
+
var MetadataCache = class {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new MetadataCache instance
|
|
13
|
+
*
|
|
14
|
+
* @param options - Configuration options
|
|
15
|
+
* @param options.maxSize - Maximum number of entries (default: 100)
|
|
16
|
+
* @param options.ttl - Time to live in milliseconds (default: 5 minutes)
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
__publicField(this, "cache");
|
|
20
|
+
__publicField(this, "maxSize");
|
|
21
|
+
__publicField(this, "ttl");
|
|
22
|
+
__publicField(this, "stats");
|
|
23
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
24
|
+
this.maxSize = options.maxSize || 100;
|
|
25
|
+
this.ttl = options.ttl || 5 * 60 * 1e3;
|
|
26
|
+
this.stats = {
|
|
27
|
+
hits: 0,
|
|
28
|
+
misses: 0,
|
|
29
|
+
evictions: 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get a value from cache or fetch it using the provided fetcher function
|
|
34
|
+
*
|
|
35
|
+
* @param key - Cache key
|
|
36
|
+
* @param fetcher - Async function to fetch data if not in cache
|
|
37
|
+
* @returns Promise resolving to the cached or fetched data
|
|
38
|
+
*/
|
|
39
|
+
async get(key, fetcher) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const cached = this.cache.get(key);
|
|
42
|
+
if (cached) {
|
|
43
|
+
const age = now - cached.timestamp;
|
|
44
|
+
if (age < this.ttl) {
|
|
45
|
+
cached.accessCount++;
|
|
46
|
+
cached.lastAccessed = now;
|
|
47
|
+
this.stats.hits++;
|
|
48
|
+
this.cache.delete(key);
|
|
49
|
+
this.cache.set(key, cached);
|
|
50
|
+
return cached.data;
|
|
51
|
+
} else {
|
|
52
|
+
this.cache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.stats.misses++;
|
|
56
|
+
const data = await fetcher();
|
|
57
|
+
this.set(key, data);
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Set a value in the cache
|
|
62
|
+
*
|
|
63
|
+
* @param key - Cache key
|
|
64
|
+
* @param data - Data to cache
|
|
65
|
+
*/
|
|
66
|
+
set(key, data) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
69
|
+
this.evictLRU();
|
|
70
|
+
}
|
|
71
|
+
this.cache.set(key, {
|
|
72
|
+
data,
|
|
73
|
+
timestamp: now,
|
|
74
|
+
accessCount: 1,
|
|
75
|
+
lastAccessed: now
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Evict the least recently used entry
|
|
80
|
+
*/
|
|
81
|
+
evictLRU() {
|
|
82
|
+
const firstKey = this.cache.keys().next().value;
|
|
83
|
+
if (firstKey !== void 0) {
|
|
84
|
+
this.cache.delete(firstKey);
|
|
85
|
+
this.stats.evictions++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Invalidate a specific cache entry or all entries
|
|
90
|
+
*
|
|
91
|
+
* @param key - Optional key to invalidate. If omitted, invalidates all entries
|
|
92
|
+
*/
|
|
93
|
+
invalidate(key) {
|
|
94
|
+
if (key) {
|
|
95
|
+
this.cache.delete(key);
|
|
96
|
+
} else {
|
|
97
|
+
this.cache.clear();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all cache entries and reset statistics
|
|
102
|
+
*/
|
|
103
|
+
clear() {
|
|
104
|
+
this.cache.clear();
|
|
105
|
+
this.stats = {
|
|
106
|
+
hits: 0,
|
|
107
|
+
misses: 0,
|
|
108
|
+
evictions: 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get cache statistics
|
|
113
|
+
*
|
|
114
|
+
* @returns Cache statistics including hit rate
|
|
115
|
+
*/
|
|
116
|
+
getStats() {
|
|
117
|
+
const total = this.stats.hits + this.stats.misses;
|
|
118
|
+
const hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
119
|
+
return {
|
|
120
|
+
size: this.cache.size,
|
|
121
|
+
maxSize: this.maxSize,
|
|
122
|
+
hits: this.stats.hits,
|
|
123
|
+
misses: this.stats.misses,
|
|
124
|
+
evictions: this.stats.evictions,
|
|
125
|
+
hitRate
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if a key exists in the cache (and is not expired)
|
|
130
|
+
*
|
|
131
|
+
* @param key - Cache key to check
|
|
132
|
+
* @returns true if the key exists and is not expired
|
|
133
|
+
*/
|
|
134
|
+
has(key) {
|
|
135
|
+
const cached = this.cache.get(key);
|
|
136
|
+
if (!cached) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const age = Date.now() - cached.timestamp;
|
|
140
|
+
if (age >= this.ttl) {
|
|
141
|
+
this.cache.delete(key);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/errors.ts
|
|
149
|
+
var ObjectStackError = class extends Error {
|
|
150
|
+
/**
|
|
151
|
+
* Create a new ObjectStackError
|
|
152
|
+
*
|
|
153
|
+
* @param message - Human-readable error message
|
|
154
|
+
* @param code - Unique error code for programmatic handling
|
|
155
|
+
* @param statusCode - Optional HTTP status code
|
|
156
|
+
* @param details - Optional additional error details for debugging
|
|
157
|
+
*/
|
|
158
|
+
constructor(message, code, statusCode, details) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.code = code;
|
|
161
|
+
this.statusCode = statusCode;
|
|
162
|
+
this.details = details;
|
|
163
|
+
this.name = "ObjectStackError";
|
|
164
|
+
if (Error.captureStackTrace) {
|
|
165
|
+
Error.captureStackTrace(this, this.constructor);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Convert error to JSON for logging/debugging
|
|
170
|
+
*/
|
|
171
|
+
toJSON() {
|
|
172
|
+
return {
|
|
173
|
+
name: this.name,
|
|
174
|
+
message: this.message,
|
|
175
|
+
code: this.code,
|
|
176
|
+
statusCode: this.statusCode,
|
|
177
|
+
details: this.details,
|
|
178
|
+
stack: this.stack
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var MetadataNotFoundError = class extends ObjectStackError {
|
|
183
|
+
constructor(objectName, details) {
|
|
184
|
+
super(
|
|
185
|
+
`Metadata not found for object: ${objectName}`,
|
|
186
|
+
"METADATA_NOT_FOUND",
|
|
187
|
+
404,
|
|
188
|
+
{ objectName, ...details }
|
|
189
|
+
);
|
|
190
|
+
this.name = "MetadataNotFoundError";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var BulkOperationError = class extends ObjectStackError {
|
|
194
|
+
/**
|
|
195
|
+
* Create a new BulkOperationError
|
|
196
|
+
*
|
|
197
|
+
* @param operation - The bulk operation that failed (create, update, delete)
|
|
198
|
+
* @param successCount - Number of successful operations
|
|
199
|
+
* @param failureCount - Number of failed operations
|
|
200
|
+
* @param errors - Array of individual errors
|
|
201
|
+
* @param details - Additional error details
|
|
202
|
+
*/
|
|
203
|
+
constructor(operation, successCount, failureCount, errors, details) {
|
|
204
|
+
super(
|
|
205
|
+
`Bulk ${operation} operation failed: ${successCount} succeeded, ${failureCount} failed`,
|
|
206
|
+
"BULK_OPERATION_ERROR",
|
|
207
|
+
500,
|
|
208
|
+
{
|
|
209
|
+
operation,
|
|
210
|
+
successCount,
|
|
211
|
+
failureCount,
|
|
212
|
+
errors,
|
|
213
|
+
...details
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
this.successCount = successCount;
|
|
217
|
+
this.failureCount = failureCount;
|
|
218
|
+
this.errors = errors;
|
|
219
|
+
this.name = "BulkOperationError";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get a summary of the bulk operation failure
|
|
223
|
+
*/
|
|
224
|
+
getSummary() {
|
|
225
|
+
const total = this.successCount + this.failureCount;
|
|
226
|
+
const failureRate = total > 0 ? this.failureCount / total : 0;
|
|
227
|
+
return {
|
|
228
|
+
operation: this.details?.operation,
|
|
229
|
+
total,
|
|
230
|
+
successful: this.successCount,
|
|
231
|
+
failed: this.failureCount,
|
|
232
|
+
failureRate,
|
|
233
|
+
errors: this.errors
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var ConnectionError = class extends ObjectStackError {
|
|
238
|
+
constructor(message, url, details, statusCode) {
|
|
239
|
+
super(
|
|
240
|
+
`Connection error: ${message}`,
|
|
241
|
+
"CONNECTION_ERROR",
|
|
242
|
+
statusCode || 503,
|
|
243
|
+
{ url, ...details }
|
|
244
|
+
);
|
|
245
|
+
this.url = url;
|
|
246
|
+
this.name = "ConnectionError";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var AuthenticationError = class extends ObjectStackError {
|
|
250
|
+
constructor(message = "Authentication failed", details, statusCode) {
|
|
251
|
+
super(
|
|
252
|
+
message,
|
|
253
|
+
"AUTHENTICATION_ERROR",
|
|
254
|
+
statusCode || 401,
|
|
255
|
+
details
|
|
256
|
+
);
|
|
257
|
+
this.name = "AuthenticationError";
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
var ValidationError = class extends ObjectStackError {
|
|
261
|
+
/**
|
|
262
|
+
* Create a new ValidationError
|
|
263
|
+
*
|
|
264
|
+
* @param message - Human-readable error message
|
|
265
|
+
* @param field - The field that failed validation (optional)
|
|
266
|
+
* @param validationErrors - Array of validation error details
|
|
267
|
+
* @param details - Additional error details
|
|
268
|
+
*/
|
|
269
|
+
constructor(message, field, validationErrors, details) {
|
|
270
|
+
super(
|
|
271
|
+
message,
|
|
272
|
+
"VALIDATION_ERROR",
|
|
273
|
+
400,
|
|
274
|
+
{
|
|
275
|
+
field,
|
|
276
|
+
validationErrors,
|
|
277
|
+
...details
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
this.field = field;
|
|
281
|
+
this.validationErrors = validationErrors;
|
|
282
|
+
this.name = "ValidationError";
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get all validation errors as a formatted list
|
|
286
|
+
*/
|
|
287
|
+
getValidationErrors() {
|
|
288
|
+
return this.validationErrors || [];
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function createErrorFromResponse(response, context) {
|
|
292
|
+
const status = response?.status || response?.statusCode || 500;
|
|
293
|
+
const message = response?.message || response?.statusText || "Unknown error";
|
|
294
|
+
const details = {
|
|
295
|
+
context,
|
|
296
|
+
response: {
|
|
297
|
+
status,
|
|
298
|
+
data: response?.data,
|
|
299
|
+
headers: response?.headers
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
switch (status) {
|
|
303
|
+
case 401:
|
|
304
|
+
return new AuthenticationError(message, details, 401);
|
|
305
|
+
case 403:
|
|
306
|
+
return new AuthenticationError(message, details, 403);
|
|
307
|
+
case 404:
|
|
308
|
+
if (context?.includes("metadata") || context?.includes("schema") || context?.includes("getObjectSchema")) {
|
|
309
|
+
const objectName = extractObjectName(context);
|
|
310
|
+
return new MetadataNotFoundError(objectName, details);
|
|
311
|
+
}
|
|
312
|
+
return new ObjectStackError(message, "NOT_FOUND", 404, details);
|
|
313
|
+
case 400:
|
|
314
|
+
return new ValidationError(message, void 0, response?.data?.errors, details);
|
|
315
|
+
case 503:
|
|
316
|
+
return new ConnectionError(message, response?.config?.url, details, 503);
|
|
317
|
+
case 504:
|
|
318
|
+
return new ConnectionError(message, response?.config?.url, details, 504);
|
|
319
|
+
default:
|
|
320
|
+
return new ObjectStackError(message, "UNKNOWN_ERROR", status, details);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function extractObjectName(context) {
|
|
324
|
+
if (!context) return "unknown";
|
|
325
|
+
const match = context.match(/\(([^)]+)\)/);
|
|
326
|
+
return match ? match[1] : "unknown";
|
|
327
|
+
}
|
|
328
|
+
function isObjectStackError(error) {
|
|
329
|
+
return error instanceof ObjectStackError;
|
|
330
|
+
}
|
|
331
|
+
function isErrorType(error, errorClass) {
|
|
332
|
+
return error instanceof errorClass;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/index.ts
|
|
8
336
|
var ObjectStackAdapter = class {
|
|
9
337
|
constructor(config) {
|
|
10
338
|
__publicField(this, "client");
|
|
11
339
|
__publicField(this, "connected", false);
|
|
340
|
+
__publicField(this, "metadataCache");
|
|
341
|
+
__publicField(this, "connectionState", "disconnected");
|
|
342
|
+
__publicField(this, "connectionStateListeners", []);
|
|
343
|
+
__publicField(this, "batchProgressListeners", []);
|
|
344
|
+
__publicField(this, "autoReconnect");
|
|
345
|
+
__publicField(this, "maxReconnectAttempts");
|
|
346
|
+
__publicField(this, "reconnectDelay");
|
|
347
|
+
__publicField(this, "reconnectAttempts", 0);
|
|
348
|
+
__publicField(this, "baseUrl");
|
|
349
|
+
__publicField(this, "token");
|
|
12
350
|
this.client = new ObjectStackClient(config);
|
|
351
|
+
this.metadataCache = new MetadataCache(config.cache);
|
|
352
|
+
this.autoReconnect = config.autoReconnect ?? true;
|
|
353
|
+
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
|
|
354
|
+
this.reconnectDelay = config.reconnectDelay ?? 1e3;
|
|
355
|
+
this.baseUrl = config.baseUrl;
|
|
356
|
+
this.token = config.token;
|
|
13
357
|
}
|
|
14
358
|
/**
|
|
15
359
|
* Ensure the client is connected to the server.
|
|
@@ -17,10 +361,105 @@ var ObjectStackAdapter = class {
|
|
|
17
361
|
*/
|
|
18
362
|
async connect() {
|
|
19
363
|
if (!this.connected) {
|
|
20
|
-
|
|
21
|
-
|
|
364
|
+
this.setConnectionState("connecting");
|
|
365
|
+
try {
|
|
366
|
+
await this.client.connect();
|
|
367
|
+
this.connected = true;
|
|
368
|
+
this.reconnectAttempts = 0;
|
|
369
|
+
this.setConnectionState("connected");
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to connect to ObjectStack server";
|
|
372
|
+
const connectionError = new ConnectionError(
|
|
373
|
+
errorMessage,
|
|
374
|
+
void 0,
|
|
375
|
+
{ originalError: error }
|
|
376
|
+
);
|
|
377
|
+
this.setConnectionState("error", connectionError);
|
|
378
|
+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
379
|
+
await this.attemptReconnect();
|
|
380
|
+
} else {
|
|
381
|
+
throw connectionError;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
22
384
|
}
|
|
23
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Attempt to reconnect to the server with exponential backoff
|
|
388
|
+
*/
|
|
389
|
+
async attemptReconnect() {
|
|
390
|
+
this.reconnectAttempts++;
|
|
391
|
+
this.setConnectionState("reconnecting");
|
|
392
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
394
|
+
this.connected = false;
|
|
395
|
+
await this.connect();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get the current connection state
|
|
399
|
+
*/
|
|
400
|
+
getConnectionState() {
|
|
401
|
+
return this.connectionState;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check if the adapter is currently connected
|
|
405
|
+
*/
|
|
406
|
+
isConnected() {
|
|
407
|
+
return this.connected && this.connectionState === "connected";
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Register a listener for connection state changes
|
|
411
|
+
*/
|
|
412
|
+
onConnectionStateChange(listener) {
|
|
413
|
+
this.connectionStateListeners.push(listener);
|
|
414
|
+
return () => {
|
|
415
|
+
const index = this.connectionStateListeners.indexOf(listener);
|
|
416
|
+
if (index > -1) {
|
|
417
|
+
this.connectionStateListeners.splice(index, 1);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Register a listener for batch operation progress
|
|
423
|
+
*/
|
|
424
|
+
onBatchProgress(listener) {
|
|
425
|
+
this.batchProgressListeners.push(listener);
|
|
426
|
+
return () => {
|
|
427
|
+
const index = this.batchProgressListeners.indexOf(listener);
|
|
428
|
+
if (index > -1) {
|
|
429
|
+
this.batchProgressListeners.splice(index, 1);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Set connection state and notify listeners
|
|
435
|
+
*/
|
|
436
|
+
setConnectionState(state, error) {
|
|
437
|
+
this.connectionState = state;
|
|
438
|
+
const event = {
|
|
439
|
+
state,
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
error
|
|
442
|
+
};
|
|
443
|
+
this.connectionStateListeners.forEach((listener) => {
|
|
444
|
+
try {
|
|
445
|
+
listener(event);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error("Error in connection state listener:", err);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Emit batch progress event to listeners
|
|
453
|
+
*/
|
|
454
|
+
emitBatchProgress(event) {
|
|
455
|
+
this.batchProgressListeners.forEach((listener) => {
|
|
456
|
+
try {
|
|
457
|
+
listener(event);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error("Error in batch progress listener:", err);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
24
463
|
/**
|
|
25
464
|
* Find multiple records with query parameters.
|
|
26
465
|
* Converts OData-style params to ObjectStack query options.
|
|
@@ -29,12 +468,23 @@ var ObjectStackAdapter = class {
|
|
|
29
468
|
await this.connect();
|
|
30
469
|
const queryOptions = this.convertQueryParams(params);
|
|
31
470
|
const result = await this.client.data.find(resource, queryOptions);
|
|
471
|
+
if (Array.isArray(result)) {
|
|
472
|
+
return {
|
|
473
|
+
data: result,
|
|
474
|
+
total: result.length,
|
|
475
|
+
page: 1,
|
|
476
|
+
pageSize: result.length,
|
|
477
|
+
hasMore: false
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const resultObj = result;
|
|
32
481
|
return {
|
|
33
|
-
data:
|
|
34
|
-
total:
|
|
35
|
-
|
|
482
|
+
data: resultObj.value || [],
|
|
483
|
+
total: resultObj.count || (resultObj.value ? resultObj.value.length : 0),
|
|
484
|
+
// Calculate page number safely
|
|
485
|
+
page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
|
|
36
486
|
pageSize: params?.$top,
|
|
37
|
-
hasMore:
|
|
487
|
+
hasMore: params?.$top ? (resultObj.value?.length || 0) === params.$top : false
|
|
38
488
|
};
|
|
39
489
|
}
|
|
40
490
|
/**
|
|
@@ -43,8 +493,8 @@ var ObjectStackAdapter = class {
|
|
|
43
493
|
async findOne(resource, id, _params) {
|
|
44
494
|
await this.connect();
|
|
45
495
|
try {
|
|
46
|
-
const
|
|
47
|
-
return record;
|
|
496
|
+
const result = await this.client.data.get(resource, String(id));
|
|
497
|
+
return result.record;
|
|
48
498
|
} catch (error) {
|
|
49
499
|
if (error?.status === 404) {
|
|
50
500
|
return null;
|
|
@@ -57,14 +507,16 @@ var ObjectStackAdapter = class {
|
|
|
57
507
|
*/
|
|
58
508
|
async create(resource, data) {
|
|
59
509
|
await this.connect();
|
|
60
|
-
|
|
510
|
+
const result = await this.client.data.create(resource, data);
|
|
511
|
+
return result.record;
|
|
61
512
|
}
|
|
62
513
|
/**
|
|
63
514
|
* Update an existing record.
|
|
64
515
|
*/
|
|
65
516
|
async update(resource, id, data) {
|
|
66
517
|
await this.connect();
|
|
67
|
-
|
|
518
|
+
const result = await this.client.data.update(resource, String(id), data);
|
|
519
|
+
return result.record;
|
|
68
520
|
}
|
|
69
521
|
/**
|
|
70
522
|
* Delete a record.
|
|
@@ -72,31 +524,136 @@ var ObjectStackAdapter = class {
|
|
|
72
524
|
async delete(resource, id) {
|
|
73
525
|
await this.connect();
|
|
74
526
|
const result = await this.client.data.delete(resource, String(id));
|
|
75
|
-
return result.
|
|
527
|
+
return result.deleted;
|
|
76
528
|
}
|
|
77
529
|
/**
|
|
78
|
-
* Bulk operations
|
|
530
|
+
* Bulk operations with optimized batch processing and error handling.
|
|
531
|
+
* Emits progress events for tracking operation status.
|
|
532
|
+
*
|
|
533
|
+
* @param resource - Resource name
|
|
534
|
+
* @param operation - Operation type (create, update, delete)
|
|
535
|
+
* @param data - Array of records to process
|
|
536
|
+
* @returns Promise resolving to array of results
|
|
79
537
|
*/
|
|
80
538
|
async bulk(resource, operation, data) {
|
|
81
539
|
await this.connect();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
540
|
+
if (!data || data.length === 0) {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
const total = data.length;
|
|
544
|
+
let completed = 0;
|
|
545
|
+
let failed = 0;
|
|
546
|
+
const emitProgress = () => {
|
|
547
|
+
this.emitBatchProgress({
|
|
548
|
+
operation,
|
|
549
|
+
total,
|
|
550
|
+
completed,
|
|
551
|
+
failed,
|
|
552
|
+
percentage: total > 0 ? (completed + failed) / total * 100 : 0
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
try {
|
|
556
|
+
switch (operation) {
|
|
557
|
+
case "create": {
|
|
558
|
+
emitProgress();
|
|
559
|
+
const created = await this.client.data.createMany(resource, data);
|
|
560
|
+
completed = created.length;
|
|
561
|
+
failed = total - completed;
|
|
562
|
+
emitProgress();
|
|
563
|
+
return created;
|
|
564
|
+
}
|
|
565
|
+
case "delete": {
|
|
566
|
+
const ids = data.map((item) => item.id).filter(Boolean);
|
|
567
|
+
if (ids.length === 0) {
|
|
568
|
+
const errors = data.map((_, index) => ({
|
|
569
|
+
index,
|
|
570
|
+
error: `Missing ID for item at index ${index}`
|
|
571
|
+
}));
|
|
572
|
+
failed = data.length;
|
|
573
|
+
emitProgress();
|
|
574
|
+
throw new BulkOperationError("delete", 0, data.length, errors);
|
|
575
|
+
}
|
|
576
|
+
emitProgress();
|
|
577
|
+
await this.client.data.deleteMany(resource, ids);
|
|
578
|
+
completed = ids.length;
|
|
579
|
+
failed = total - completed;
|
|
580
|
+
emitProgress();
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
case "update": {
|
|
584
|
+
if (typeof this.client.data.updateMany === "function") {
|
|
585
|
+
try {
|
|
586
|
+
emitProgress();
|
|
587
|
+
const updateMany = this.client.data.updateMany;
|
|
588
|
+
const updated = await updateMany(resource, data);
|
|
589
|
+
completed = updated.length;
|
|
590
|
+
failed = total - completed;
|
|
591
|
+
emitProgress();
|
|
592
|
+
return updated;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const results = [];
|
|
597
|
+
const errors = [];
|
|
598
|
+
for (let i = 0; i < data.length; i++) {
|
|
599
|
+
const item = data[i];
|
|
600
|
+
const id = item.id;
|
|
601
|
+
if (!id) {
|
|
602
|
+
errors.push({ index: i, error: "Missing ID" });
|
|
603
|
+
failed++;
|
|
604
|
+
emitProgress();
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const result = await this.client.data.update(resource, String(id), item);
|
|
609
|
+
results.push(result.record);
|
|
610
|
+
completed++;
|
|
611
|
+
emitProgress();
|
|
612
|
+
} catch (error) {
|
|
613
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
614
|
+
errors.push({ index: i, error: errorMessage });
|
|
615
|
+
failed++;
|
|
616
|
+
emitProgress();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (errors.length > 0) {
|
|
620
|
+
throw new BulkOperationError(
|
|
621
|
+
"update",
|
|
622
|
+
results.length,
|
|
623
|
+
errors.length,
|
|
624
|
+
errors,
|
|
625
|
+
{ resource, totalRecords: data.length }
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
return results;
|
|
629
|
+
}
|
|
630
|
+
default:
|
|
631
|
+
throw new ObjectStackError(
|
|
632
|
+
`Unsupported bulk operation: ${operation}`,
|
|
633
|
+
"UNSUPPORTED_OPERATION",
|
|
634
|
+
400
|
|
635
|
+
);
|
|
97
636
|
}
|
|
98
|
-
|
|
99
|
-
|
|
637
|
+
} catch (error) {
|
|
638
|
+
emitProgress();
|
|
639
|
+
if (error instanceof BulkOperationError) {
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
if (error instanceof ObjectStackError) {
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
646
|
+
const errors = data.map((_, index) => ({
|
|
647
|
+
index,
|
|
648
|
+
error: errorMessage
|
|
649
|
+
}));
|
|
650
|
+
throw new BulkOperationError(
|
|
651
|
+
operation,
|
|
652
|
+
0,
|
|
653
|
+
data.length,
|
|
654
|
+
errors,
|
|
655
|
+
{ resource, originalError: error }
|
|
656
|
+
);
|
|
100
657
|
}
|
|
101
658
|
}
|
|
102
659
|
/**
|
|
@@ -110,13 +667,26 @@ var ObjectStackAdapter = class {
|
|
|
110
667
|
options.select = params.$select;
|
|
111
668
|
}
|
|
112
669
|
if (params.$filter) {
|
|
113
|
-
|
|
670
|
+
if (Array.isArray(params.$filter)) {
|
|
671
|
+
options.filters = params.$filter;
|
|
672
|
+
} else {
|
|
673
|
+
options.filters = convertFiltersToAST(params.$filter);
|
|
674
|
+
}
|
|
114
675
|
}
|
|
115
676
|
if (params.$orderby) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
677
|
+
if (Array.isArray(params.$orderby)) {
|
|
678
|
+
options.sort = params.$orderby.map((item) => {
|
|
679
|
+
if (typeof item === "string") return item;
|
|
680
|
+
const field = item.field;
|
|
681
|
+
const order = item.order || "asc";
|
|
682
|
+
return order === "desc" ? `-${field}` : field;
|
|
683
|
+
});
|
|
684
|
+
} else {
|
|
685
|
+
const sortArray = Object.entries(params.$orderby).map(([field, order]) => {
|
|
686
|
+
return order === "desc" ? `-${field}` : field;
|
|
687
|
+
});
|
|
688
|
+
options.sort = sortArray;
|
|
689
|
+
}
|
|
120
690
|
}
|
|
121
691
|
if (params.$skip !== void 0) {
|
|
122
692
|
options.skip = params.$skip;
|
|
@@ -128,6 +698,7 @@ var ObjectStackAdapter = class {
|
|
|
128
698
|
}
|
|
129
699
|
/**
|
|
130
700
|
* Get object schema/metadata from ObjectStack.
|
|
701
|
+
* Uses caching to improve performance for repeated requests.
|
|
131
702
|
*
|
|
132
703
|
* @param objectName - Object name
|
|
133
704
|
* @returns Promise resolving to the object schema
|
|
@@ -135,11 +706,23 @@ var ObjectStackAdapter = class {
|
|
|
135
706
|
async getObjectSchema(objectName) {
|
|
136
707
|
await this.connect();
|
|
137
708
|
try {
|
|
138
|
-
const schema = await this.
|
|
709
|
+
const schema = await this.metadataCache.get(objectName, async () => {
|
|
710
|
+
const result = await this.client.meta.getObject(objectName);
|
|
711
|
+
if (result && result.item) {
|
|
712
|
+
return result.item;
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
});
|
|
139
716
|
return schema;
|
|
140
717
|
} catch (error) {
|
|
141
|
-
|
|
142
|
-
|
|
718
|
+
const errorObj = error;
|
|
719
|
+
if (errorObj?.status === 404 || errorObj?.statusCode === 404) {
|
|
720
|
+
throw new MetadataNotFoundError(objectName, { originalError: error });
|
|
721
|
+
}
|
|
722
|
+
if (error instanceof ObjectStackError) {
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
throw createErrorFromResponse(errorObj, `getObjectSchema(${objectName})`);
|
|
143
726
|
}
|
|
144
727
|
}
|
|
145
728
|
/**
|
|
@@ -148,11 +731,194 @@ var ObjectStackAdapter = class {
|
|
|
148
731
|
getClient() {
|
|
149
732
|
return this.client;
|
|
150
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Get the discovery information from the connected server.
|
|
736
|
+
* Returns the capabilities and service status of the ObjectStack server.
|
|
737
|
+
*
|
|
738
|
+
* Note: This accesses an internal property of the ObjectStackClient.
|
|
739
|
+
* The discovery data is populated during client.connect() and cached.
|
|
740
|
+
*
|
|
741
|
+
* @returns Promise resolving to discovery data, or null if not connected
|
|
742
|
+
*/
|
|
743
|
+
async getDiscovery() {
|
|
744
|
+
try {
|
|
745
|
+
await this.connect();
|
|
746
|
+
return this.client.discoveryInfo || null;
|
|
747
|
+
} catch {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Get a view definition for an object.
|
|
753
|
+
* Attempts to fetch from the server metadata API.
|
|
754
|
+
* Falls back to null if the server doesn't provide view definitions,
|
|
755
|
+
* allowing the consumer to use static config.
|
|
756
|
+
*
|
|
757
|
+
* @param objectName - Object name
|
|
758
|
+
* @param viewId - View identifier
|
|
759
|
+
* @returns Promise resolving to the view definition or null
|
|
760
|
+
*/
|
|
761
|
+
async getView(objectName, viewId) {
|
|
762
|
+
await this.connect();
|
|
763
|
+
try {
|
|
764
|
+
const cacheKey = `view:${objectName}:${viewId}`;
|
|
765
|
+
return await this.metadataCache.get(cacheKey, async () => {
|
|
766
|
+
const result = await this.client.meta.getItem(objectName, `views/${viewId}`);
|
|
767
|
+
if (result && result.item) return result.item;
|
|
768
|
+
return result ?? null;
|
|
769
|
+
});
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get an application definition by name or ID.
|
|
776
|
+
* Attempts to fetch from the server metadata API.
|
|
777
|
+
* Falls back to null if the server doesn't provide app definitions,
|
|
778
|
+
* allowing the consumer to use static config.
|
|
779
|
+
*
|
|
780
|
+
* @param appId - Application identifier
|
|
781
|
+
* @returns Promise resolving to the app definition or null
|
|
782
|
+
*/
|
|
783
|
+
async getApp(appId) {
|
|
784
|
+
await this.connect();
|
|
785
|
+
try {
|
|
786
|
+
const cacheKey = `app:${appId}`;
|
|
787
|
+
return await this.metadataCache.get(cacheKey, async () => {
|
|
788
|
+
const result = await this.client.meta.getItem("apps", appId);
|
|
789
|
+
if (result && result.item) return result.item;
|
|
790
|
+
return result ?? null;
|
|
791
|
+
});
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Get cache statistics for monitoring performance.
|
|
798
|
+
*/
|
|
799
|
+
getCacheStats() {
|
|
800
|
+
return this.metadataCache.getStats();
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Invalidate metadata cache entries.
|
|
804
|
+
*
|
|
805
|
+
* @param key - Optional key to invalidate. If omitted, invalidates all entries.
|
|
806
|
+
*/
|
|
807
|
+
invalidateCache(key) {
|
|
808
|
+
this.metadataCache.invalidate(key);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Clear all cache entries and statistics.
|
|
812
|
+
*/
|
|
813
|
+
clearCache() {
|
|
814
|
+
this.metadataCache.clear();
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Upload a single file to a resource.
|
|
818
|
+
* Posts the file as multipart/form-data to the ObjectStack server.
|
|
819
|
+
*
|
|
820
|
+
* @param resource - The resource/object name to attach the file to
|
|
821
|
+
* @param file - File object or Blob to upload
|
|
822
|
+
* @param options - Additional upload options (recordId, fieldName, metadata)
|
|
823
|
+
* @returns Promise resolving to the upload result (file URL, metadata)
|
|
824
|
+
*/
|
|
825
|
+
async uploadFile(resource, file, options) {
|
|
826
|
+
await this.connect();
|
|
827
|
+
const formData = new FormData();
|
|
828
|
+
formData.append("file", file);
|
|
829
|
+
if (options?.recordId) {
|
|
830
|
+
formData.append("recordId", options.recordId);
|
|
831
|
+
}
|
|
832
|
+
if (options?.fieldName) {
|
|
833
|
+
formData.append("fieldName", options.fieldName);
|
|
834
|
+
}
|
|
835
|
+
if (options?.metadata) {
|
|
836
|
+
formData.append("metadata", JSON.stringify(options.metadata));
|
|
837
|
+
}
|
|
838
|
+
const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
|
|
839
|
+
const response = await fetch(url, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
body: formData,
|
|
842
|
+
headers: {
|
|
843
|
+
...this.getAuthHeaders()
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
848
|
+
throw new ObjectStackError(
|
|
849
|
+
error.message || `Upload failed with status ${response.status}`,
|
|
850
|
+
"UPLOAD_ERROR",
|
|
851
|
+
response.status
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return response.json();
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Upload multiple files to a resource.
|
|
858
|
+
* Posts all files as a single multipart/form-data request.
|
|
859
|
+
*
|
|
860
|
+
* @param resource - The resource/object name to attach the files to
|
|
861
|
+
* @param files - Array of File objects or Blobs to upload
|
|
862
|
+
* @param options - Additional upload options
|
|
863
|
+
* @returns Promise resolving to array of upload results
|
|
864
|
+
*/
|
|
865
|
+
async uploadFiles(resource, files, options) {
|
|
866
|
+
await this.connect();
|
|
867
|
+
const formData = new FormData();
|
|
868
|
+
files.forEach((file, idx) => {
|
|
869
|
+
formData.append(`files`, file, file.name || `file-${idx}`);
|
|
870
|
+
});
|
|
871
|
+
if (options?.recordId) {
|
|
872
|
+
formData.append("recordId", options.recordId);
|
|
873
|
+
}
|
|
874
|
+
if (options?.fieldName) {
|
|
875
|
+
formData.append("fieldName", options.fieldName);
|
|
876
|
+
}
|
|
877
|
+
if (options?.metadata) {
|
|
878
|
+
formData.append("metadata", JSON.stringify(options.metadata));
|
|
879
|
+
}
|
|
880
|
+
const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
|
|
881
|
+
const response = await fetch(url, {
|
|
882
|
+
method: "POST",
|
|
883
|
+
body: formData,
|
|
884
|
+
headers: {
|
|
885
|
+
...this.getAuthHeaders()
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
if (!response.ok) {
|
|
889
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
890
|
+
throw new ObjectStackError(
|
|
891
|
+
error.message || `Upload failed with status ${response.status}`,
|
|
892
|
+
"UPLOAD_ERROR",
|
|
893
|
+
response.status
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
return response.json();
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Get authorization headers from the adapter config.
|
|
900
|
+
*/
|
|
901
|
+
getAuthHeaders() {
|
|
902
|
+
const headers = {};
|
|
903
|
+
if (this.token) {
|
|
904
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
905
|
+
}
|
|
906
|
+
return headers;
|
|
907
|
+
}
|
|
151
908
|
};
|
|
152
909
|
function createObjectStackAdapter(config) {
|
|
153
910
|
return new ObjectStackAdapter(config);
|
|
154
911
|
}
|
|
155
912
|
export {
|
|
913
|
+
AuthenticationError,
|
|
914
|
+
BulkOperationError,
|
|
915
|
+
ConnectionError,
|
|
916
|
+
MetadataNotFoundError,
|
|
156
917
|
ObjectStackAdapter,
|
|
157
|
-
|
|
918
|
+
ObjectStackError,
|
|
919
|
+
ValidationError,
|
|
920
|
+
createErrorFromResponse,
|
|
921
|
+
createObjectStackAdapter,
|
|
922
|
+
isErrorType,
|
|
923
|
+
isObjectStackError
|
|
158
924
|
};
|