@atscript/db 0.1.127 → 0.1.129

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 (41) hide show
  1. package/dist/{db-error-DXwEzmYJ.cjs → db-error-C4JuLcvb.cjs} +27 -0
  2. package/dist/{db-error-BHPXOKzc.mjs → db-error-COrO58t5.mjs} +22 -1
  3. package/dist/{db-readable-BkAGccv9.d.mts → db-readable-B7eYWS5q.d.cts} +299 -17
  4. package/dist/{db-readable-C0nDKX8A.d.cts → db-readable-Bn1bV_eC.d.mts} +299 -17
  5. package/dist/{db-space-B_ASuDaR.d.mts → db-space-C2UCnGHd.d.cts} +108 -30
  6. package/dist/{db-space-CSntT6yS.d.cts → db-space-DdIPYD0Q.d.mts} +108 -30
  7. package/dist/{db-view-BP0Qbeux.cjs → db-view-CRBgkEp0.cjs} +786 -100
  8. package/dist/{db-view-C8rZM5_N.mjs → db-view-Dl0aDTiT.mjs} +733 -101
  9. package/dist/index.cjs +48 -3
  10. package/dist/index.d.cts +162 -37
  11. package/dist/index.d.mts +162 -37
  12. package/dist/index.mjs +37 -5
  13. package/dist/{nested-writer-DI-HeTky.mjs → nested-writer-CkDo-ZfH.mjs} +1 -1
  14. package/dist/{nested-writer-DoDhl3X3.cjs → nested-writer-DxPhmWFz.cjs} +1 -1
  15. package/dist/{ops-DJRnNTVo.d.cts → ops-AqhV7s9o.d.cts} +24 -1
  16. package/dist/{ops-DJRnNTVo.d.mts → ops-AqhV7s9o.d.mts} +24 -1
  17. package/dist/ops.cjs +44 -1
  18. package/dist/ops.d.cts +2 -2
  19. package/dist/ops.d.mts +2 -2
  20. package/dist/ops.mjs +44 -2
  21. package/dist/plugin.cjs +12 -5
  22. package/dist/plugin.mjs +12 -5
  23. package/dist/rel.cjs +2 -2
  24. package/dist/rel.d.cts +1 -1
  25. package/dist/rel.d.mts +1 -1
  26. package/dist/rel.mjs +2 -2
  27. package/dist/{relation-loader-BnUgJsUG.cjs → relation-loader-C8GOpNYJ.cjs} +1 -1
  28. package/dist/{relation-loader-BmeOMj0b.mjs → relation-loader-CUGcxJ18.mjs} +1 -1
  29. package/dist/sync.cjs +1270 -398
  30. package/dist/sync.d.cts +255 -18
  31. package/dist/sync.d.mts +255 -18
  32. package/dist/sync.mjs +1269 -399
  33. package/dist/{validator-0vRXN51D.mjs → validator-CeD_fqyW.mjs} +21 -4
  34. package/dist/{validator-CSGug4vg.cjs → validator-lkCJKuoo.cjs} +32 -3
  35. package/dist/{validator-BcBtg8yW.d.cts → validator-wBARmD68.d.cts} +57 -1
  36. package/dist/{validator-BcBtg8yW.d.mts → validator-wBARmD68.d.mts} +57 -1
  37. package/dist/validator.cjs +7 -1
  38. package/dist/validator.d.cts +3 -3
  39. package/dist/validator.d.mts +3 -3
  40. package/dist/validator.mjs +4 -3
  41. package/package.json +8 -8
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as TIdentification, A as TDbActionLevel, B as TDbIndexType, C as TCascadeResolver, D as TCrudPermissions, E as TCrudOp, F as TDbDeleteResult, G as TDbStorageType, H as TDbInsertResult, I as TDbFieldMeta, J as TExistingTableOption, K as TDbUpdateResult, L as TDbForeignKey, M as TDbCollation, N as TDbDefaultFn, O as TDbActionInfo, P as TDbDefaultValue, Q as TIdDescriptor, R as TDbIndex, S as PrimaryKeyOf, T as TColumnDiff, U as TDbReferentialAction, V as TDbInsertManyResult, W as TDbRelation, X as TFkLookupResolver, Y as TFieldMeta, Z as TFkLookupTarget, _ as FieldOpsFor, _t as TGenericLogger, a as TDbEncryptionOptions, at as TTableOptionDiff, b as NavPropsOf, c as BaseDbAdapter, ct as TWriteTableResolver, d as AggregateFn, dt as UniqueryControls, et as TMetaResponse, f as AggregateQuery, ft as WithRelation, g as DbQuery, gt as NoopLogger, h as DbControls, ht as isGeoPointType, i as DbEncryption, it as TSyncColumnResult, j as TDbActionProcessor, k as TDbActionIntent, l as AggregateControls, lt as TypedWithRelation, m as AtscriptDbWritable, mt as isGeoIndexableType, n as DbResponse, nt as TRelationInfo, o as DocumentFieldMapper, ot as TTableResolver, p as AggregateResult, pt as TableMetadata, q as TExistingColumn, r as resolveDesignType, rt as TSearchIndexInfo, s as FieldMappingStrategy, st as TValueFormatterPair, t as AtscriptDbReadable, tt as TMetadataOverrides, u as AggregateExpr, ut as Uniquery, v as FilterExpr, vt as UniquSelect, w as TCascadeTarget, x as OwnPropsOf, y as FlatOf, z as TDbIndexField } from "./db-readable-BkAGccv9.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-DJRnNTVo.mjs";
3
- import { a as TViewColumnMapping, c as AtscriptQueryNode, d as TViewPlan, f as translateQueryTree, h as NativeIntegrity, i as AtscriptDbView, l as AtscriptRef, m as IntegrityStrategy, n as TAdapterFactory, o as AtscriptQueryComparison, p as AtscriptDbTable, r as TDbSpaceOptions, s as AtscriptQueryFieldRef, t as DbSpace, u as TViewJoin } from "./db-space-B_ASuDaR.mjs";
1
+ import { $ as TDbWriteAction, A as TCrudPermissions, At as findAncestorInSet, B as TDbForeignKey, C as NullableOptional, Ct as TWriteOptions, D as TCascadeTarget, Dt as UniqueryControls, E as TCascadeResolver, Et as Uniquery, F as TDbCollation, Ft as UniquSelect, G as TDbInsertResult, H as TDbIndexField, I as TDbDefaultFn, J as TDbRelation, K as TDbObjectKind, L as TDbDefaultValue, M as TDbActionIntent, Mt as isGeoPointType, N as TDbActionLevel, Nt as NoopLogger, O as TColumnDiff, Ot as WithRelation, P as TDbActionProcessor, Pt as TGenericLogger, Q as TDbUpdateResult, R as TDbDeleteResult, S as NavPropsOf, St as TValueFormatterPair, T as PrimaryKeyOf, Tt as TypedWithRelation, U as TDbIndexType, V as TDbIndex, W as TDbInsertManyResult, X as TDbRemoveGuardContext, Y as TDbRemoveGuard, Z as TDbStorageType, _ as DbQuery, _t as TSearchIndexInfo, a as TDbEncryptionOptions, at as TExistingForeignKey, b as FilterExpr, bt as TTableResolver, c as BaseDbAdapter, ct as TFkLookupResolver, d as AggregateFn, dt as TIdentification, et as TDbWriteGuard, f as AggregateQuery, ft as TMetaResponse, g as DbPatch, gt as TRelationInfo, h as DbControls, ht as TReferencingForeignKey, i as DbEncryption, it as TExistingColumn, j as TDbActionInfo, jt as isGeoIndexableType, k as TCrudOp, kt as TableMetadata, l as AggregateControls, lt as TFkLookupTarget, m as AtscriptDbWritable, mt as TPrimaryKeyChange, n as DbResponse, nt as TDeleteOptions, o as DocumentFieldMapper, ot as TExistingTableOption, p as AggregateResult, pt as TMetadataOverrides, q as TDbReferentialAction, r as resolveDesignType, rt as TEnsureTableOptions, s as FieldMappingStrategy, st as TFieldMeta, t as AtscriptDbReadable, tt as TDbWriteGuardContext, u as AggregateExpr, ut as TIdDescriptor, v as DbRow, vt as TSyncColumnResult, w as OwnPropsOf, wt as TWriteTableResolver, x as FlatOf, xt as TTouchManyOptions, y as FieldOpsFor, yt as TTableOptionDiff, z as TDbFieldMeta } from "./db-readable-Bn1bV_eC.mjs";
2
+ import { _ as separateFieldOps, a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateCas, h as reconcileCas, 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-AqhV7s9o.mjs";
3
+ import { a as TViewColumnMapping, c as AtscriptQueryFieldRef, d as TViewJoin, f as TViewPlan, g as NativeIntegrity, h as IntegrityStrategy, i as AtscriptDbView, l as AtscriptQueryNode, m as AtscriptDbTable, n as TAdapterFactory, o as isAtscriptDbView, p as translateQueryTree, r as TDbSpaceOptions, s as AtscriptQueryComparison, t as DbSpace, u as AtscriptRef } from "./db-space-DdIPYD0Q.mjs";
4
+ import { _ as DbErrorCode, a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, g as DbError, h as CasMismatchError, l as isEmptyObject, m as CasExhaustedError, n as ValidatorMode, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isPlainObject } from "./validator-wBARmD68.mjs";
4
5
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
5
- import { a as buildValidationContext, c as isNavRelation, d as getKeyProps, l as TArrayPatch, n as ValidatorMode, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as TDbPatch } from "./validator-BcBtg8yW.mjs";
6
6
  import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
7
7
 
8
8
  //#region src/shared/consts.d.ts
@@ -88,33 +88,6 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
88
88
  private buildCascadeChildFilter;
89
89
  }
