@atscript/db 0.1.83 → 0.1.84

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.
Files changed (37) hide show
  1. package/dist/{db-error-D8tQhNgM.cjs → db-error-CrtPzaZ-.cjs} +24 -0
  2. package/dist/{db-error-Cepx-RsQ.mjs → db-error-yx2jdg8B.mjs} +19 -1
  3. package/dist/{db-readable-B2TRNZHv.d.mts → db-readable-BUUVDkLf.d.mts} +18 -4
  4. package/dist/{db-readable-BSrIy08e.d.cts → db-readable-DgIxrEVQ.d.cts} +18 -4
  5. package/dist/{db-space-Dx8x-Ke1.d.cts → db-space-CZooARt_.d.mts} +2 -2
  6. package/dist/{db-space-SUFqMvKz.d.mts → db-space-DiErv7Q1.d.cts} +2 -2
  7. package/dist/{db-view-CqEIhTFa.mjs → db-view-Dfw-FP5o.mjs} +126 -29
  8. package/dist/{db-view-Bf5P3_7k.cjs → db-view-da5OTSRH.cjs} +130 -27
  9. package/dist/index.cjs +57 -3
  10. package/dist/index.d.cts +76 -5
  11. package/dist/index.d.mts +76 -5
  12. package/dist/index.mjs +54 -5
  13. package/dist/{nested-writer-CT2rLURx.mjs → nested-writer-CVMRAPoF.mjs} +1 -1
  14. package/dist/{nested-writer-v_LPR1yJ.cjs → nested-writer-Chl_zySG.cjs} +1 -1
  15. package/dist/{ops-C61kelof.d.mts → ops-Blqr0ipy.d.mts} +48 -1
  16. package/dist/{ops-DXJ4Zw0P.d.cts → ops-lzmfzuY9.d.cts} +48 -1
  17. package/dist/ops.cjs +73 -0
  18. package/dist/ops.d.cts +2 -2
  19. package/dist/ops.d.mts +2 -2
  20. package/dist/ops.mjs +72 -1
  21. package/dist/plugin.cjs +24 -1
  22. package/dist/plugin.mjs +24 -1
  23. package/dist/rel.cjs +1 -1
  24. package/dist/rel.d.cts +1 -1
  25. package/dist/rel.d.mts +1 -1
  26. package/dist/rel.mjs +1 -1
  27. package/dist/sync.cjs +1 -1
  28. package/dist/sync.d.cts +2 -2
  29. package/dist/sync.d.mts +2 -2
  30. package/dist/sync.mjs +1 -1
  31. package/dist/{validator-BB5h1Le3.mjs → validator-DTDf9yWe.mjs} +1 -1
  32. package/dist/{validator-BIuw_T0k.cjs → validator-DrmUaZA3.cjs} +1 -1
  33. package/dist/validator.cjs +2 -1
  34. package/dist/validator.d.cts +2 -2
  35. package/dist/validator.d.mts +2 -2
  36. package/dist/validator.mjs +3 -3
  37. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,8 +1,57 @@
