@objectstack/objectql 4.1.1 → 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.mjs CHANGED
@@ -619,6 +619,25 @@ function simpleHash(str) {
619
619
  }
620
620
  return Math.abs(hash).toString(16);
621
621
  }
622
+ var ConcurrentUpdateError = class extends Error {
623
+ constructor(opts) {
624
+ super(opts.message ?? "Record was modified by another user");
625
+ this.code = "CONCURRENT_UPDATE";
626
+ this.status = 409;
627
+ this.name = "ConcurrentUpdateError";
628
+ this.currentVersion = opts.currentVersion;
629
+ this.currentRecord = opts.currentRecord;
630
+ }
631
+ };
632
+ function normaliseVersionToken(v) {
633
+ if (v === null || v === void 0) return null;
634
+ const s = String(v).trim();
635
+ if (!s) return null;
636
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
637
+ return s.slice(1, -1);
638
+ }
639
+ return s;
640
+ }
622
641
  var SERVICE_CONFIG = {
623
642
  auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
624
643
  automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
@@ -1212,6 +1231,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1212
1231
  };
1213
1232
  }
1214
1233
  async updateData(request) {
1234
+ await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
1215
1235
  const opts = { where: { id: request.id } };
1216
1236
  if (request.context !== void 0) opts.context = request.context;
1217
1237
  const result = await this.engine.update(request.object, request.data, opts);
@@ -1222,6 +1242,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1222
1242
  };
1223
1243
  }
1224
1244
  async deleteData(request) {
1245
+ await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
1225
1246
  const opts = { where: { id: request.id } };
1226
1247
  if (request.context !== void 0) opts.context = request.context;
1227
1248
  await this.engine.delete(request.object, opts);
@@ -1231,6 +1252,42 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1231
1252
  success: true
1232
1253
  };
1233
1254
  }
1255
+ /**
1256
+ * Optimistic Concurrency Control gate shared by updateData/deleteData.
1257
+ *
1258
+ * When the caller passes a non-empty `expectedVersion` token (typically
1259
+ * the `updated_at` value they read), this fetches the current record
1260
+ * and compares its `updated_at` against the token. Mismatch → throw
1261
+ * `ConcurrentUpdateError` which the REST layer maps to 409.
1262
+ *
1263
+ * Behaviour:
1264
+ * - Empty/missing token → no check (opt-in semantics; existing callers
1265
+ * that haven't yet adopted OCC are unaffected).
1266
+ * - Record not found → no check; downstream `engine.update` will
1267
+ * surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
1268
+ * treat "missing record" as a concurrency conflict.
1269
+ * - Record has no `updated_at` field (timestamps disabled) → no check.
1270
+ * Logging would be noisy here; OCC is opt-in and the absence of a
1271
+ * version column is an explicit "this object doesn't support OCC"
1272
+ * signal.
1273
+ */
1274
+ async assertVersionMatch(object, id, expectedVersion, context) {
1275
+ const expected = normaliseVersionToken(expectedVersion);
1276
+ if (!expected) return;
1277
+ const findOpts = { where: { id } };
1278
+ if (context !== void 0) findOpts.context = context;
1279
+ const current = await this.engine.findOne(object, findOpts);
1280
+ if (!current) return;
1281
+ const currentVersion = normaliseVersionToken(current.updated_at);
1282
+ if (!currentVersion) return;
1283
+ if (currentVersion !== expected) {
1284
+ throw new ConcurrentUpdateError({
1285
+ currentVersion,
1286
+ currentRecord: current,
1287
+ message: `Record ${object}/${id} was modified by another user (current version ${currentVersion}, expected ${expected})`
1288
+ });
1289
+ }
1290
+ }
1234
1291
  // ==========================================
1235
1292
  // Global Search (M10.5)
1236
1293
  // ==========================================