90
90
  //#endregion
91
- //#region src/db-error.d.ts
92
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH";
93
- declare class DbError extends Error {
94
- readonly code: DbErrorCode;
95
- readonly errors: Array<{
96
- path: string;
97
- message: string;
98
- }>;
99
- name: string;
100
- constructor(code: DbErrorCode, errors: Array<{
101
- path: string;
102
- message: string;
103
- }>, message?: string);
104
- }
105
- /**
106
- * Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
107
- * without a successful CAS commit — the target row kept changing under
108
- * the read-modify-write loop. Surfaces the attempt count and the
109
- * last-observed version so callers can log/report the contention.
110
- */
111
- declare class CasExhaustedError extends DbError {
112
- readonly attempts: number;
113
- readonly lastSeenVersion: number | undefined;
114
- name: string;
115
- constructor(attempts: number, lastSeenVersion: number | undefined);
116
- }
117
- //#endregion
118
91
  //#region src/query/query-guards.d.ts
119
92
  /**
120
93
  * Engine-agnostic query-time guards, applied in the core layer BEFORE filter
@@ -127,6 +100,11 @@ declare class CasExhaustedError extends DbError {
127
100
  * - `$geoWithin` on a non-geoPoint field → `FILTER_TYPE_MISMATCH`
128
101
  * - `$geoWithin` with a malformed circle → `INVALID_QUERY`
129
102
  * - `$geoWithin` on an adapter without geo support → `GEO_NOT_SUPPORTED`
103
+ * - every filter / `$sort` / `$select` / `$groupBy` / `$having` / aggregate
104
+ * path must resolve to physical storage on THIS adapter and pass the
105
+ * adapter's `canFilterField` / `canSortField` → `INVALID_QUERY`
106
+ * (see {@link guardPaths}). Runs after the checks above so `ENC_*` codes
107
+ * keep firing first for encrypted subtrees.
130
108
  */