1
- import { t as DbError } from "./db-error-Cepx-RsQ.mjs";
2
- import { a as BaseDbAdapter, c as AtscriptDbReadable, d as DocumentFieldMapper, f as FieldMappingStrategy, h as NoopLogger, i as ApplicationIntegrity, l as resolveDesignType, m as TableMetadata, n as AtscriptDbTable, o as IntegrityStrategy, p as UniquSelect, r as decomposePatch, s as NativeIntegrity, t as AtscriptDbView, u as RelationalFieldMapper } from "./db-view-CqEIhTFa.mjs";
3
- import { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateFieldOps } from "./ops.mjs";
4
- import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-BB5h1Le3.mjs";
1
+ import { n as DbError, t as CasExhaustedError } from "./db-error-yx2jdg8B.mjs";
2
+ import { a as ApplicationIntegrity, c as NativeIntegrity, d as RelationalFieldMapper, f as DocumentFieldMapper, g as NoopLogger, h as TableMetadata, i as decomposePatch, l as AtscriptDbReadable, m as UniquSelect, n as AtscriptDbTable, o as BaseDbAdapter, p as FieldMappingStrategy, r as assertNoVersionWrites, s as IntegrityStrategy, t as AtscriptDbView, u as resolveDesignType } from "./db-view-Dfw-FP5o.mjs";
3
+ import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps } from "./ops.mjs";
4
+ import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-DTDf9yWe.mjs";
5
5
  import { computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
6
+ //#region src/with-optimistic-retry.ts
7
+ /**
8
+ * Runs a read-modify-write loop under optimistic concurrency control (OCC).
9
+ *
10
+ * Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
11
+ * the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
12
+ * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
13
+ * mutator with the fresh state, and retries — up to `maxAttempts` times.
14
+ *
15
+ * The filter (typically the primary key) is threaded into the update payload
16
+ * so the table layer can extract the row identity. If `mutator` returns
17
+ * fields that overlap with the filter, the patch wins (last-write semantics
18
+ * inside a single object spread).
19
+ *
20
+ * @throws {DbError} with code `INVALID_QUERY` if `table` has no
21
+ * `@db.column.version` column — the helper would have no version to thread
22
+ * into `$cas` and would silently degrade to last-write-wins.
23
+ * @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
24
+ * `null`. The mutator is not invoked with a fabricated row.
25
+ * @throws {CasExhaustedError} if `maxAttempts` is reached without a
26
+ * successful commit.
27
+ */
28
+ async function withOptimisticRetry(table, filter, mutator, opts) {
29
+ const versionColumn = table.versionColumn;
30
+ if (versionColumn === void 0) throw new DbError("INVALID_QUERY", [{
31
+ path: "$cas",
32
+ message: `withOptimisticRetry: table "${table.tableName}" has no @db.column.version column — CAS cannot be applied`
33
+ }]);
34
+ const maxAttempts = opts?.maxAttempts ?? 5;
35
+ let lastSeenVersion;
36
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
37
+ const row = await table.findOne({ filter });
38
+ if (row === null) throw new DbError("NOT_FOUND", [{
39
+ path: "$cas",
40
+ message: `withOptimisticRetry: row not found in "${table.tableName}" for filter ${JSON.stringify(filter)}`
41
+ }]);
42
+ lastSeenVersion = row[versionColumn];
43
+ const patch = await mutator(row);
44
+ const result = await table.updateOne({
45
+ ...filter,
46
+ ...patch,
47
+ $cas: { [versionColumn]: lastSeenVersion }
48
+ });
49
+ if (result.matchedCount > 0) return result;
50
+ if (attempt < maxAttempts && opts?.delay !== void 0) await opts.delay(attempt);
51
+ }
52
+ throw new CasExhaustedError(maxAttempts, lastSeenVersion);
53
+ }
54
+ //#endregion
6
55
  //#region src/table/db-space.ts
7
56
  /**
8
57
  * A database space — a registry of tables and views sharing the same adapter type and driver.
@@ -138,4 +187,4 @@ function translateQueryTree(node, resolveField) {
138
187
  return { [leftField]: { [comp.op]: comp.right } };
139
188
  }
140
189
  //#endregion
141
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateFieldOps, translateQueryTree, walkFilter };
190
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
@@ -1,4 +1,4 @@
1
- import { n as DepthLimitExceededError, t as DbError } from "./db-error-Cepx-RsQ.mjs";
1
+ import { n as DbError, r as DepthLimitExceededError } from "./db-error-yx2jdg8B.mjs";
2
2
  import { r as resolveRelationTargetTable } from "./relation-helpers-CLasawQq.mjs";
3
3
  import { ValidatorError } from "@atscript/typescript/utils";
4
4
  //#region src/table/error-utils.ts
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-D8tQhNgM.cjs");
1
+ const require_db_error = require("./db-error-CrtPzaZ-.cjs");
2
2
  const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
4
  //#region src/table/error-utils.ts
@@ -5,12 +5,37 @@ interface TDbFieldOp {
5
5
  $dec?: number;
6
6
  $mul?: number;
7
7
  }
8
+ /**
9
+ * A compare-and-set assertion: the update applies only if the version
10
+ * column currently equals `value`. Used inline in update payloads:
11
+ *
12
+ * { ...patch, $cas: { version: 4 } }
13
+ *
14
+ * The map shape is forward-compatible with multi-field CAS; v1 has
15
+ * exactly one entry keyed by the table's version column name.
16
+ */
17
+ interface TDbCas {
18
+ [versionColumn: string]: number;
19
+ }
8
20
  /** Increment a numeric field by `value` (default 1). */
