@objectstack/objectql 4.1.0 → 4.2.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/dist/index.d.mts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -418,6 +418,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
418
418
|
object: string;
|
|
419
419
|
id: string;
|
|
420
420
|
data: any;
|
|
421
|
+
expectedVersion?: string;
|
|
421
422
|
context?: any;
|
|
422
423
|
}): Promise<{
|
|
423
424
|
object: string;
|
|
@@ -427,12 +428,33 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
427
428
|
deleteData(request: {
|
|
428
429
|
object: string;
|
|
429
430
|
id: string;
|
|
431
|
+
expectedVersion?: string;
|
|
430
432
|
context?: any;
|
|
431
433
|
}): Promise<{
|
|
432
434
|
object: string;
|
|
433
435
|
id: string;
|
|
434
436
|
success: boolean;
|
|
435
437
|
}>;
|
|
438
|
+
/**
|
|
439
|
+
* Optimistic Concurrency Control gate shared by updateData/deleteData.
|
|
440
|
+
*
|
|
441
|
+
* When the caller passes a non-empty `expectedVersion` token (typically
|
|
442
|
+
* the `updated_at` value they read), this fetches the current record
|
|
443
|
+
* and compares its `updated_at` against the token. Mismatch → throw
|
|
444
|
+
* `ConcurrentUpdateError` which the REST layer maps to 409.
|
|
445
|
+
*
|
|
446
|
+
* Behaviour:
|
|
447
|
+
* - Empty/missing token → no check (opt-in semantics; existing callers
|
|
448
|
+
* that haven't yet adopted OCC are unaffected).
|
|
449
|
+
* - Record not found → no check; downstream `engine.update` will
|
|
450
|
+
* surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
|
|
451
|
+
* treat "missing record" as a concurrency conflict.
|
|
452
|
+
* - Record has no `updated_at` field (timestamps disabled) → no check.
|
|
453
|
+
* Logging would be noisy here; OCC is opt-in and the absence of a
|
|
454
|
+
* version column is an explicit "this object doesn't support OCC"
|
|
455
|
+
* signal.
|
|
456
|
+
*/
|
|
457
|
+
private assertVersionMatch;
|
|
436
458
|
/**
|
|
437
459
|
* Cross-object substring search across all registered objects that opt in
|
|
438
460
|
* via `enable.searchable !== false` and `enable.apiEnabled !== false`.
|
package/dist/index.d.ts
CHANGED
|
@@ -418,6 +418,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
418
418
|
object: string;
|
|
419
419
|
id: string;
|
|
420
420
|
data: any;
|
|
421
|
+
expectedVersion?: string;
|
|
421
422
|
context?: any;
|
|
422
423
|
}): Promise<{
|
|
423
424
|
object: string;
|
|
@@ -427,12 +428,33 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
427
428
|
deleteData(request: {
|
|
428
429
|
object: string;
|
|
429
430
|
id: string;
|
|
431
|
+
expectedVersion?: string;
|
|
430
432
|
context?: any;
|
|
431
433
|
}): Promise<{
|
|
432
434
|
object: string;
|
|
433
435
|
id: string;
|
|
434
436
|
success: boolean;
|
|
435
437
|
}>;
|
|
438
|
+
/**
|
|
439
|
+
* Optimistic Concurrency Control gate shared by updateData/deleteData.
|
|
440
|
+
*
|
|
441
|
+
* When the caller passes a non-empty `expectedVersion` token (typically
|
|
442
|
+
* the `updated_at` value they read), this fetches the current record
|
|
443
|
+
* and compares its `updated_at` against the token. Mismatch → throw
|
|
444
|
+
* `ConcurrentUpdateError` which the REST layer maps to 409.
|
|
445
|
+
*
|
|
446
|
+
* Behaviour:
|
|
447
|
+
* - Empty/missing token → no check (opt-in semantics; existing callers
|
|
448
|
+
* that haven't yet adopted OCC are unaffected).
|
|
449
|
+
* - Record not found → no check; downstream `engine.update` will
|
|
450
|
+
* surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
|
|
451
|
+
* treat "missing record" as a concurrency conflict.
|
|
452
|
+
* - Record has no `updated_at` field (timestamps disabled) → no check.
|
|
453
|
+
* Logging would be noisy here; OCC is opt-in and the absence of a
|
|
454
|
+
* version column is an explicit "this object doesn't support OCC"
|
|
455
|
+
* signal.
|
|
456
|
+
*/
|
|
457
|
+
private assertVersionMatch;
|
|
436
458
|
/**
|
|
437
459
|
* Cross-object substring search across all registered objects that opt in
|
|
438
460
|
* via `enable.searchable !== false` and `enable.apiEnabled !== false`.
|
package/dist/index.js
CHANGED
|
@@ -668,6 +668,25 @@ function simpleHash(str) {
|
|
|
668
668
|
}
|
|
669
669
|
return Math.abs(hash).toString(16);
|
|
670
670
|
}
|
|
671
|
+
var ConcurrentUpdateError = class extends Error {
|
|
672
|
+
constructor(opts) {
|
|
673
|
+
super(opts.message ?? "Record was modified by another user");
|
|
674
|
+
this.code = "CONCURRENT_UPDATE";
|
|
675
|
+
this.status = 409;
|
|
676
|
+
this.name = "ConcurrentUpdateError";
|
|
677
|
+
this.currentVersion = opts.currentVersion;
|
|
678
|
+
this.currentRecord = opts.currentRecord;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
function normaliseVersionToken(v) {
|
|
682
|
+
if (v === null || v === void 0) return null;
|
|
683
|
+
const s = String(v).trim();
|
|
684
|
+
if (!s) return null;
|
|
685
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
686
|
+
return s.slice(1, -1);
|
|
687
|
+
}
|
|
688
|
+
return s;
|
|
689
|
+
}
|
|
671
690
|
var SERVICE_CONFIG = {
|
|
672
691
|
auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
|
|
673
692
|
automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
|
|
@@ -1261,6 +1280,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1261
1280
|
};
|
|
1262
1281
|
}
|
|
1263
1282
|
async updateData(request) {
|
|
1283
|
+
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
1264
1284
|
const opts = { where: { id: request.id } };
|
|
1265
1285
|
if (request.context !== void 0) opts.context = request.context;
|
|
1266
1286
|
const result = await this.engine.update(request.object, request.data, opts);
|
|
@@ -1271,6 +1291,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1271
1291
|
};
|
|
1272
1292
|
}
|
|
1273
1293
|
async deleteData(request) {
|
|
1294
|
+
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
1274
1295
|
const opts = { where: { id: request.id } };
|
|
1275
1296
|
if (request.context !== void 0) opts.context = request.context;
|
|
1276
1297
|
await this.engine.delete(request.object, opts);
|
|
@@ -1280,6 +1301,42 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1280
1301
|
success: true
|
|
1281
1302
|
};
|
|
1282
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Optimistic Concurrency Control gate shared by updateData/deleteData.
|
|
1306
|
+
*
|
|
1307
|
+
* When the caller passes a non-empty `expectedVersion` token (typically
|
|
1308
|
+
* the `updated_at` value they read), this fetches the current record
|
|
1309
|
+
* and compares its `updated_at` against the token. Mismatch → throw
|
|
1310
|
+
* `ConcurrentUpdateError` which the REST layer maps to 409.
|
|
1311
|
+
*
|
|
1312
|
+
* Behaviour:
|
|
1313
|
+
* - Empty/missing token → no check (opt-in semantics; existing callers
|
|
1314
|
+
* that haven't yet adopted OCC are unaffected).
|
|
1315
|
+
* - Record not found → no check; downstream `engine.update` will
|
|
1316
|
+
* surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
|
|
1317
|
+
* treat "missing record" as a concurrency conflict.
|
|
1318
|
+
* - Record has no `updated_at` field (timestamps disabled) → no check.
|
|
1319
|
+
* Logging would be noisy here; OCC is opt-in and the absence of a
|
|
1320
|
+
* version column is an explicit "this object doesn't support OCC"
|
|
1321
|
+
* signal.
|
|
1322
|
+
*/
|
|
1323
|
+
async assertVersionMatch(object, id, expectedVersion, context) {
|
|
1324
|
+
const expected = normaliseVersionToken(expectedVersion);
|
|
1325
|
+
if (!expected) return;
|
|
1326
|
+
const findOpts = { where: { id } };
|
|
1327
|
+
if (context !== void 0) findOpts.context = context;
|
|
1328
|
+
const current = await this.engine.findOne(object, findOpts);
|
|
1329
|
+
if (!current) return;
|
|
1330
|
+
const currentVersion = normaliseVersionToken(current.updated_at);
|
|
1331
|
+
if (!currentVersion) return;
|
|
1332
|
+
if (currentVersion !== expected) {
|
|
1333
|
+
throw new ConcurrentUpdateError({
|
|
1334
|
+
currentVersion,
|
|
1335
|
+
currentRecord: current,
|
|
1336
|
+
message: `Record ${object}/${id} was modified by another user (current version ${currentVersion}, expected ${expected})`
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1283
1340
|
// ==========================================
|
|
1284
1341
|
// Global Search (M10.5)
|
|
1285
1342
|
// ==========================================
|