131
109
  /** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
132
110
  declare function assertGeoPoint(point: unknown, path: string): asserts point is [number, number];
@@ -135,14 +113,156 @@ declare function assertGeoPoint(point: unknown, path: string): asserts point is
135
113
  * validating `$geoWithin` operator nodes.
136
114
  */
137
115
  declare function guardFilter(meta: TableMetadata, adapter: BaseDbAdapter, filter: FilterExpr$1 | undefined, encCode?: "ENC_FIELD_FILTER" | "ENC_FIELD_AGG"): void;
138
- /** Shared read-path guard: filter + $sort. */
139
- declare function guardQuery(meta: TableMetadata, adapter: BaseDbAdapter, query: {
116
+ /** The query positions a field path can appear in. */
117
+ type TQueryPathOp = "filter" | "sort" | "select" | "groupBy" | "having" | "aggregate";
118
+ /** Query shape accepted by {@link guardPaths} / {@link collectQueryPaths} — the raw (pre-translation) uniqu controls. */
119
+ interface TGuardedQuery {
140
120
  filter?: FilterExpr$1;
141
121
  controls?: {
142
122
  $sort?: unknown;
123
+ $select?: unknown;
124
+ $groupBy?: unknown;
125
+ $having?: unknown;
126
+ };
127
+ }
128
+ /**
129
+ * The one wording for a filter-node `$`-key that is not `$and` / `$or` /
130
+ * `$not` (uniqu's walker would treat it as a field named `$…`). The core
131
+ * guard and the HTTP gate both answer with it; `path` is the operator itself.
132
+ */
133
+ declare function unsupportedOperatorMessage(op: string): string;
134
+ /**
135
+ * Normalizes every accepted `$sort` form (`"a,-b"`, `["a", "-b"]`,
136
+ * `[{ a: 1 }]`, `{ a: 1 }`) into its field names. Mirrors the HTTP layer's
137
+ * walker so programmatic callers get the same acceptance set.
138
+ */
139
+ declare function sortFieldNames(sort: unknown): string[];
140
+ /** Every logical path a parsed query references, grouped by position (since 0.1.128). */
141
+ interface TQueryPathRefs {
142
+ /** Filter keys (recursing through `$and` / `$or` / `$not`), minus the geo predicates below. */
143
+ filter: string[];
144
+ /**
145
+ * Filter keys whose predicate is `$geoWithin`. Their shape and index support
146
+ * are validated by `guardFilter`, so the core path guard skips the adapter's
147
+ * scalar filter veto for them; the HTTP gate checks them like any filter path.
148
+ */
149
+ geoFilter: string[];
150
+ sort: string[];
151
+ select: string[];
152
+ groupBy: string[];
153
+ having: string[];
154
+ /** Aggregate `$field`s (`*` excluded). Only populated in aggregate mode. */
155
+ aggregate: string[];
156
+ /** `true` in aggregate mode: `$select` entries are aggregate expressions, aliases are exempt in `$sort` / `$having`. */
157
+ aggregateMode: boolean;
158
+ /**
159
+ * First filter-node `$`-key that is not `$and` / `$or` / `$not`. uniqu's
160
+ * walker would treat it as a field named `$…` — reject instead of 500.
161
+ */
162
+ unsupportedOperator?: string;
163
+ }
164
+ /**
165
+ * Walks the PARSED query structure (not the flattened insights map, which
166
+ * cannot tell `$select=assignee.name` from `$with=assignee($select=name)`)
167
+ * and returns the root paths per position. `$with` sub-trees are not
168
+ * visited — they are validated against their target relation separately.
169
+ *
170
+ * Aggregate mode is `aggregate` when given, else the presence of `$groupBy`.
171
+ * In aggregate mode `$select` entries are aggregate expressions whose
172
+ * `$field` is collected and whose alias (`$as`, else `fn_field` — the core's
173
+ * `resolveAlias`) is exempted from `$sort` / `$having`; outside it non-string
174
+ * `$select` entries are ignored (the projection seal handles them).
175
+ */
176
+ declare function collectQueryPaths(query: TGuardedQuery, aggregate?: boolean): TQueryPathRefs;
177
+ /** What a logical path is on THIS readable / adapter (since 0.1.128). */
178
+ type TQueryPathKind = /** A navigation relation or anything under one — never a column of this table. */"nav" /** A stored leaf (a non-ignored, non-nav descriptor). */ | "leaf" /** A nested-object parent: flattened away on relational adapters, an unlisted descriptor on nested-object ones. */ | "objectParent" /** A descendant of a JSON-stored column that this adapter cannot address (relational adapters). */ | "jsonDescendant" /** A descendant of an `@db.encrypted` field (the ciphertext column covers the subtree). */ | "encryptedDescendant" /** Unknown to this table. */ | "unknown";
179
+ /** The lookups {@link classifyQueryPath} needs — `TableMetadata` and moost-db's `FieldCapabilityIndex` both provide them. */
180
+ interface TQueryPathSource {
181
+ navFields: ReadonlySet<string>;
182
+ /** Stored leaves by logical path. */
183
+ leaves: {
184
+ has(path: string): boolean;
185
+ };
186
+ /** Nested-object parents by logical path. */
187
+ objectParents: {
188
+ has(path: string): boolean;
143
189
  };
144
- } | undefined): void;
145
- /** Aggregate-path guard: $groupBy / $select / $having refs + filter + $sort. */
190
+ jsonParents: ReadonlySet<string>;
191
+ encryptedFields: ReadonlySet<string>;
192
+ }
193
+ /**
194
+ * Classifies one logical path. The rules exist once, in this order, for the
195
+ * core backstop ({@link guardPath}) and the HTTP capability gate alike:
196
+ *
197
+ * 1. navigation relations (and anything under them) — `parent` is the nav
198
+ * head when the path is a descendant;
199
+ * 2. a stored leaf;
200
+ * 3. a nested-object parent;
201
+ * 4. a descendant of a JSON-stored column — `parent` names the column;
202
+ * 5. a descendant of an `@db.encrypted` field — `parent` names the field;
203
+ * 6. unknown.
204
+ */
205
+ declare function classifyQueryPath(source: TQueryPathSource, path: string): {
206
+ kind: TQueryPathKind;
207
+ parent?: string;
208
+ };
209
+ /**
210
+ * Validates ONE logical path for ONE query position against this table's
211
+ * metadata and adapter capability — the classification of
212
+ * {@link classifyQueryPath} plus the position's physical requirement:
213
+ *
214
+ * - a leaf → physical capability (`canFilterField` / `canSortField`;
215
+ * `$select` always passes);
216
+ * - a nested-object parent → only `$select`, and only when it expands to
217
+ * leaf columns (`selectExpansion`);
218
+ * - everything else is rejected.
219
+ *
220
+ * `geoPredicate` marks a filter entry whose operator is `$geoWithin`: its
221
+ * shape and index support were already validated by {@link guardFilter}, so
222
+ * the adapter's scalar `canFilterField` veto does not apply.
223
+ *
224
+ * Messages are the short programmatic forms; the HTTP wording (moost-db's
225
+ * `FieldCapabilityIndex`, with `$with` hints and leaf lists) is what clients
226
+ * see and is authoritative — the HTTP gate always answers first.
227
+ */
228
+ declare function guardPath(meta: TableMetadata, adapter: BaseDbAdapter, path: string, op: TQueryPathOp, geoPredicate?: boolean): void;
229
+ /**
230
+ * Core backstop for every read / aggregate / mutation-filter entry point:
231
+ * each referenced path (see {@link collectQueryPaths}) must exist on THIS
232
+ * adapter with the physical capability the position needs (see
233
+ * {@link guardPath}). Adapters may therefore assume every path they receive
234
+ * is physical.
235
+ *
236
+ * In aggregate mode (`aggregate = true`) `$select` entries are aggregate
237
+ * expressions whose `$field` is checked, `$groupBy` fields are checked, and
238
+ * aggregate aliases (`$as` or `fn_field`) are exempt in `$sort` / `$having`.
239
+ *
240
+ * Returns the collected refs so callers can run further structural rules
241
+ * (see {@link checkHavingKeys}) without walking the query again.
242
+ */
243
+ declare function guardPaths(meta: TableMetadata, adapter: BaseDbAdapter, query: TGuardedQuery | undefined, aggregate?: boolean): TQueryPathRefs | undefined;
244
+ /** Shared read-path guard: filter + $sort encryption checks, then the path guard. */
245
+ declare function guardQuery(meta: TableMetadata, adapter: BaseDbAdapter, query: TGuardedQuery | undefined): void;
246
+ /**
247
+ * `$having` is a post-aggregation filter, so a key is either an aggregate
248
+ * alias (`$as`, else `fn_field` — already exempt in {@link collectQueryPaths})
249
+ * or a `$groupBy` field (exact logical-path match: `metadata.clicks` grouped
250
+ * stays valid). Any other key — a real but non-grouped column included — is
251
+ * rejected here, once, for SDK and HTTP callers alike, instead of by the
252
+ * engine (PostgreSQL / MySQL error, SQLite tolerance, Mongo `[]`). Returns
253
+ * the first offending key as an error entry (`path` = the bare key, as the
254
+ * `Unknown field` rejection uses); `undefined` when every key is valid.
255
+ */
256
+ declare function checkHavingKeys(refs: TQueryPathRefs): {
257
+ path: string;
258
+ message: string;
259
+ } | undefined;
260
+ /**
261
+ * Aggregate-path guard: $groupBy / $select / $having encryption refs + filter
262
+ * + $sort, then the path guard, then the `$having` key rule
263
+ * ({@link checkHavingKeys} — after the path guard so an unknown key still
264
+ * reads `Unknown field`).
265
+ */
146
266
  declare function guardAggregate(meta: TableMetadata, adapter: BaseDbAdapter, query: AggregateQuery$1): void;