9
21
  declare function $inc(value?: number): TDbFieldOp;
10
22
  /** Decrement a numeric field by `value` (default 1). */
11
23
  declare function $dec(value?: number): TDbFieldOp;
12
24
  /** Multiply a numeric field by `value`. */
13
25
  declare function $mul(value: number): TDbFieldOp;
26
+ /**
27
+ * Build a CAS marker for an inline payload.
28
+ *
29
+ * Use as a sibling to plain SET fields in an update payload:
30
+ *
31
+ * await users.updateOne({ id, status: 'active', ...$cas('version', 4) })
32
+ *
33
+ * The wrapped object can be spread directly so the marker stays a
34
+ * single, type-safe top-level entry.
35
+ */
36
+ declare function $cas(versionColumn: string, value: number): {
37
+ $cas: TDbCas;
38
+ };
14
39
  /** Replace the entire array. */
15
40
  declare function $replace<T>(items: T[]): {
16
41
  $replace: T[];
@@ -63,5 +88,27 @@ declare function getDbFieldOp(value: unknown): {
63
88
  * avoid intermediate `{ op, value }` objects, and short-circuits on typeof.
64
89
  */
65
90
  declare function separateFieldOps(data: Record<string, unknown>): TFieldOps | undefined;
91
+ /**
92
+ * Strips the top-level `$cas` operator from a write payload.
93
+ *
94
+ * Mutates `data` (deletes `$cas`) and returns the extracted expected
95
+ * version, or `undefined` if no `$cas` was present.
96
+ *
97
+ * The caller is expected to know the table's version column name and
98
+ * either trust the lookup or pass it explicitly for validation. v1
99
+ * accepts a single-entry `$cas` map; the returned number is the value
100
+ * of that entry.
101
+ *
102
+ * Errors (all thrown as {@link DbError}):
103
+ * - `$cas` is not a plain object → `"$cas operator: ..."`
104
+ * - `$cas` map is empty
105
+ * - `$cas` value is non-numeric / non-integer
106
+ * - `$cas` has more than one entry (v1 single-column constraint)
107
+ * - `$cas` key does not match `versionColumn` (when provided)
108
+ * - `$cas` is present on a non-versioned table (`versionColumn === undefined`)
109
+ *
110
+ * Zero-allocation on the no-op (no `$cas`) path.
111
+ */
112
+ declare function separateCas(data: Record<string, unknown>, versionColumn?: string): number | undefined;
66
113
  //#endregion
67
- export { $remove as a, $upsert as c, getDbFieldOp as d, isDbFieldOp as f, $mul as i, TDbFieldOp as l, $inc as n, $replace as o, separateFieldOps as p, $insert as r, $update as s, $dec as t, TFieldOps as u };
114
+ export { $mul as a, $update as c, TDbFieldOp as d, TFieldOps as f, separateFieldOps as g, separateCas as h, $insert as i, $upsert as l, isDbFieldOp as m, $dec as n, $remove as o, getDbFieldOp as p, $inc as r, $replace as s, $cas as t, TDbCas as u };
@@ -5,12 +5,37 @@ interface TDbFieldOp {
5
5
  $dec?: number;
6
6
  $mul?: number;
7
7
  }
8
+ /**
9
+ * A compare-and-set assertion: the update applies only if the version
10
+ * column currently equals `value`. Used inline in update payloads:
11
+ *
12
+ * { ...patch, $cas: { version: 4 } }
13
+ *
14
+ * The map shape is forward-compatible with multi-field CAS; v1 has
15
+ * exactly one entry keyed by the table's version column name.
16
+ */
17
+ interface TDbCas {
18
+ [versionColumn: string]: number;
19
+ }
8
20
  /** Increment a numeric field by `value` (default 1). */
9
21
  declare function $inc(value?: number): TDbFieldOp;
10
22
  /** Decrement a numeric field by `value` (default 1). */
11
23
  declare function $dec(value?: number): TDbFieldOp;
12
24
  /** Multiply a numeric field by `value`. */
13
25
  declare function $mul(value: number): TDbFieldOp;
26
+ /**
27
+ * Build a CAS marker for an inline payload.
28
+ *
29
+ * Use as a sibling to plain SET fields in an update payload:
30
+ *
31
+ * await users.updateOne({ id, status: 'active', ...$cas('version', 4) })
32
+ *
33
+ * The wrapped object can be spread directly so the marker stays a
34
+ * single, type-safe top-level entry.
35
+ */
36
+ declare function $cas(versionColumn: string, value: number): {
37
+ $cas: TDbCas;
38
+ };
14
39
  /** Replace the entire array. */
15
40
  declare function $replace<T>(items: T[]): {
16
41
  $replace: T[];
@@ -63,5 +88,27 @@ declare function getDbFieldOp(value: unknown): {
63
88
  * avoid intermediate `{ op, value }` objects, and short-circuits on typeof.
64
89
  */
65
90
  declare function separateFieldOps(data: Record<string, unknown>): TFieldOps | undefined;
91
+ /**
92
+ * Strips the top-level `$cas` operator from a write payload.
93
+ *
94
+ * Mutates `data` (deletes `$cas`) and returns the extracted expected
95
+ * version, or `undefined` if no `$cas` was present.
96
+ *
97
+ * The caller is expected to know the table's version column name and
98
+ * either trust the lookup or pass it explicitly for validation. v1
99
+ * accepts a single-entry `$cas` map; the returned number is the value
100
+ * of that entry.
101
+ *
102
+ * Errors (all thrown as {@link DbError}):
103
+ * - `$cas` is not a plain object → `"$cas operator: ..."`
104
+ * - `$cas` map is empty
105
+ * - `$cas` value is non-numeric / non-integer
106
+ * - `$cas` has more than one entry (v1 single-column constraint)
107
+ * - `$cas` key does not match `versionColumn` (when provided)
108
+ * - `$cas` is present on a non-versioned table (`versionColumn === undefined`)
109
+ *
110
+ * Zero-allocation on the no-op (no `$cas`) path.
111
+ */
112
+ declare function separateCas(data: Record<string, unknown>, versionColumn?: string): number | undefined;
66
113
  //#endregion
67
- export { $remove as a, $upsert as c, getDbFieldOp as d, isDbFieldOp as f, $mul as i, TDbFieldOp as l, $inc as n, $replace as o, separateFieldOps as p, $insert as r, $update as s, $dec as t, TFieldOps as u };
114
+ export { $mul as a, $update as c, TDbFieldOp as d, TFieldOps as f, separateFieldOps as g, separateCas as h, $insert as i, $upsert as l, isDbFieldOp as m, $dec as n, $remove as o, getDbFieldOp as p, $inc as r, $replace as s, $cas as t, TDbCas as u };
package/dist/ops.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_db_error = require("./db-error-CrtPzaZ-.cjs");
2
3
  //#region src/ops.ts
3
4
  /** Increment a numeric field by `value` (default 1). */
4
5
  function $inc(value = 1) {
@@ -12,6 +13,19 @@ function $dec(value = 1) {
12
13
  function $mul(value) {
13
14
  return { $mul: value };
14
15
  }
16
+ /**
17
+ * Build a CAS marker for an inline payload.
18
+ *
19
+ * Use as a sibling to plain SET fields in an update payload:
20
+ *
21
+ * await users.updateOne({ id, status: 'active', ...$cas('version', 4) })
22
+ *
23
+ * The wrapped object can be spread directly so the marker stays a
24
+ * single, type-safe top-level entry.
25
+ */
26
+ function $cas(versionColumn, value) {
27
+ return { $cas: { [versionColumn]: value } };
28
+ }
15
29
  /** Replace the entire array. */
16
30
  function $replace(items) {
17
31
  return { $replace: items };
@@ -109,7 +123,65 @@ function separateFieldOps(data) {
109
123
  }
110
124
  return ops;
111
125
  }
126
+ /**
127
+ * Strips the top-level `$cas` operator from a write payload.
128
+ *
129
+ * Mutates `data` (deletes `$cas`) and returns the extracted expected
130
+ * version, or `undefined` if no `$cas` was present.
131
+ *
132
+ * The caller is expected to know the table's version column name and
133
+ * either trust the lookup or pass it explicitly for validation. v1
134
+ * accepts a single-entry `$cas` map; the returned number is the value
135
+ * of that entry.
136
+ *
137
+ * Errors (all thrown as {@link DbError}):
138
+ * - `$cas` is not a plain object → `"$cas operator: ..."`
139
+ * - `$cas` map is empty
140
+ * - `$cas` value is non-numeric / non-integer
141
+ * - `$cas` has more than one entry (v1 single-column constraint)
142
+ * - `$cas` key does not match `versionColumn` (when provided)
143
+ * - `$cas` is present on a non-versioned table (`versionColumn === undefined`)
144
+ *
145
+ * Zero-allocation on the no-op (no `$cas`) path.
146
+ */
147
+ function separateCas(data, versionColumn) {
148
+ if (!("$cas" in data)) return void 0;
149
+ if (versionColumn === void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
150
+ path: "$cas",
151
+ message: "$cas operator: table has no @db.column.version; cannot use $cas"
152
+ }]);
153
+ const raw = data.$cas;
154
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new require_db_error.DbError("INVALID_QUERY", [{
155
+ path: "$cas",
156
+ message: "$cas operator: must be a plain object"
157
+ }]);
158
+ const map = raw;
159
+ let foundKey;
160
+ for (const k in map) {
161
+ if (foundKey !== void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
162
+ path: "$cas",
163
+ message: "$cas operator: expected exactly one entry (v1 single-column CAS)"
164
+ }]);
165
+ foundKey = k;
166
+ }
167
+ if (foundKey === void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
168
+ path: "$cas",
169
+ message: "$cas operator: must contain a single version entry"
170
+ }]);
171
+ if (versionColumn !== void 0 && foundKey !== versionColumn) throw new require_db_error.DbError("INVALID_QUERY", [{
172
+ path: `$cas.${foundKey}`,
173
+ message: `$cas operator: key "${foundKey}" does not match version column "${versionColumn}"`
174
+ }]);
175
+ const foundValue = map[foundKey];
176
+ if (typeof foundValue !== "number" || !Number.isInteger(foundValue)) throw new require_db_error.DbError("INVALID_QUERY", [{
177
+ path: `$cas.${foundKey}`,
178
+ message: `$cas operator: value for "${foundKey}" must be an integer`
179
+ }]);
180
+ delete data.$cas;
181
+ return foundValue;
182
+ }
112
183
  //#endregion
