@atscript/db 0.1.83 → 0.1.85
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/{db-error-D8tQhNgM.cjs → db-error-CrtPzaZ-.cjs} +24 -0
- package/dist/{db-error-Cepx-RsQ.mjs → db-error-yx2jdg8B.mjs} +19 -1
- package/dist/{db-readable-B2TRNZHv.d.mts → db-readable-BUUVDkLf.d.mts} +18 -4
- package/dist/{db-readable-BSrIy08e.d.cts → db-readable-DgIxrEVQ.d.cts} +18 -4
- package/dist/{db-space-Dx8x-Ke1.d.cts → db-space-CZooARt_.d.mts} +2 -2
- package/dist/{db-space-SUFqMvKz.d.mts → db-space-DiErv7Q1.d.cts} +2 -2
- package/dist/{db-view-CqEIhTFa.mjs → db-view-Dfw-FP5o.mjs} +126 -29
- package/dist/{db-view-Bf5P3_7k.cjs → db-view-da5OTSRH.cjs} +130 -27
- package/dist/index.cjs +57 -3
- package/dist/index.d.cts +76 -5
- package/dist/index.d.mts +76 -5
- package/dist/index.mjs +54 -5
- package/dist/{nested-writer-CT2rLURx.mjs → nested-writer-CVMRAPoF.mjs} +1 -1
- package/dist/{nested-writer-v_LPR1yJ.cjs → nested-writer-Chl_zySG.cjs} +1 -1
- package/dist/{ops-C61kelof.d.mts → ops-Blqr0ipy.d.mts} +48 -1
- package/dist/{ops-DXJ4Zw0P.d.cts → ops-lzmfzuY9.d.cts} +48 -1
- package/dist/ops.cjs +73 -0
- package/dist/ops.d.cts +2 -2
- package/dist/ops.d.mts +2 -2
- package/dist/ops.mjs +72 -1
- package/dist/plugin.cjs +24 -1
- package/dist/plugin.mjs +24 -1
- package/dist/rel.cjs +1 -1
- package/dist/rel.d.cts +1 -1
- package/dist/rel.d.mts +1 -1
- package/dist/rel.mjs +1 -1
- package/dist/sync.cjs +1 -1
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +1 -1
- package/dist/{validator-BB5h1Le3.mjs → validator-DTDf9yWe.mjs} +1 -1
- package/dist/{validator-BIuw_T0k.cjs → validator-DrmUaZA3.cjs} +1 -1
- package/dist/validator.cjs +2 -1
- package/dist/validator.d.cts +2 -2
- package/dist/validator.d.mts +2 -2
- package/dist/validator.mjs +3 -3
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const require_db_error = require("./db-error-
|
|
1
|
+
const require_db_error = require("./db-error-CrtPzaZ-.cjs");
|
|
2
2
|
const require_agg = require("./agg.cjs");
|
|
3
3
|
const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
|
|
4
4
|
const require_ops = require("./ops.cjs");
|
|
5
|
-
const require_validator = require("./validator-
|
|
6
|
-
const require_nested_writer = require("./nested-writer-
|
|
5
|
+
const require_validator = require("./validator-DrmUaZA3.cjs");
|
|
6
|
+
const require_nested_writer = require("./nested-writer-Chl_zySG.cjs");
|
|
7
7
|
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
8
8
|
let node_async_hooks = require("node:async_hooks");
|
|
9
9
|
//#region src/logger.ts
|
|
@@ -64,6 +64,8 @@ var TableMetadata = class {
|
|
|
64
64
|
columnMap = /* @__PURE__ */ new Map();
|
|
65
65
|
dimensions = [];
|
|
66
66
|
measures = [];
|
|
67
|
+
/** Logical field name annotated with `@db.column.version`, if any. */
|
|
68
|
+
versionField;
|
|
67
69
|
/** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
|
|
68
70
|
quantityRefByField = /* @__PURE__ */ new Map();
|
|
69
71
|
pathToPhysical = /* @__PURE__ */ new Map();
|
|
@@ -258,6 +260,14 @@ var TableMetadata = class {
|
|
|
258
260
|
if (!hasExplicitIndex) this._addIndexField("plain", fieldName, fieldName);
|
|
259
261
|
}
|
|
260
262
|
if (metadata.has("db.column.measure")) this.measures.push(fieldName);
|
|
263
|
+
if (metadata.has("db.column.version")) if (this.versionField !== void 0) logger.warn(`@db.column.version declared on multiple fields ("${this.versionField}" and "${fieldName}") — only one is allowed; using "${this.versionField}"`);
|
|
264
|
+
else {
|
|
265
|
+
this.versionField = fieldName;
|
|
266
|
+
if (!this.defaults.has(fieldName)) this.defaults.set(fieldName, {
|
|
267
|
+
kind: "value",
|
|
268
|
+
value: "0"
|
|
269
|
+
});
|
|
270
|
+
}
|
|
261
271
|
}
|
|
262
272
|
_addIndexField(type, name, field, opts) {
|
|
263
273
|
const key = indexKey(type, name);
|
|
@@ -1249,6 +1259,17 @@ var AtscriptDbReadable = class {
|
|
|
1249
1259
|
}
|
|
1250
1260
|
return this._metaIdPhysical;
|
|
1251
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Physical column name of the field annotated with `@db.column.version`, or
|
|
1264
|
+
* `undefined` when the table has no version column. Used by adapters and the
|
|
1265
|
+
* REST integration to drive optimistic concurrency control (OCC).
|
|
1266
|
+
*/
|
|
1267
|
+
get versionColumn() {
|
|
1268
|
+
this._ensureBuilt();
|
|
1269
|
+
const field = this._meta.versionField;
|
|
1270
|
+
if (field === void 0) return void 0;
|
|
1271
|
+
return this._meta.columnMap.get(field) ?? field;
|
|
1272
|
+
}
|
|
1252
1273
|
/** Dimension fields from `@db.column.dimension`. */
|
|
1253
1274
|
get dimensions() {
|
|
1254
1275
|
this._ensureBuilt();
|
|
@@ -1940,7 +1961,7 @@ var BaseDbAdapter = class {
|
|
|
1940
1961
|
* @param patch - The patch payload with array operations.
|
|
1941
1962
|
* @returns Update result.
|
|
1942
1963
|
*/
|
|
1943
|
-
async nativePatch(_filter, _patch, _ops) {
|
|
1964
|
+
async nativePatch(_filter, _patch, _ops, _expectedVersion) {
|
|
1944
1965
|
throw new Error("Native patch not supported by this adapter");
|
|
1945
1966
|
}
|
|
1946
1967
|
/**
|
|
@@ -2473,6 +2494,37 @@ function decomposeArrayPatch(key, value, fieldType, update, _table) {
|
|
|
2473
2494
|
if (value.$remove !== void 0) update[`${key}.__$remove`] = value.$remove;
|
|
2474
2495
|
if (keyProps.size > 0 && (value.$upsert !== void 0 || value.$update !== void 0 || value.$remove !== void 0)) update[`${key}.__$keys`] = [...keyProps];
|
|
2475
2496
|
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Throws if the payload attempts to write the version column directly.
|
|
2499
|
+
*
|
|
2500
|
+
* The version column is server-managed: the adapter auto-increments it
|
|
2501
|
+
* on every update. Direct writes (plain SET, `$inc`, `$mul`) would
|
|
2502
|
+
* either silently no-op (overwritten by the auto-bump) or corrupt the
|
|
2503
|
+
* OCC invariant. Reject them at the patch layer so the failure is
|
|
2504
|
+
* loud and adapter-agnostic.
|
|
2505
|
+
*
|
|
2506
|
+
* Inspects the payload AFTER `separateCas` has stripped `$cas` (so
|
|
2507
|
+
* that the legitimate `$cas` → expectedVersion path is not flagged).
|
|
2508
|
+
*
|
|
2509
|
+
* The check is intentionally narrow: only the physical column name at
|
|
2510
|
+
* the top level of the (already-decomposed) payload. Nested objects
|
|
2511
|
+
* cannot reach the version column through dot-paths because the
|
|
2512
|
+
* decomposer flattens those into top-level dot-keys; the version
|
|
2513
|
+
* column, being a scalar at the root, cannot be nested.
|
|
2514
|
+
*
|
|
2515
|
+
* Zero-allocation when the version column is not present.
|
|
2516
|
+
*
|
|
2517
|
+
* @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
|
|
2518
|
+
* column appears as a top-level key in `data` or as the target of a
|
|
2519
|
+
* `$inc`/`$mul` field op.
|
|
2520
|
+
*/
|
|
2521
|
+
function assertNoVersionWrites(data, versionColumn) {
|
|
2522
|
+
if (!(versionColumn in data)) return;
|
|
2523
|
+
throw new require_db_error.DbError("VERSION_COLUMN_WRITE", [{
|
|
2524
|
+
path: versionColumn,
|
|
2525
|
+
message: `Cannot write to version column "${versionColumn}" directly; omit it from the payload or use $cas for conflict detection`
|
|
2526
|
+
}]);
|
|
2527
|
+
}
|
|
2476
2528
|
//#endregion
|
|
2477
2529
|
//#region src/table/db-table.ts
|
|
2478
2530
|
/**
|
|
@@ -2619,7 +2671,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2619
2671
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
2620
2672
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
2621
2673
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
2622
|
-
const
|
|
2674
|
+
const versionColumn = this.versionColumn;
|
|
2675
|
+
const expectedVersions = Array.from({ length: payloads.length });
|
|
2676
|
+
const items = payloads.map((p, i) => {
|
|
2677
|
+
const clone = { ...p };
|
|
2678
|
+
expectedVersions[i] = require_ops.separateCas(clone, versionColumn);
|
|
2679
|
+
return this._applyDefaults(clone);
|
|
2680
|
+
});
|
|
2623
2681
|
const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
|
|
2624
2682
|
const validator = this.getValidator("bulkReplace");
|
|
2625
2683
|
const ctx = {
|
|
@@ -2634,11 +2692,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2634
2692
|
if (canNest) await require_nested_writer.preValidateNestedFrom(host, originals);
|
|
2635
2693
|
let matchedCount = 0;
|
|
2636
2694
|
let modifiedCount = 0;
|
|
2637
|
-
for (
|
|
2695
|
+
for (let i = 0; i < items.length; i++) {
|
|
2696
|
+
const data = items[i];
|
|
2638
2697
|
for (const navField of this._meta.navFields) delete data[navField];
|
|
2698
|
+
if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
|
|
2639
2699
|
const filter = this._extractRecordFilter(data);
|
|
2640
2700
|
const prepared = this._fieldMapper.prepareForWrite(data, this._meta, this.adapter);
|
|
2641
|
-
const result = await this.adapter.replaceOne(this._fieldMapper.translateFilter(filter, this._meta), prepared);
|
|
2701
|
+
const result = await this.adapter.replaceOne(this._fieldMapper.translateFilter(filter, this._meta), prepared, expectedVersions[i]);
|
|
2642
2702
|
matchedCount += result.matchedCount;
|
|
2643
2703
|
modifiedCount += result.modifiedCount;
|
|
2644
2704
|
}
|
|
@@ -2671,6 +2731,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2671
2731
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
2672
2732
|
if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
|
|
2673
2733
|
return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
|
|
2734
|
+
const versionColumn = this.versionColumn;
|
|
2735
|
+
const expectedVersions = Array.from({ length: payloads.length });
|
|
2736
|
+
const cloned = payloads.map((p, i) => {
|
|
2737
|
+
const c = { ...p };
|
|
2738
|
+
expectedVersions[i] = require_ops.separateCas(c, versionColumn);
|
|
2739
|
+
return c;
|
|
2740
|
+
});
|
|
2674
2741
|
const validator = this.getValidator("bulkUpdate");
|
|
2675
2742
|
const ctx = {
|
|
2676
2743
|
mode: "patch",
|
|
@@ -2678,18 +2745,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2678
2745
|
navFields: this._meta.navFields
|
|
2679
2746
|
};
|
|
2680
2747
|
this._applyDepthCtx(ctx, depth);
|
|
2681
|
-
require_nested_writer.validateBatch(validator,
|
|
2682
|
-
const originals = canNest ?
|
|
2748
|
+
require_nested_writer.validateBatch(validator, cloned, ctx);
|
|
2749
|
+
const originals = canNest ? cloned.map((p) => ({ ...p })) : [];
|
|
2683
2750
|
const host = this;
|
|
2684
|
-
if (canNest) await require_nested_writer.batchPatchNestedTo(host,
|
|
2685
|
-
await this._integrity.validateForeignKeys(
|
|
2751
|
+
if (canNest) await require_nested_writer.batchPatchNestedTo(host, cloned, maxDepth, depth);
|
|
2752
|
+
await this._integrity.validateForeignKeys(cloned, this._meta, this._fkLookupResolver, this._writeTableResolver, true);
|
|
2686
2753
|
let matchedCount = 0;
|
|
2687
2754
|
let modifiedCount = 0;
|
|
2688
|
-
for (
|
|
2755
|
+
for (let i = 0; i < cloned.length; i++) {
|
|
2756
|
+
const payload = cloned[i];
|
|
2757
|
+
const expectedVersion = expectedVersions[i];
|
|
2689
2758
|
const data = { ...payload };
|
|
2690
2759
|
for (const navField of this._meta.navFields) delete data[navField];
|
|
2691
2760
|
const filter = this._extractRecordFilter(data);
|
|
2692
2761
|
for (const key of Object.keys(filter)) delete data[key];
|
|
2762
|
+
if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
|
|
2693
2763
|
if (_isEmptyObj(data)) {
|
|
2694
2764
|
matchedCount += 1;
|
|
2695
2765
|
modifiedCount += 0;
|
|
@@ -2701,7 +2771,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2701
2771
|
const ops = require_ops.separateFieldOps(data);
|
|
2702
2772
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
2703
2773
|
const translatedData = this._fieldMapper.translatePatchKeys(data, this._meta);
|
|
2704
|
-
result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps);
|
|
2774
|
+
result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion);
|
|
2705
2775
|
} else {
|
|
2706
2776
|
const update = decomposePatch(data, this);
|
|
2707
2777
|
const ops = require_ops.separateFieldOps(update);
|
|
@@ -2712,8 +2782,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2712
2782
|
filter: translatedFilter,
|
|
2713
2783
|
controls: {}
|
|
2714
2784
|
}), this);
|
|
2715
|
-
result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps);
|
|
2716
|
-
} else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps);
|
|
2785
|
+
result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion);
|
|
2786
|
+
} else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion);
|
|
2717
2787
|
}
|
|
2718
2788
|
matchedCount += result.matchedCount;
|
|
2719
2789
|
modifiedCount += result.modifiedCount;
|
|
@@ -2746,7 +2816,14 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2746
2816
|
async updateMany(filter, data) {
|
|
2747
2817
|
this._ensureBuilt();
|
|
2748
2818
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
|
|
2749
|
-
const
|
|
2819
|
+
const dataCopy = { ...data };
|
|
2820
|
+
const versionColumn = this.versionColumn;
|
|
2821
|
+
if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
2822
|
+
path: "$cas",
|
|
2823
|
+
message: "$cas is not supported on updateMany — use bulkUpdate with per-row $cas for version-locked batch updates"
|
|
2824
|
+
}]);
|
|
2825
|
+
if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
|
|
2826
|
+
const update = decomposePatch(dataCopy, this);
|
|
2750
2827
|
const ops = require_ops.separateFieldOps(update);
|
|
2751
2828
|
const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
|
|
2752
2829
|
const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
|
|
@@ -2787,17 +2864,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2787
2864
|
_applyDefaults(data) {
|
|
2788
2865
|
const nativeValues = this.adapter.supportsNativeValueDefaults();
|
|
2789
2866
|
const nativeFns = this.adapter.nativeDefaultFns();
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
data[field] =
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2867
|
+
const versionField = this._meta.versionField;
|
|
2868
|
+
for (const [field, def] of this._meta.defaults.entries()) {
|
|
2869
|
+
if (field === versionField) continue;
|
|
2870
|
+
if (data[field] === void 0) {
|
|
2871
|
+
if (def.kind === "value" && !nativeValues) {
|
|
2872
|
+
const fieldType = this._meta.flatMap?.get(field);
|
|
2873
|
+
data[field] = (fieldType?.type.kind === "" && fieldType.type.designType) === "string" ? def.value : JSON.parse(def.value);
|
|
2874
|
+
} else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
|
|
2875
|
+
case "now":
|
|
2876
|
+
data[field] = Date.now();
|
|
2877
|
+
break;
|
|
2878
|
+
case "uuid":
|
|
2879
|
+
data[field] = crypto.randomUUID();
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2801
2882
|
}
|
|
2802
2883
|
}
|
|
2803
2884
|
return data;
|
|
@@ -2922,6 +3003,22 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
2922
3003
|
const adapterPlugins = this.adapter.getValidatorPlugins();
|
|
2923
3004
|
if (purpose === "insert" || purpose === "patch" || purpose === "bulkReplace") {
|
|
2924
3005
|
const mode = purpose === "bulkReplace" ? "replace" : purpose;
|
|
3006
|
+
const versionField = this._meta.versionField;
|
|
3007
|
+
if (versionField !== void 0) {
|
|
3008
|
+
const plugins = adapterPlugins.length ? [...adapterPlugins, require_validator.dbPlugin] : [require_validator.dbPlugin];
|
|
3009
|
+
return this.createValidator({
|
|
3010
|
+
plugins,
|
|
3011
|
+
partial: mode === "patch",
|
|
3012
|
+
replace: (def, path) => {
|
|
3013
|
+
const transformed = require_validator.forceNavNonOptional(def);
|
|
3014
|
+
if (path === versionField && !transformed.optional) return {
|
|
3015
|
+
...transformed,
|
|
3016
|
+
optional: true
|
|
3017
|
+
};
|
|
3018
|
+
return transformed;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
2925
3022
|
return require_validator.buildDbValidator(this.type, mode, adapterPlugins);
|
|
2926
3023
|
}
|
|
2927
3024
|
if (purpose === "bulkUpdate") {
|
|
@@ -3156,6 +3253,12 @@ Object.defineProperty(exports, "UniquSelect", {
|
|
|
3156
3253
|
return UniquSelect;
|
|
3157
3254
|
}
|
|
3158
3255
|
});
|
|
3256
|
+
Object.defineProperty(exports, "assertNoVersionWrites", {
|
|
3257
|
+
enumerable: true,
|
|
3258
|
+
get: function() {
|
|
3259
|
+
return assertNoVersionWrites;
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3159
3262
|
Object.defineProperty(exports, "decomposePatch", {
|
|
3160
3263
|
enumerable: true,
|
|
3161
3264
|
get: function() {
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_db_error = require("./db-error-
|
|
3
|
-
const require_db_view = require("./db-view-
|
|
2
|
+
const require_db_error = require("./db-error-CrtPzaZ-.cjs");
|
|
3
|
+
const require_db_view = require("./db-view-da5OTSRH.cjs");
|
|
4
4
|
const require_ops = require("./ops.cjs");
|
|
5
|
-
const require_validator = require("./validator-
|
|
5
|
+
const require_validator = require("./validator-DrmUaZA3.cjs");
|
|
6
6
|
let _uniqu_core = require("@uniqu/core");
|
|
7
|
+
//#region src/with-optimistic-retry.ts
|
|
8
|
+
/**
|
|
9
|
+
* Runs a read-modify-write loop under optimistic concurrency control (OCC).
|
|
10
|
+
*
|
|
11
|
+
* Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
|
|
12
|
+
* the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
|
|
13
|
+
* a version conflict (`matchedCount === 0`) it re-reads the row, calls the
|
|
14
|
+
* mutator with the fresh state, and retries — up to `maxAttempts` times.
|
|
15
|
+
*
|
|
16
|
+
* The filter (typically the primary key) is threaded into the update payload
|
|
17
|
+
* so the table layer can extract the row identity. If `mutator` returns
|
|
18
|
+
* fields that overlap with the filter, the patch wins (last-write semantics
|
|
19
|
+
* inside a single object spread).
|
|
20
|
+
*
|
|
21
|
+
* @throws {DbError} with code `INVALID_QUERY` if `table` has no
|
|
22
|
+
* `@db.column.version` column — the helper would have no version to thread
|
|
23
|
+
* into `$cas` and would silently degrade to last-write-wins.
|
|
24
|
+
* @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
|
|
25
|
+
* `null`. The mutator is not invoked with a fabricated row.
|
|
26
|
+
* @throws {CasExhaustedError} if `maxAttempts` is reached without a
|
|
27
|
+
* successful commit.
|
|
28
|
+
*/
|
|
29
|
+
async function withOptimisticRetry(table, filter, mutator, opts) {
|
|
30
|
+
const versionColumn = table.versionColumn;
|
|
31
|
+
if (versionColumn === void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
32
|
+
path: "$cas",
|
|
33
|
+
message: `withOptimisticRetry: table "${table.tableName}" has no @db.column.version column — CAS cannot be applied`
|
|
34
|
+
}]);
|
|
35
|
+
const maxAttempts = opts?.maxAttempts ?? 5;
|
|
36
|
+
let lastSeenVersion;
|
|
37
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
38
|
+
const row = await table.findOne({ filter });
|
|
39
|
+
if (row === null) throw new require_db_error.DbError("NOT_FOUND", [{
|
|
40
|
+
path: "$cas",
|
|
41
|
+
message: `withOptimisticRetry: row not found in "${table.tableName}" for filter ${JSON.stringify(filter)}`
|
|
42
|
+
}]);
|
|
43
|
+
lastSeenVersion = row[versionColumn];
|
|
44
|
+
const patch = await mutator(row);
|
|
45
|
+
const result = await table.updateOne({
|
|
46
|
+
...filter,
|
|
47
|
+
...patch,
|
|
48
|
+
$cas: { [versionColumn]: lastSeenVersion }
|
|
49
|
+
});
|
|
50
|
+
if (result.matchedCount > 0) return result;
|
|
51
|
+
if (attempt < maxAttempts && opts?.delay !== void 0) await opts.delay(attempt);
|
|
52
|
+
}
|
|
53
|
+
throw new require_db_error.CasExhaustedError(maxAttempts, lastSeenVersion);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
7
56
|
//#region src/table/db-space.ts
|
|
8
57
|
/**
|
|
9
58
|
* A database space — a registry of tables and views sharing the same adapter type and driver.
|
|
@@ -139,6 +188,7 @@ function translateQueryTree(node, resolveField) {
|
|
|
139
188
|
return { [leftField]: { [comp.op]: comp.right } };
|
|
140
189
|
}
|
|
141
190
|
//#endregion
|
|
191
|
+
exports.$cas = require_ops.$cas;
|
|
142
192
|
exports.$dec = require_ops.$dec;
|
|
143
193
|
exports.$inc = require_ops.$inc;
|
|
144
194
|
exports.$insert = require_ops.$insert;
|
|
@@ -152,6 +202,7 @@ exports.AtscriptDbReadable = require_db_view.AtscriptDbReadable;
|
|
|
152
202
|
exports.AtscriptDbTable = require_db_view.AtscriptDbTable;
|
|
153
203
|
exports.AtscriptDbView = require_db_view.AtscriptDbView;
|
|
154
204
|
exports.BaseDbAdapter = require_db_view.BaseDbAdapter;
|
|
205
|
+
exports.CasExhaustedError = require_db_error.CasExhaustedError;
|
|
155
206
|
exports.DbError = require_db_error.DbError;
|
|
156
207
|
exports.DbSpace = DbSpace;
|
|
157
208
|
exports.DocumentFieldMapper = require_db_view.DocumentFieldMapper;
|
|
@@ -162,6 +213,7 @@ exports.NoopLogger = require_db_view.NoopLogger;
|
|
|
162
213
|
exports.RelationalFieldMapper = require_db_view.RelationalFieldMapper;
|
|
163
214
|
exports.TableMetadata = require_db_view.TableMetadata;
|
|
164
215
|
exports.UniquSelect = require_db_view.UniquSelect;
|
|
216
|
+
exports.assertNoVersionWrites = require_db_view.assertNoVersionWrites;
|
|
165
217
|
exports.buildDbValidator = require_validator.buildDbValidator;
|
|
166
218
|
exports.buildValidationContext = require_validator.buildValidationContext;
|
|
167
219
|
Object.defineProperty(exports, "computeInsights", {
|
|
@@ -184,6 +236,7 @@ Object.defineProperty(exports, "isPrimitive", {
|
|
|
184
236
|
}
|
|
185
237
|
});
|
|
186
238
|
exports.resolveDesignType = require_db_view.resolveDesignType;
|
|
239
|
+
exports.separateCas = require_ops.separateCas;
|
|
187
240
|
exports.separateFieldOps = require_ops.separateFieldOps;
|
|
188
241
|
exports.translateQueryTree = translateQueryTree;
|
|
189
242
|
Object.defineProperty(exports, "walkFilter", {
|
|
@@ -192,3 +245,4 @@ Object.defineProperty(exports, "walkFilter", {
|
|
|
192
245
|
return _uniqu_core.walkFilter;
|
|
193
246
|
}
|
|
194
247
|
});
|
|
248
|
+
exports.withOptimisticRetry = withOptimisticRetry;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-
|
|
2
|
-
import { a as $
|
|
3
|
-
import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-
|
|
1
|
+
import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-DgIxrEVQ.cjs";
|
|
2
|
+
import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-lzmfzuY9.cjs";
|
|
3
|
+
import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-DiErv7Q1.cjs";
|
|
4
4
|
import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DRGMCEn3.cjs";
|
|
5
5
|
import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-_z_A3cKa.cjs";
|
|
6
6
|
import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
|
|
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
|
|
|
81
81
|
}
|
|
82
82
|
//#endregion
|
|
83
83
|
//#region src/db-error.d.ts
|
|
84
|
-
type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED";
|
|
84
|
+
type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
|
|
85
85
|
declare class DbError extends Error {
|
|
86
86
|
readonly code: DbErrorCode;
|
|
87
87
|
readonly errors: Array<{
|
|
@@ -94,6 +94,52 @@ declare class DbError extends Error {
|
|
|
94
94
|
message: string;
|
|
95
95
|
}>, message?: string);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
|
|
99
|
+
* without a successful CAS commit — the target row kept changing under
|
|
100
|
+
* the read-modify-write loop. Surfaces the attempt count and the
|
|
101
|
+
* last-observed version so callers can log/report the contention.
|
|
102
|
+
*/
|
|
103
|
+
declare class CasExhaustedError extends DbError {
|
|
104
|
+
readonly attempts: number;
|
|
105
|
+
readonly lastSeenVersion: number | undefined;
|
|
106
|
+
name: string;
|
|
107
|
+
constructor(attempts: number, lastSeenVersion: number | undefined);
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/with-optimistic-retry.d.ts
|
|
111
|
+
interface WithOptimisticRetryOptions {
|
|
112
|
+
/** Maximum number of attempts before giving up. Defaults to `5`. */
|
|
113
|
+
maxAttempts?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Optional hook invoked between failed attempts. Receives the 1-based
|
|
116
|
+
* attempt number that just failed. Useful for exponential backoff with
|
|
117
|
+
* jitter. Default behavior is to retry immediately.
|
|
118
|
+
*/
|
|
119
|
+
delay?: (attempt: number) => Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Runs a read-modify-write loop under optimistic concurrency control (OCC).
|
|
123
|
+
*
|
|
124
|
+
* Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
|
|
125
|
+
* the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
|
|
126
|
+
* a version conflict (`matchedCount === 0`) it re-reads the row, calls the
|
|
127
|
+
* mutator with the fresh state, and retries — up to `maxAttempts` times.
|
|
128
|
+
*
|
|
129
|
+
* The filter (typically the primary key) is threaded into the update payload
|
|
130
|
+
* so the table layer can extract the row identity. If `mutator` returns
|
|
131
|
+
* fields that overlap with the filter, the patch wins (last-write semantics
|
|
132
|
+
* inside a single object spread).
|
|
133
|
+
*
|
|
134
|
+
* @throws {DbError} with code `INVALID_QUERY` if `table` has no
|
|
135
|
+
* `@db.column.version` column — the helper would have no version to thread
|
|
136
|
+
* into `$cas` and would silently degrade to last-write-wins.
|
|
137
|
+
* @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
|
|
138
|
+
* `null`. The mutator is not invoked with a fabricated row.
|
|
139
|
+
* @throws {CasExhaustedError} if `maxAttempts` is reached without a
|
|
140
|
+
* successful commit.
|
|
141
|
+
*/
|
|
142
|
+
declare function withOptimisticRetry<TRow extends Record<string, unknown>>(table: AtscriptDbTable, filter: Record<string, unknown>, mutator: (row: TRow) => Promise<Record<string, unknown>> | Record<string, unknown>, opts?: WithOptimisticRetryOptions): Promise<TDbUpdateResult>;
|
|
97
143
|
//#endregion
|
|
98
144
|
//#region src/patch/patch-decomposer.d.ts
|
|
99
145
|
/**
|
|
@@ -113,5 +159,30 @@ declare class DbError extends Error {
|
|
|
113
159
|
* @returns A flat update object suitable for a basic `updateOne` call.
|
|
114
160
|
*/
|
|
115
161
|
declare function decomposePatch(payload: Record<string, unknown>, table: AtscriptDbTable): Record<string, unknown>;
|
|
162
|
+
/**
|
|
163
|
+
* Throws if the payload attempts to write the version column directly.
|
|
164
|
+
*
|
|
165
|
+
* The version column is server-managed: the adapter auto-increments it
|
|
166
|
+
* on every update. Direct writes (plain SET, `$inc`, `$mul`) would
|
|
167
|
+
* either silently no-op (overwritten by the auto-bump) or corrupt the
|
|
168
|
+
* OCC invariant. Reject them at the patch layer so the failure is
|
|
169
|
+
* loud and adapter-agnostic.
|
|
170
|
+
*
|
|
171
|
+
* Inspects the payload AFTER `separateCas` has stripped `$cas` (so
|
|
172
|
+
* that the legitimate `$cas` → expectedVersion path is not flagged).
|
|
173
|
+
*
|
|
174
|
+
* The check is intentionally narrow: only the physical column name at
|
|
175
|
+
* the top level of the (already-decomposed) payload. Nested objects
|
|
176
|
+
* cannot reach the version column through dot-paths because the
|
|
177
|
+
* decomposer flattens those into top-level dot-keys; the version
|
|
178
|
+
* column, being a scalar at the root, cannot be nested.
|
|
179
|
+
*
|
|
180
|
+
* Zero-allocation when the version column is not present.
|
|
181
|
+
*
|
|
182
|
+
* @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
|
|
183
|
+
* column appears as a top-level key in `data` or as the target of a
|
|
184
|
+
* `$inc`/`$mul` field op.
|
|
185
|
+
*/
|
|
186
|
+
declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
|
|
116
187
|
//#endregion
|
|
117
|
-
export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithRelation, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateFieldOps, translateQueryTree, walkFilter };
|
|
188
|
+
export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-
|
|
2
|
-
import { a as $
|
|
3
|
-
import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-
|
|
1
|
+
import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-BUUVDkLf.mjs";
|
|
2
|
+
import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-Blqr0ipy.mjs";
|
|
3
|
+
import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-CZooARt_.mjs";
|
|
4
4
|
import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DDvYyv5t.mjs";
|
|
5
5
|
import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-DttN2e5_.mjs";
|
|
6
6
|
import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
|
|
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
|
|
|
81
81
|
}
|
|
82
82
|
//#endregion
|
|
83
83
|
//#region src/db-error.d.ts
|
|
84
|
-
type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED";
|
|
84
|
+
type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
|
|
85
85
|
declare class DbError extends Error {
|
|
86
86
|
readonly code: DbErrorCode;
|
|
87
87
|
readonly errors: Array<{
|
|
@@ -94,6 +94,52 @@ declare class DbError extends Error {
|
|
|
94
94
|
message: string;
|
|
95
95
|
}>, message?: string);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
|
|
99
|
+
* without a successful CAS commit — the target row kept changing under
|
|
100
|
+
* the read-modify-write loop. Surfaces the attempt count and the
|
|
101
|
+
* last-observed version so callers can log/report the contention.
|
|
102
|
+
*/
|
|
103
|
+
declare class CasExhaustedError extends DbError {
|
|
104
|
+
readonly attempts: number;
|
|
105
|
+
readonly lastSeenVersion: number | undefined;
|
|
106
|
+
name: string;
|
|
107
|
+
constructor(attempts: number, lastSeenVersion: number | undefined);
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/with-optimistic-retry.d.ts
|
|
111
|
+
interface WithOptimisticRetryOptions {
|
|
112
|
+
/** Maximum number of attempts before giving up. Defaults to `5`. */
|
|
113
|
+
maxAttempts?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Optional hook invoked between failed attempts. Receives the 1-based
|
|
116
|
+
* attempt number that just failed. Useful for exponential backoff with
|
|
117
|
+
* jitter. Default behavior is to retry immediately.
|
|
118
|
+
*/
|
|
119
|
+
delay?: (attempt: number) => Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Runs a read-modify-write loop under optimistic concurrency control (OCC).
|
|
123
|
+
*
|
|
124
|
+
* Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
|
|
125
|
+
* the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
|
|
126
|
+
* a version conflict (`matchedCount === 0`) it re-reads the row, calls the
|
|
127
|
+
* mutator with the fresh state, and retries — up to `maxAttempts` times.
|
|
128
|
+
*
|
|
129
|
+
* The filter (typically the primary key) is threaded into the update payload
|
|
130
|
+
* so the table layer can extract the row identity. If `mutator` returns
|
|
131
|
+
* fields that overlap with the filter, the patch wins (last-write semantics
|
|
132
|
+
* inside a single object spread).
|
|
133
|
+
*
|
|
134
|
+
* @throws {DbError} with code `INVALID_QUERY` if `table` has no
|
|
135
|
+
* `@db.column.version` column — the helper would have no version to thread
|
|
136
|
+
* into `$cas` and would silently degrade to last-write-wins.
|
|
137
|
+
* @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
|
|
138
|
+
* `null`. The mutator is not invoked with a fabricated row.
|
|
139
|
+
* @throws {CasExhaustedError} if `maxAttempts` is reached without a
|
|
140
|
+
* successful commit.
|
|
141
|
+
*/
|
|
142
|
+
declare function withOptimisticRetry<TRow extends Record<string, unknown>>(table: AtscriptDbTable, filter: Record<string, unknown>, mutator: (row: TRow) => Promise<Record<string, unknown>> | Record<string, unknown>, opts?: WithOptimisticRetryOptions): Promise<TDbUpdateResult>;
|
|
97
143
|
//#endregion
|
|
98
144
|
//#region src/patch/patch-decomposer.d.ts
|
|
99
145
|
/**
|
|
@@ -113,5 +159,30 @@ declare class DbError extends Error {
|
|
|
113
159
|
* @returns A flat update object suitable for a basic `updateOne` call.
|
|
114
160
|
*/
|
|
115
161
|
declare function decomposePatch(payload: Record<string, unknown>, table: AtscriptDbTable): Record<string, unknown>;
|
|
162
|
+
/**
|
|
163
|
+
* Throws if the payload attempts to write the version column directly.
|
|
164
|
+
*
|
|
165
|
+
* The version column is server-managed: the adapter auto-increments it
|
|
166
|
+
* on every update. Direct writes (plain SET, `$inc`, `$mul`) would
|
|
167
|
+
* either silently no-op (overwritten by the auto-bump) or corrupt the
|
|
168
|
+
* OCC invariant. Reject them at the patch layer so the failure is
|
|
169
|
+
* loud and adapter-agnostic.
|
|
170
|
+
*
|
|
171
|
+
* Inspects the payload AFTER `separateCas` has stripped `$cas` (so
|
|
172
|
+
* that the legitimate `$cas` → expectedVersion path is not flagged).
|
|
173
|
+
*
|
|
174
|
+
* The check is intentionally narrow: only the physical column name at
|
|
175
|
+
* the top level of the (already-decomposed) payload. Nested objects
|
|
176
|
+
* cannot reach the version column through dot-paths because the
|
|
177
|
+
* decomposer flattens those into top-level dot-keys; the version
|
|
178
|
+
* column, being a scalar at the root, cannot be nested.
|
|
179
|
+
*
|
|
180
|
+
* Zero-allocation when the version column is not present.
|
|
181
|
+
*
|
|
182
|
+
* @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
|
|
183
|
+
* column appears as a top-level key in `data` or as the target of a
|
|
184
|
+
* `$inc`/`$mul` field op.
|
|
185
|
+
*/
|
|
186
|
+
declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
|
|
116
187
|
//#endregion
|
|
117
|
-
export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithRelation, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateFieldOps, translateQueryTree, walkFilter };
|
|
188
|
+
export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
|