147
267
  //#endregion
148
268
  //#region src/with-optimistic-retry.d.ts
@@ -164,6 +284,11 @@ interface WithOptimisticRetryOptions {
164
284
  * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
165
285
  * mutator with the fresh state, and retries — up to `maxAttempts` times.
166
286
  *
287
+ * A mutator that returns `undefined` or an empty object `{}` aborts without
288
+ * writing: the helper resolves `{ matchedCount: 1, modifiedCount: 0 }` and the
289
+ * version does not move (since 0.1.128 — previously `{}` reported a fabricated
290
+ * match as well, but through the empty-patch short-circuit).
291
+ *
167
292
  * The filter (typically the primary key) is threaded into the update payload
168
293
  * so the table layer can extract the row identity. If `mutator` returns
169
294
  * fields that overlap with the filter, the patch wins (last-write semantics
@@ -177,7 +302,7 @@ interface WithOptimisticRetryOptions {
177
302
  * @throws {CasExhaustedError} if `maxAttempts` is reached without a
178
303
  * successful commit.
179
304
  */
180
- 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>;
305
+ declare function withOptimisticRetry<TRow extends Record<string, unknown>>(table: AtscriptDbTable, filter: Record<string, unknown>, mutator: (row: TRow) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined, opts?: WithOptimisticRetryOptions): Promise<TDbUpdateResult>;
181
306
  //#endregion
182
307
  //#region src/patch/patch-decomposer.d.ts
183
308
  /**
@@ -237,4 +362,4 @@ declare function createFailureCollector(what: string): {
237
362
  throwIfAny: () => void;
238
363
  };
239
364
  //#endregion
240
- 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, DEFAULT_DB_SPACE, type DbControls, DbEncryption, 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 TDbEncryptionOptions, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbSpaceOptions, 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, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, createFailureCollector, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardQuery, isDbFieldOp, isGeoIndexableType, isGeoPointType, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
365
+ 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, CasMismatchError, DEFAULT_DB_SPACE, type DbControls, DbEncryption, DbError, type DbErrorCode, type DbPatch, type DbQuery, type DbResponse, type DbRow, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type NullableOptional, 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 TDbEncryptionOptions, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbObjectKind, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbRemoveGuard, type TDbRemoveGuardContext, type TDbSpaceOptions, type TDbStorageType, type TDbUpdateResult, type TDbWriteAction, type TDbWriteGuard, type TDbWriteGuardContext, type TDeleteOptions, type TEnsureTableOptions, type TExistingColumn, type TExistingForeignKey, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TGuardedQuery, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TPrimaryKeyChange, type TQueryPathKind, type TQueryPathOp, type TQueryPathRefs, type TQueryPathSource, type TReferencingForeignKey, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TTouchManyOptions, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteOptions, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, checkHavingKeys, classifyQueryPath, collectQueryPaths, computeInsights, createDbValidatorPlugin, createFailureCollector, decomposePatch, findAncestorInSet, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardPath, guardPaths, guardQuery, isAtscriptDbView, isDbFieldOp, isEmptyObject, isGeoIndexableType, isGeoPointType, isNavRelation, isPlainObject, isPrimitive, reconcileCas, resolveDesignType, separateCas, separateFieldOps, sortFieldNames, translateQueryTree, unsupportedOperatorMessage, walkFilter, withOptimisticRetry };
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { n as DbError, t as CasExhaustedError } from "./db-error-BHPXOKzc.mjs";
2
- import { C as NoopLogger, S as isGeoPointType, _ as DocumentFieldMapper, a as ApplicationIntegrity, b as TableMetadata, c as IntegrityStrategy, d as resolveDesignType, f as assertGeoPoint, g as RelationalFieldMapper, h as guardQuery, i as decomposePatch, l as NativeIntegrity, m as guardFilter, n as AtscriptDbTable, o as BaseDbAdapter, p as guardAggregate, r as assertNoVersionWrites, s as createFailureCollector, t as AtscriptDbView, u as AtscriptDbReadable, v as FieldMappingStrategy, x as isGeoIndexableType, y as UniquSelect } from "./db-view-C8rZM5_N.mjs";
1
+ import { n as CasMismatchError, r as DbError, t as CasExhaustedError } from "./db-error-COrO58t5.mjs";
2
+ import { A as isGeoIndexableType, C as unsupportedOperatorMessage, D as UniquSelect, E as FieldMappingStrategy, M as NoopLogger, O as TableMetadata, S as sortFieldNames, T as DocumentFieldMapper, _ as guardAggregate, a as decomposePatch, b as guardPaths, c as createFailureCollector, d as AtscriptDbReadable, f as resolveDesignType, g as collectQueryPaths, h as classifyQueryPath, i as assertNoVersionWrites, j as isGeoPointType, k as findAncestorInSet, l as IntegrityStrategy, m as checkHavingKeys, n as isAtscriptDbView, o as ApplicationIntegrity, p as assertGeoPoint, r as AtscriptDbTable, s as BaseDbAdapter, t as AtscriptDbView, u as NativeIntegrity, v as guardFilter, w as RelationalFieldMapper, x as guardQuery, y as guardPath } from "./db-view-Dl0aDTiT.mjs";
3
+ import { a as forceNavNonOptional, c as getKeyProps, l as isEmptyObject, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator, u as isPlainObject } from "./validator-CeD_fqyW.mjs";
3
4
  import { t as DEFAULT_DB_SPACE } from "./consts-C_-5_pFq.mjs";
4
- import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps } from "./ops.mjs";
5
- import { a as forceNavNonOptional, c as getKeyProps, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-0vRXN51D.mjs";
5
+ import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, reconcileCas, separateCas, separateFieldOps } from "./ops.mjs";
6
6
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
7
7
  import { computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
8
8
  //#region src/encryption.ts
@@ -149,6 +149,11 @@ var DbEncryption = class {
149
149
  * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
150
150
  * mutator with the fresh state, and retries — up to `maxAttempts` times.
151
151
  *
152
+ * A mutator that returns `undefined` or an empty object `{}` aborts without
153
+ * writing: the helper resolves `{ matchedCount: 1, modifiedCount: 0 }` and the
154
+ * version does not move (since 0.1.128 — previously `{}` reported a fabricated
155
+ * match as well, but through the empty-patch short-circuit).
156
+ *
152
157
  * The filter (typically the primary key) is threaded into the update payload
153
158
  * so the table layer can extract the row identity. If `mutator` returns
154
159
  * fields that overlap with the filter, the patch wins (last-write semantics
@@ -178,6 +183,10 @@ async function withOptimisticRetry(table, filter, mutator, opts) {
178
183
  }]);
179
184
  lastSeenVersion = row[versionColumn];
180
185
  const patch = await mutator(row);
186
+ if (patch === void 0 || isEmptyObject(patch)) return {
187
+ matchedCount: 1,
188
+ modifiedCount: 0
189
+ };
181
190
  const result = await table.updateOne({
182
191
  ...filter,
183
192
  ...patch,
@@ -293,6 +302,29 @@ var DbSpace = class {
293
302
  const adapter = this._getAdminAdapter();
294
303
  if (adapter.dropViewByName) await adapter.dropViewByName(viewName);
295
304
  }
305
+ /**
306
+ * Drops a group of mutually referencing tables as one operation.
307
+ * Used by schema sync to remove a foreign-key cycle no longer in the schema.
308
+ * @since 0.1.128
309
+ */
310
+ async dropTablesByName(tableNames) {
311
+ await this._getAdminAdapter().dropTablesByName(tableNames);
312
+ }
313
+ /**
314
+ * Live foreign keys referencing `tableName`, or `undefined` when the
315
+ * adapter cannot introspect them. Used by schema sync for drop ordering
316
+ * and surviving-reference checks of tables without a registered readable.
317
+ * @since 0.1.128
318
+ */
319
+ async getReferencingForeignKeys(tableName) {
320
+ return this._getAdminAdapter().getReferencingForeignKeys?.(tableName);
321
+ }
322
+ /**
323
+ * A factory-fresh adapter with NO registered readable. Only the name-taking
324
+ * primitives may run on it (`dropTableByName`, `dropViewByName`,
325
+ * `dropTablesByName`, `getReferencingForeignKeys`) — adapters derive the
326
+ * schema for those from the driver/connection, not from a bound table.
327
+ */
296
328
  _getAdminAdapter() {
297
329
  return this._adminAdapter ??= this.adapterFactory();
298
330
  }
@@ -340,4 +372,4 @@ function translateQueryTree(node, resolveField) {
340
372
  return { [leftField]: { [comp.op]: comp.right } };
341
373
  }
342
374
  //#endregion
343
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, DEFAULT_DB_SPACE, DbEncryption, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, createFailureCollector, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardQuery, isDbFieldOp, isGeoIndexableType, isGeoPointType, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
375
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, CasMismatchError, DEFAULT_DB_SPACE, DbEncryption, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, checkHavingKeys, classifyQueryPath, collectQueryPaths, computeInsights, createDbValidatorPlugin, createFailureCollector, decomposePatch, findAncestorInSet, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardPath, guardPaths, guardQuery, isAtscriptDbView, isDbFieldOp, isEmptyObject, isGeoIndexableType, isGeoPointType, isNavRelation, isPlainObject, isPrimitive, reconcileCas, resolveDesignType, separateCas, separateFieldOps, sortFieldNames, translateQueryTree, unsupportedOperatorMessage, walkFilter, withOptimisticRetry };
@@ -1,4 +1,4 @@
1
- import { n as DbError, r as DepthLimitExceededError } from "./db-error-BHPXOKzc.mjs";
1
+ import { i as DepthLimitExceededError, r as DbError } from "./db-error-COrO58t5.mjs";
2
2
  import { ValidatorError } from "@atscript/typescript/utils";
3
3
  //#region src/rel/relation-helpers.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-DXwEzmYJ.cjs");
1
+ const require_db_error = require("./db-error-C4JuLcvb.cjs");
2
2
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
3
3
  //#region src/rel/relation-helpers.ts
4
4
  /**
@@ -109,6 +109,29 @@ declare function separateFieldOps(data: Record<string, unknown>): TFieldOps | un
109
109
  *
110
110
  * Zero-allocation on the no-op (no `$cas`) path.
111
111
  */
112
+ /**
113
+ * Reconciles the two spellings of one write item's expected version, in
114
+ * place (since 0.1.128): the SDK operator `$cas: { [versionColumn]: N }` and a
115
+ * top-level `[versionColumn]: N` (the HTTP wire shape, where a body's version
116
+ * column is a CAS predicate, not a SET). Shared by the moost-db controller
117
+ * (`target: "cas"` — normalises to `$cas` for the table) and the db-client
118
+ * (`target: "version"` — normalises to the wire shape).
119
+ *
120
+ * - neither present → no-op, returns `undefined`
121
+ * - one present → normalised to `target`, returns the expected version
122
+ * - both present and equal → normalised once
123
+ * - both present and different → `DbError("INVALID_QUERY")` at path `$cas`:
124
+ * `Ambiguous version: "<col>" and "$cas.<col>" differ`
125
+ * - `$cas` malformed → {@link separateCas}'s own error (checked first, so a
126
+ * malformed `$cas` is never reported as "ambiguous")
127
+ * - `$cas: undefined` counts as absent (the key is dropped)
128
+ * - `versionColumn === undefined` with a `$cas` → {@link separateCas}'s
129
+ * "table has no @db.column.version" error
130
+ *
131
+ * Only a finite number counts as a present version column value — anything
132
+ * else is left untouched for the write validator to reject.
133
+ */
134
+ declare function reconcileCas(data: Record<string, unknown>, versionColumn: string | undefined, target: "cas" | "version"): number | undefined;
112
135
  declare function separateCas(data: Record<string, unknown>, versionColumn?: string): number | undefined;
113
136
  //#endregion
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 };
137
+ export { separateFieldOps as _, $mul as a, $update as c, TDbFieldOp as d, TFieldOps as f, separateCas as g, reconcileCas 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 };
@@ -109,6 +109,29 @@ declare function separateFieldOps(data: Record<string, unknown>): TFieldOps | un
109
109
  *
110
110
  * Zero-allocation on the no-op (no `$cas`) path.
111
111
  */
112
+ /**
113
+ * Reconciles the two spellings of one write item's expected version, in
114
+ * place (since 0.1.128): the SDK operator `$cas: { [versionColumn]: N }` and a
115
+ * top-level `[versionColumn]: N` (the HTTP wire shape, where a body's version
116
+ * column is a CAS predicate, not a SET). Shared by the moost-db controller
117
+ * (`target: "cas"` — normalises to `$cas` for the table) and the db-client
118
+ * (`target: "version"` — normalises to the wire shape).
119
+ *
120
+ * - neither present → no-op, returns `undefined`
121
+ * - one present → normalised to `target`, returns the expected version
122
+ * - both present and equal → normalised once
123
+ * - both present and different → `DbError("INVALID_QUERY")` at path `$cas`:
124
+ * `Ambiguous version: "<col>" and "$cas.<col>" differ`
125
+ * - `$cas` malformed → {@link separateCas}'s own error (checked first, so a
126
+ * malformed `$cas` is never reported as "ambiguous")
127
+ * - `$cas: undefined` counts as absent (the key is dropped)
128
+ * - `versionColumn === undefined` with a `$cas` → {@link separateCas}'s
129
+ * "table has no @db.column.version" error
130
+ *
131
+ * Only a finite number counts as a present version column value — anything
132
+ * else is left untouched for the write validator to reject.
133
+ */
134
+ declare function reconcileCas(data: Record<string, unknown>, versionColumn: string | undefined, target: "cas" | "version"): number | undefined;
112
135
  declare function separateCas(data: Record<string, unknown>, versionColumn?: string): number | undefined;
113
136
  //#endregion
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 };
137
+ export { separateFieldOps as _, $mul as a, $update as c, TDbFieldOp as d, TFieldOps as f, separateCas as g, reconcileCas 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,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-DXwEzmYJ.cjs");
2
+ const require_db_error = require("./db-error-C4JuLcvb.cjs");
3
3
  //#region src/ops.ts
4
4
  /** Increment a numeric field by `value` (default 1). */
5
5
  function $inc(value = 1) {
@@ -144,6 +144,48 @@ function separateFieldOps(data) {
144
144
  *
145
145
  * Zero-allocation on the no-op (no `$cas`) path.
146
146
  */
147
+ /**
148
+ * Reconciles the two spellings of one write item's expected version, in
149
+ * place (since 0.1.128): the SDK operator `$cas: { [versionColumn]: N }` and a
150
+ * top-level `[versionColumn]: N` (the HTTP wire shape, where a body's version
151
+ * column is a CAS predicate, not a SET). Shared by the moost-db controller
152
+ * (`target: "cas"` — normalises to `$cas` for the table) and the db-client
153
+ * (`target: "version"` — normalises to the wire shape).
154
+ *
155
+ * - neither present → no-op, returns `undefined`
156
+ * - one present → normalised to `target`, returns the expected version
157
+ * - both present and equal → normalised once
158
+ * - both present and different → `DbError("INVALID_QUERY")` at path `$cas`:
159
+ * `Ambiguous version: "<col>" and "$cas.<col>" differ`
160
+ * - `$cas` malformed → {@link separateCas}'s own error (checked first, so a
161
+ * malformed `$cas` is never reported as "ambiguous")
162
+ * - `$cas: undefined` counts as absent (the key is dropped)
163
+ * - `versionColumn === undefined` with a `$cas` → {@link separateCas}'s
164
+ * "table has no @db.column.version" error
165
+ *
166
+ * Only a finite number counts as a present version column value — anything
167
+ * else is left untouched for the write validator to reject.
168
+ */
169
+ function reconcileCas(data, versionColumn, target) {
170
+ if ("$cas" in data && data.$cas === void 0) delete data.$cas;
171
+ const versionValue = versionColumn === void 0 ? void 0 : data[versionColumn];
172
+ const hasVersion = typeof versionValue === "number" && Number.isFinite(versionValue);
173
+ const hasCas = "$cas" in data;
174
+ if (!hasCas && !hasVersion) return void 0;
175
+ let expected;
176
+ if (hasCas) {
177
+ expected = separateCas(data, versionColumn);
178
+ if (hasVersion && versionValue !== expected) throw new require_db_error.DbError("INVALID_QUERY", [{
179
+ path: "$cas",
180
+ message: `Ambiguous version: "${versionColumn}" and "$cas.${versionColumn}" differ`
181
+ }]);
182
+ } else expected = versionValue;
183
+ if (target === "cas") {
184
+ delete data[versionColumn];
185
+ data.$cas = { [versionColumn]: expected };
186
+ } else data[versionColumn] = expected;
187
+ return expected;
188
+ }
147
189
  function separateCas(data, versionColumn) {
148
190
  if (!("$cas" in data)) return void 0;
149
191
  if (versionColumn === void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
@@ -192,5 +234,6 @@ exports.$update = $update;
192
234
  exports.$upsert = $upsert;
193
235
  exports.getDbFieldOp = getDbFieldOp;
194
236
  exports.isDbFieldOp = isDbFieldOp;
237
+ exports.reconcileCas = reconcileCas;
195
238
  exports.separateCas = separateCas;
196
239
  exports.separateFieldOps = separateFieldOps;
package/dist/ops.d.cts CHANGED
@@ -1,2 +1,2 @@
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-DJRnNTVo.cjs";
2
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
1
+ import { _ as separateFieldOps, a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateCas, h as reconcileCas, 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-AqhV7s9o.cjs";
2
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, reconcileCas, separateCas, separateFieldOps };
package/dist/ops.d.mts CHANGED
@@ -1,2 +1,2 @@
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-DJRnNTVo.mjs";
2
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
1
+ import { _ as separateFieldOps, a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateCas, h as reconcileCas, 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-AqhV7s9o.mjs";
2
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbCas, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, reconcileCas, separateCas, separateFieldOps };
package/dist/ops.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as DbError } from "./db-error-BHPXOKzc.mjs";
1
+ import { r as DbError } from "./db-error-COrO58t5.mjs";
2
2
  //#region src/ops.ts
3
3
  /** Increment a numeric field by `value` (default 1). */
4
4
  function $inc(value = 1) {
@@ -143,6 +143,48 @@ function separateFieldOps(data) {
143
143
  *
144
144
  * Zero-allocation on the no-op (no `$cas`) path.
145
145
  */
146
+ /**
147
+ * Reconciles the two spellings of one write item's expected version, in
148
+ * place (since 0.1.128): the SDK operator `$cas: { [versionColumn]: N }` and a
149
+ * top-level `[versionColumn]: N` (the HTTP wire shape, where a body's version
150
+ * column is a CAS predicate, not a SET). Shared by the moost-db controller
151
+ * (`target: "cas"` — normalises to `$cas` for the table) and the db-client
152
+ * (`target: "version"` — normalises to the wire shape).
153
+ *
154
+ * - neither present → no-op, returns `undefined`
155
+ * - one present → normalised to `target`, returns the expected version
156
+ * - both present and equal → normalised once
157
+ * - both present and different → `DbError("INVALID_QUERY")` at path `$cas`:
158
+ * `Ambiguous version: "<col>" and "$cas.<col>" differ`
159
+ * - `$cas` malformed → {@link separateCas}'s own error (checked first, so a
160
+ * malformed `$cas` is never reported as "ambiguous")
161
+ * - `$cas: undefined` counts as absent (the key is dropped)
162
+ * - `versionColumn === undefined` with a `$cas` → {@link separateCas}'s
163
+ * "table has no @db.column.version" error
164
+ *
165
+ * Only a finite number counts as a present version column value — anything
166
+ * else is left untouched for the write validator to reject.
167
+ */
168
+ function reconcileCas(data, versionColumn, target) {
169
+ if ("$cas" in data && data.$cas === void 0) delete data.$cas;
170
+ const versionValue = versionColumn === void 0 ? void 0 : data[versionColumn];
171
+ const hasVersion = typeof versionValue === "number" && Number.isFinite(versionValue);
172
+ const hasCas = "$cas" in data;
173
+ if (!hasCas && !hasVersion) return void 0;
174
+ let expected;
175
+ if (hasCas) {
176
+ expected = separateCas(data, versionColumn);
177
+ if (hasVersion && versionValue !== expected) throw new DbError("INVALID_QUERY", [{
178
+ path: "$cas",
179
+ message: `Ambiguous version: "${versionColumn}" and "$cas.${versionColumn}" differ`
180
+ }]);
181
+ } else expected = versionValue;
182
+ if (target === "cas") {
183
+ delete data[versionColumn];
184
+ data.$cas = { [versionColumn]: expected };
185
+ } else data[versionColumn] = expected;
186
+ return expected;
187
+ }
146
188
  function separateCas(data, versionColumn) {
147
189
  if (!("$cas" in data)) return void 0;
148
190
  if (versionColumn === void 0) throw new DbError("INVALID_QUERY", [{
@@ -180,4 +222,4 @@ function separateCas(data, versionColumn) {
180
222
  return foundValue;
181
223
  }
182
224
  //#endregion
183
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps };
225
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, reconcileCas, separateCas, separateFieldOps };
package/dist/plugin.cjs CHANGED
@@ -405,7 +405,13 @@ const dbColumnAnnotations = {
405
405
  description: "Starting value for the auto-increment sequence. Adapter-specific behavior; some adapters may ignore this."
406
406
  },
407
407
  validate(token, args, doc) {
408
- return require_validation_utils.validateFieldBaseType(token, doc, "db.default.increment", "number");
408
+ const errors = require_validation_utils.validateFieldBaseType(token, doc, "db.default.increment", "number");
409
+ if (token.parentNode.countAnnotations("meta.id") === 0) errors.push({
410
+ message: `@db.default.increment on a field without @meta.id — auto-increment columns must be primary-key columns for schema sync to manage them`,
411
+ severity: 2,
412
+ range: token.range
413
+ });
414
+ return errors;
409
415
  }
410
416
  }),
411
417
  uuid: new _atscript_core.AnnotationSpec({
@@ -506,7 +512,7 @@ const dbColumnAnnotations = {
506
512
  function columnCapability(capability, verb) {
507
513
  const example = capability === "filterable" ? " @db.column.filterable\n email: string\n" : " @db.column.sortable\n createdAt: number.timestamp\n";
508
514
  return new _atscript_core.AnnotationSpec({
509
- description: `Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on all columns (default-open, back-compat).
515
+ description: `Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on every column the adapter can handle (default-open, back-compat) and the annotation is a no-op. Adapter capability always wins: \`@db.json\` / array columns stay non-sortable (and non-filterable on SQL adapters) even when annotated. \`/meta.fields\` advertises exactly what the gate accepts; index-backed columns additionally carry the advisory \`indexed\` flag.
510
516
 
511
517
  **Example:**
512
518
  \`\`\`atscript
@@ -598,7 +604,7 @@ const dbIndexAnnotations = { index: {
598
604
  //#region src/plugin/annotations/rel.ts
599
605
  const dbRelAnnotations = { rel: {
600
606
  FK: new _atscript_core.AnnotationSpec({
601
- description: "Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (stamped by its readable controller) supplies the picker URL.\n\n**Example:**\n```atscript\n@db.rel.FK\nauthorId: User.id\n\n// With alias (required when multiple FKs point to the same type)\n@db.rel.FK \"author\"\nauthorId: User.id\n```\n",
607
+ description: "Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (stamped by its readable controller) supplies the picker URL.\n- In `/meta` (and `/meta/form/:name`) the marker is inherited through reference chains: a field declared as `code: Issue.code` where `Issue.code: Dict.code` carries `@db.rel.FK` is served with `ref` pointing at the terminal field (`Dict.code`) and `db.rel.FK: true`, so view fields get the dictionary picker. Runtime metadata is untouched.\n\n**Example:**\n```atscript\n@db.rel.FK\nauthorId: User.id\n\n// With alias (required when multiple FKs point to the same type)\n@db.rel.FK \"author\"\nauthorId: User.id\n```\n",
602
608
  nodeType: ["prop"],
603
609
  passedWhenReferred: false,
604
610
  argument: {
@@ -1211,9 +1217,10 @@ function tableCapability(capability) {
1211
1217
  const example = capability === "filterable" ? " @db.column.filterable\n email: string\n // other fields not filterable via the controller\n" : " @db.column.sortable\n createdAt: number.timestamp\n // other fields not sortable via the controller\n";
1212
1218
  const verb = capability === "filterable" ? "filter" : "sort";
1213
1219
  return new _atscript_core.AnnotationSpec({
1214
- description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column is ${capability}.\n- **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
1220
+ description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column the adapter can ${verb} is ${capability} (JSON / array columns are never sortable, and not filterable on SQL adapters).
1221
+ - **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
1215
1222
 
1216
- Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
1223
+ \`/meta.fields[*].` + capability + "` advertises exactly what the gate accepts in either mode. The policy applies to " + (capability === "filterable" ? "filters only — `$groupBy`, `$having` keys and aggregate fields use the adapter's physical capability.\n\n" : "`$sort` only.\n\n") + `Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
1217
1224
 
1218
1225
  **Example:**
1219
1226
  \`\`\`atscript