184
+ exports.$cas = $cas;
113
185
  exports.$dec = $dec;
114
186
  exports.$inc = $inc;
115
187
  exports.$insert = $insert;
@@ -120,4 +192,5 @@ exports.$update = $update;
120
192
  exports.$upsert = $upsert;
121
193
  exports.getDbFieldOp = getDbFieldOp;
122
194
  exports.isDbFieldOp = isDbFieldOp;
195
+ exports.separateCas = separateCas;
123
196
  exports.separateFieldOps = separateFieldOps;
package/dist/ops.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-DXJ4Zw0P.cjs";
2
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateFieldOps };
1
+ 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";
2
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
package/dist/ops.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-C61kelof.mjs";
2
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateFieldOps };
1
+ 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";
2
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
package/dist/ops.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as DbError } from "./db-error-yx2jdg8B.mjs";
1
2
  //#region src/ops.ts
2
3
  /** Increment a numeric field by `value` (default 1). */
3
4
  function $inc(value = 1) {
@@ -11,6 +12,19 @@ function $dec(value = 1) {
11
12
  function $mul(value) {
12
13
  return { $mul: value };
13
14
  }
15
+ /**
16
+ * Build a CAS marker for an inline payload.
17
+ *
18
+ * Use as a sibling to plain SET fields in an update payload:
19
+ *
20
+ * await users.updateOne({ id, status: 'active', ...$cas('version', 4) })
21
+ *
22
+ * The wrapped object can be spread directly so the marker stays a
23
+ * single, type-safe top-level entry.
24
+ */
25
+ function $cas(versionColumn, value) {
26
+ return { $cas: { [versionColumn]: value } };
27
+ }
14
28
  /** Replace the entire array. */
15
29
  function $replace(items) {
16
30
  return { $replace: items };
@@ -108,5 +122,62 @@ function separateFieldOps(data) {
108
122
  }
109
123
  return ops;
110
124
  }
125
+ /**
126
+ * Strips the top-level `$cas` operator from a write payload.
127
+ *
128
+ * Mutates `data` (deletes `$cas`) and returns the extracted expected
129
+ * version, or `undefined` if no `$cas` was present.
130
+ *
131
+ * The caller is expected to know the table's version column name and
132
+ * either trust the lookup or pass it explicitly for validation. v1
133
+ * accepts a single-entry `$cas` map; the returned number is the value
134
+ * of that entry.
135
+ *
136
+ * Errors (all thrown as {@link DbError}):
137
+ * - `$cas` is not a plain object → `"$cas operator: ..."`
138
+ * - `$cas` map is empty
139
+ * - `$cas` value is non-numeric / non-integer
140
+ * - `$cas` has more than one entry (v1 single-column constraint)
141
+ * - `$cas` key does not match `versionColumn` (when provided)
142
+ * - `$cas` is present on a non-versioned table (`versionColumn === undefined`)
143
+ *
144
+ * Zero-allocation on the no-op (no `$cas`) path.
145
+ */
146
+ function separateCas(data, versionColumn) {
147
+ if (!("$cas" in data)) return void 0;
148
+ if (versionColumn === void 0) throw new DbError("INVALID_QUERY", [{
149
+ path: "$cas",
150
+ message: "$cas operator: table has no @db.column.version; cannot use $cas"
151
+ }]);
152
+ const raw = data.$cas;
153
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new DbError("INVALID_QUERY", [{
154
+ path: "$cas",
155
+ message: "$cas operator: must be a plain object"
156
+ }]);
157
+ const map = raw;
158
+ let foundKey;
159
+ for (const k in map) {
160
+ if (foundKey !== void 0) throw new DbError("INVALID_QUERY", [{
161
+ path: "$cas",
162
+ message: "$cas operator: expected exactly one entry (v1 single-column CAS)"
163
+ }]);
164
+ foundKey = k;
165
+ }
166
+ if (foundKey === void 0) throw new DbError("INVALID_QUERY", [{
167
+ path: "$cas",
168
+ message: "$cas operator: must contain a single version entry"
169
+ }]);
170
+ if (versionColumn !== void 0 && foundKey !== versionColumn) throw new DbError("INVALID_QUERY", [{
171
+ path: `$cas.${foundKey}`,
172
+ message: `$cas operator: key "${foundKey}" does not match version column "${versionColumn}"`
173
+ }]);
174
+ const foundValue = map[foundKey];
175
+ if (typeof foundValue !== "number" || !Number.isInteger(foundValue)) throw new DbError("INVALID_QUERY", [{
176
+ path: `$cas.${foundKey}`,
177
+ message: `$cas operator: value for "${foundKey}" must be an integer`
178
+ }]);
179
+ delete data.$cas;
180
+ return foundValue;
181
+ }
111
182
  //#endregion
112
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateFieldOps };
183
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
package/dist/plugin.cjs CHANGED
@@ -210,7 +210,30 @@ const dbColumnAnnotations = {
210
210
  }
211
211
  }),
212
212
  filterable: columnCapability("filterable", "filtering"),
213
- sortable: columnCapability("sortable", "sorting")
213
+ sortable: columnCapability("sortable", "sorting"),
214
+ version: new _atscript_core.AnnotationSpec({
215
+ description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
216
+ nodeType: ["prop"],
217
+ validate(token, _args, doc) {
218
+ const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.column.version", "number");
219
+ if (token.parentNode.has("optional")) errors.push({
220
+ message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
221
+ severity: 1,
222
+ range: token.range
223
+ });
224
+ const struct = require_validation_utils.getParentStruct(token);
225
+ if (struct) {
226
+ let count = 0;
227
+ for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
228
+ if (count > 1) errors.push({
229
+ message: "At most one @db.column.version per table",
230
+ severity: 1,
231
+ range: token.range
232
+ });
233
+ }
234
+ return errors;
235
+ }
236
+ })
214
237
  },
215
238
  default: {
216
239
  $self: new _atscript_core.AnnotationSpec({
package/dist/plugin.mjs CHANGED
@@ -206,7 +206,30 @@ const dbColumnAnnotations = {
206
206
  }
207
207
  }),
208
208
  filterable: columnCapability("filterable", "filtering"),
209
- sortable: columnCapability("sortable", "sorting")
209
+ sortable: columnCapability("sortable", "sorting"),
210
+ version: new AnnotationSpec({
211
+ description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
212
+ nodeType: ["prop"],
213
+ validate(token, _args, doc) {
214
+ const errors = validateFieldBaseType(token, doc, "@db.column.version", "number");
215
+ if (token.parentNode.has("optional")) errors.push({
216
+ message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
217
+ severity: 1,
218
+ range: token.range
219
+ });
220
+ const struct = getParentStruct(token);
221
+ if (struct) {
222
+ let count = 0;
223
+ for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
224
+ if (count > 1) errors.push({
225
+ message: "At most one @db.column.version per table",
226
+ severity: 1,
227
+ range: token.range
228
+ });
229
+ }
230
+ return errors;
231
+ }
232
+ })
210
233
  },
211
234
  default: {
212
235
  $self: new AnnotationSpec({
package/dist/rel.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_relation_loader = require("./relation-loader-CRC5LcqM.cjs");
3
3
  const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
4
- const require_nested_writer = require("./nested-writer-v_LPR1yJ.cjs");
4
+ const require_nested_writer = require("./nested-writer-Chl_zySG.cjs");
5
5
  exports.batchInsertNestedFrom = require_nested_writer.batchInsertNestedFrom;
6
6
  exports.batchInsertNestedTo = require_nested_writer.batchInsertNestedTo;
7
7
  exports.batchInsertNestedVia = require_nested_writer.batchInsertNestedVia;
package/dist/rel.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as TDbForeignKey, H as TDbRelation, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger } from "./db-readable-BSrIy08e.cjs";
1
+ import { F as TDbForeignKey, H as TDbRelation, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger } from "./db-readable-DgIxrEVQ.cjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-DRGMCEn3.cjs";
3
3
  import { FilterExpr, WithRelation } from "@uniqu/core";
4
4
  import { Validator } from "@atscript/typescript/utils";
package/dist/rel.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as TDbForeignKey, H as TDbRelation, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger } from "./db-readable-B2TRNZHv.mjs";
1
+ import { F as TDbForeignKey, H as TDbRelation, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger } from "./db-readable-BUUVDkLf.mjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-DDvYyv5t.mjs";
3
3
  import { Validator } from "@atscript/typescript/utils";
4
4
  import { FilterExpr, WithRelation } from "@uniqu/core";
package/dist/rel.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { t as loadRelationsImpl } from "./relation-loader-BEOTXNcq.mjs";
2
2
  import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-CLasawQq.mjs";
3
- import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, n as batchInsertNestedTo, o as batchPatchNestedVia, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CT2rLURx.mjs";
3
+ import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, n as batchInsertNestedTo, o as batchPatchNestedVia, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CVMRAPoF.mjs";
4
4
  export { batchInsertNestedFrom, batchInsertNestedTo, batchInsertNestedVia, batchPatchNestedFrom, batchPatchNestedTo, batchPatchNestedVia, batchReplaceNestedFrom, batchReplaceNestedTo, batchReplaceNestedVia, checkDepthOverflow, findFKForRelation, findRemoteFK, loadRelationsImpl, preValidateNestedFrom, resolveRelationTargetTable, validateBatch };
package/dist/sync.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_view = require("./db-view-Bf5P3_7k.cjs");
2
+ const require_db_view = require("./db-view-da5OTSRH.cjs");
3
3
  //#region src/schema/schema-hash.ts
4
4
  /** Extracts sorted field snapshots from a readable's field descriptors. */
5
5
  function extractFieldSnapshots(fields, typeMapper) {
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as TColumnDiff, F as TDbForeignKey, G as TExistingColumn, K as TExistingTableOption, M as TDbDefaultValue, P as TDbFieldMeta, U as TDbStorageType, pt as TGenericLogger, rt as TTableOptionDiff, t as AtscriptDbReadable } from "./db-readable-BSrIy08e.cjs";
2
- import { r as AtscriptDbView, t as DbSpace } from "./db-space-Dx8x-Ke1.cjs";
1
+ import { C as TColumnDiff, F as TDbForeignKey, G as TExistingColumn, K as TExistingTableOption, M as TDbDefaultValue, P as TDbFieldMeta, U as TDbStorageType, pt as TGenericLogger, rt as TTableOptionDiff, t as AtscriptDbReadable } from "./db-readable-DgIxrEVQ.cjs";
2
+ import { r as AtscriptDbView, t as DbSpace } from "./db-space-DiErv7Q1.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as TColumnDiff, F as TDbForeignKey, G as TExistingColumn, K as TExistingTableOption, M as TDbDefaultValue, P as TDbFieldMeta, U as TDbStorageType, pt as TGenericLogger, rt as TTableOptionDiff, t as AtscriptDbReadable } from "./db-readable-B2TRNZHv.mjs";
2
- import { r as AtscriptDbView, t as DbSpace } from "./db-space-SUFqMvKz.mjs";
1
+ import { C as TColumnDiff, F as TDbForeignKey, G as TExistingColumn, K as TExistingTableOption, M as TDbDefaultValue, P as TDbFieldMeta, U as TDbStorageType, pt as TGenericLogger, rt as TTableOptionDiff, t as AtscriptDbReadable } from "./db-readable-BUUVDkLf.mjs";
2
+ import { r as AtscriptDbView, t as DbSpace } from "./db-space-CZooARt_.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
package/dist/sync.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { h as NoopLogger } from "./db-view-CqEIhTFa.mjs";
1
+ import { g as NoopLogger } from "./db-view-Dfw-FP5o.mjs";
2
2
  //#region src/schema/schema-hash.ts
3
3
  /** Extracts sorted field snapshots from a readable's field descriptors. */
4
4
  function extractFieldSnapshots(fields, typeMapper) {
@@ -1,4 +1,4 @@
1
- import { n as DepthLimitExceededError } from "./db-error-Cepx-RsQ.mjs";
1
+ import { r as DepthLimitExceededError } from "./db-error-yx2jdg8B.mjs";
2
2
  import { isDbFieldOp } from "./ops.mjs";
3
3
  import { flattenAnnotatedType } from "@atscript/typescript/utils";
4
4
  //#region src/patch/patch-types.ts
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-D8tQhNgM.cjs");
1
+ const require_db_error = require("./db-error-CrtPzaZ-.cjs");
2
2
  const require_ops = require("./ops.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
4
  //#region src/patch/patch-types.ts
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_ops = require("./ops.cjs");
3
- const require_validator = require("./validator-BIuw_T0k.cjs");
3
+ const require_validator = require("./validator-DrmUaZA3.cjs");
4
+ exports.$cas = require_ops.$cas;
4
5
  exports.$dec = require_ops.$dec;
5
6
  exports.$inc = require_ops.$inc;
6
7
  exports.$insert = require_ops.$insert;
@@ -1,4 +1,4 @@
1
- import { a as $remove, c as $upsert, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, r as $insert, s as $update, t as $dec } from "./ops-DXJ4Zw0P.cjs";
1
+ import { a as $mul, c as $update, d as TDbFieldOp, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-lzmfzuY9.cjs";
2
2
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DRGMCEn3.cjs";
3
3
  import { a as dbPlugin, 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";
4
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbValidationContext, TArrayPatch, TDbFieldOp, TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
4
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbValidationContext, TArrayPatch, TDbCas, TDbFieldOp, TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
@@ -1,4 +1,4 @@
1
- import { a as $remove, c as $upsert, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, r as $insert, s as $update, t as $dec } from "./ops-C61kelof.mjs";
1
+ import { a as $mul, c as $update, d as TDbFieldOp, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-Blqr0ipy.mjs";
2
2
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DDvYyv5t.mjs";
3
3
  import { a as dbPlugin, 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";
4
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbValidationContext, TArrayPatch, TDbFieldOp, TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
4
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbValidationContext, TArrayPatch, TDbCas, TDbFieldOp, TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
@@ -1,3 +1,3 @@
1
- import { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, isDbFieldOp } from "./ops.mjs";
2
- import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, r as dbPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-BB5h1Le3.mjs";
3
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
1
+ import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, isDbFieldOp } from "./ops.mjs";
2
+ import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, r as dbPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-DTDf9yWe.mjs";
3
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, buildDbValidator, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isNavRelation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db",
3
- "version": "0.1.83",
3
+ "version": "0.1.84",
4
4
  "description": "Database adapter utilities for atscript.",
5
5
  "keywords": [
6
6
  "atscript",