@atscript/db 0.1.154 → 0.1.156

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 (35) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-CBKL_ZWE.d.mts → buckets-8mG67Mtw.d.mts} +168 -16
  4. package/dist/{buckets-jy8BfswO.d.cts → buckets-B4T_alUe.d.cts} +168 -16
  5. package/dist/{column-diff-DAIAq6XR.mjs → column-diff-DR1glykD.mjs} +419 -75
  6. package/dist/{column-diff-qnzx1o3c.cjs → column-diff-DpeUbhEI.cjs} +430 -74
  7. package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-5gX42hBt.d.mts} +2 -1
  8. package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-BWdQ7kRZ.d.cts} +2 -1
  9. package/dist/index.cjs +3 -3
  10. package/dist/index.d.cts +15 -9
  11. package/dist/index.d.mts +15 -9
  12. package/dist/index.mjs +3 -3
  13. package/dist/plugin.cjs +21 -4
  14. package/dist/plugin.mjs +21 -4
  15. package/dist/rel.d.cts +2 -2
  16. package/dist/rel.d.mts +2 -2
  17. package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-17_2f5hX.d.mts} +1 -1
  18. package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-BLBrLQYi.d.cts} +1 -1
  19. package/dist/{search-fields-DBIZKwyb.mjs → search-fields-CE8cmj9X.mjs} +1 -0
  20. package/dist/{search-fields-DwrsJUC1.cjs → search-fields-CT_UZ9fg.cjs} +1 -0
  21. package/dist/shared.cjs +2 -1
  22. package/dist/shared.d.cts +10 -3
  23. package/dist/shared.d.mts +10 -3
  24. package/dist/shared.mjs +2 -2
  25. package/dist/sync.cjs +317 -35
  26. package/dist/sync.d.cts +34 -5
  27. package/dist/sync.d.mts +34 -5
  28. package/dist/sync.mjs +317 -35
  29. package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-BGjqc2jU.mjs} +24 -6
  30. package/dist/{validation-utils-debIYpq0.cjs → validation-utils-CtLNlBtB.cjs} +29 -5
  31. package/dist/{validator-BACewoRj.mjs → validator-B3fPJjS1.mjs} +178 -10
  32. package/dist/{validator-BxWwh3wf.cjs → validator-B4YDLUkj.cjs} +201 -9
  33. package/dist/validator.cjs +1 -1
  34. package/dist/validator.mjs +1 -1
  35. package/package.json +6 -6
@@ -1,4 +1,4 @@
1
- import { i as DB_ENTITY_ANNOTATIONS, t as searchKindOf } from "./search-fields-DBIZKwyb.mjs";
1
+ import { i as DB_ENTITY_ANNOTATIONS, t as searchKindOf } from "./search-fields-CE8cmj9X.mjs";
2
2
  import { AnnotationSpec, isArray, isInterface, isPrimitive, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
3
3
  //#region src/shared/annotation-utils.ts
4
4
  /** Asserts the field carrying this annotation does not also carry any of `others`. */
@@ -18,6 +18,22 @@ function primitiveBaseType(def) {
18
18
  const ct = def.config.type;
19
19
  return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
20
20
  }
21
+ /**
22
+ * Whether a definition is a union with a `null` member — `T | null`, or a
23
+ * ref to an alias of one (compile-time mirror of the runtime nullable union,
24
+ * since 0.1.155).
25
+ */
26
+ function isNullableUnion(def, doc, depth = 0) {
27
+ if (!def || depth > 8) return false;
28
+ if (isRef(def)) {
29
+ if (def.id === "null") return true;
30
+ return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
31
+ }
32
+ if (primitiveBaseType(def) === "null") return true;
33
+ const group = def;
34
+ if (!group.isGroup || group.op !== "|" || isStructure(def)) return false;
35
+ return group.unwrap().some((member) => isNullableUnion(member, doc, depth + 1));
36
+ }
21
37
  /** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
22
38
  function getPrimitiveBaseType(node, doc) {
23
39
  const def = node.getDefinition();
@@ -50,16 +66,18 @@ function searchFieldVerdict(field, doc) {
50
66
  /**
51
67
  * Why an integer fulltext member is not index-backed (its exact-number match
52
68
  * would scan — or fail on MongoDB next to `$text`), or `undefined`. Backed:
53
- * `_id`, the first `@meta.id` of the type, or the first field (in declaration
54
- * order) of a `@db.index.plain` / `@db.index.unique` group.
69
+ * `_id` or the first `@meta.id` of an interface's own fields (an inline
70
+ * nested object's are no key of the row, like at runtime), or the first field
71
+ * (in declaration order) of a `@db.index.plain` / `@db.index.unique` group.
55
72
  */
56
73
  function integerMemberIndexProblem(token) {
57
74
  const field = token.parentNode;
58
75
  const struct = getParentStruct(token);
59
76
  if (!struct) return void 0;
60
- if (field.id === "_id") return void 0;
61
77
  const props = [...struct.props.values()];
62
- if (field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
78
+ const topLevel = !!struct.ownerNode && isInterface(struct.ownerNode);
79
+ if (topLevel && field.id === "_id") return void 0;
80
+ if (topLevel && field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
63
81
  for (const ann of ["db.index.plain", "db.index.unique"]) for (const own of field.annotations?.filter((a) => a.name === ann) ?? []) {
64
82
  const group = own.args[0]?.text ?? field.id;
65
83
  if (props.find((p) => p.annotations?.some((a) => a.name === ann && (a.args[0]?.text ?? p.id) === group)) === field) return void 0;
@@ -450,4 +468,4 @@ function forEachFieldRef(expr, fn) {
450
468
  }
451
469
  }
452
470
  //#endregion
453
- export { validateFieldBaseType as C, validateExclusiveWith as S, getParentTypeName as _, hasAnyViewAnnotation as a, refActionAnnotation as b, joinTargets as c, viewJoins as d, viewScopeTypes as f, getParentStruct as g, getNavTargetTypeName as h, forEachFieldRef as i, validateQueryScope as l, getDbTableOwner as m, findFKFieldsPointingTo as n, isAliasDecl as o, getAnnotationAlias as p, findViewCycle as r, isDbSourceDecl as s, earlierJoinTargets as t, validateRefArgument as u, integerMemberIndexProblem as v, validateSiblingStringField as w, searchFieldVerdict as x, primitiveBaseType as y };
471
+ export { validateExclusiveWith as C, searchFieldVerdict as S, validateSiblingStringField as T, getParentTypeName as _, hasAnyViewAnnotation as a, primitiveBaseType as b, joinTargets as c, viewJoins as d, viewScopeTypes as f, getParentStruct as g, getNavTargetTypeName as h, forEachFieldRef as i, validateQueryScope as l, getDbTableOwner as m, findFKFieldsPointingTo as n, isAliasDecl as o, getAnnotationAlias as p, findViewCycle as r, isDbSourceDecl as s, earlierJoinTargets as t, validateRefArgument as u, integerMemberIndexProblem as v, validateFieldBaseType as w, refActionAnnotation as x, isNullableUnion as y };
@@ -1,4 +1,4 @@
1
- const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
1
+ const require_search_fields = require("./search-fields-CT_UZ9fg.cjs");
2
2
  let _atscript_core = require("@atscript/core");
3
3
  //#region src/shared/annotation-utils.ts
4
4
  /** Asserts the field carrying this annotation does not also carry any of `others`. */
@@ -18,6 +18,22 @@ function primitiveBaseType(def) {
18
18
  const ct = def.config.type;
19
19
  return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
20
20
  }
21
+ /**
22
+ * Whether a definition is a union with a `null` member — `T | null`, or a
23
+ * ref to an alias of one (compile-time mirror of the runtime nullable union,
24
+ * since 0.1.155).
25
+ */
26
+ function isNullableUnion(def, doc, depth = 0) {
27
+ if (!def || depth > 8) return false;
28
+ if ((0, _atscript_core.isRef)(def)) {
29
+ if (def.id === "null") return true;
30
+ return isNullableUnion(doc.unwindType(def.id, def.chain)?.def, doc, depth + 1);
31
+ }
32
+ if (primitiveBaseType(def) === "null") return true;
33
+ const group = def;
34
+ if (!group.isGroup || group.op !== "|" || (0, _atscript_core.isStructure)(def)) return false;
35
+ return group.unwrap().some((member) => isNullableUnion(member, doc, depth + 1));
36
+ }
21
37
  /** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
22
38
  function getPrimitiveBaseType(node, doc) {
23
39
  const def = node.getDefinition();
@@ -50,16 +66,18 @@ function searchFieldVerdict(field, doc) {
50
66
  /**
51
67
  * Why an integer fulltext member is not index-backed (its exact-number match
52
68
  * would scan — or fail on MongoDB next to `$text`), or `undefined`. Backed:
53
- * `_id`, the first `@meta.id` of the type, or the first field (in declaration
54
- * order) of a `@db.index.plain` / `@db.index.unique` group.
69
+ * `_id` or the first `@meta.id` of an interface's own fields (an inline
70
+ * nested object's are no key of the row, like at runtime), or the first field
71
+ * (in declaration order) of a `@db.index.plain` / `@db.index.unique` group.
55
72
  */
56
73
  function integerMemberIndexProblem(token) {
57
74
  const field = token.parentNode;
58
75
  const struct = getParentStruct(token);
59
76
  if (!struct) return void 0;
60
- if (field.id === "_id") return void 0;
61
77
  const props = [...struct.props.values()];
62
- if (field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
78
+ const topLevel = !!struct.ownerNode && (0, _atscript_core.isInterface)(struct.ownerNode);
79
+ if (topLevel && field.id === "_id") return void 0;
80
+ if (topLevel && field.countAnnotations("meta.id") > 0 && props.find((p) => p.countAnnotations("meta.id") > 0) === field) return;
63
81
  for (const ann of ["db.index.plain", "db.index.unique"]) for (const own of field.annotations?.filter((a) => a.name === ann) ?? []) {
64
82
  const group = own.args[0]?.text ?? field.id;
65
83
  if (props.find((p) => p.annotations?.some((a) => a.name === ann && (a.args[0]?.text ?? p.id) === group)) === field) return void 0;
@@ -528,6 +546,12 @@ Object.defineProperty(exports, "isDbSourceDecl", {
528
546
  return isDbSourceDecl;
529
547
  }
530
548
  });
549
+ Object.defineProperty(exports, "isNullableUnion", {
550
+ enumerable: true,
551
+ get: function() {
552
+ return isNullableUnion;
553
+ }
554
+ });
531
555
  Object.defineProperty(exports, "joinTargets", {
532
556
  enumerable: true,
533
557
  get: function() {
@@ -1,6 +1,110 @@
1
1
  import { i as DepthLimitExceededError } from "./db-error-eSsQX7rt.mjs";
2
2
  import { getDbFieldOp } from "./ops.mjs";
3
3
  import { createAnnotatedTypeNode, flattenAnnotatedType } from "@atscript/typescript/utils";
4
+ //#region src/shared/union-shape.ts
5
+ /** `null` / `undefined` as a type: a union member that only adds NULL. */
6
+ function isNullishType(type) {
7
+ const def = type.type;
8
+ return def.kind === "" && (def.designType === "null" || def.designType === "undefined");
9
+ }
10
+ /**
11
+ * The value members of a union — nested unions expanded (a flat-union entry
12
+ * of `flattenAnnotatedType` holds the prop's own union besides its members,
13
+ * an alias member may be a union itself), `null` / `undefined` left out,
14
+ * each member once. `nullable`: a `null` / `undefined` member was left out.
15
+ */
16
+ function unionValueMembers(type) {
17
+ const members = /* @__PURE__ */ new Set();
18
+ let nullable = false;
19
+ const visit = (t, depth) => {
20
+ if (t.type.kind === "union" && depth < 8) for (const item of t.type.items) visit(item, depth + 1);
21
+ else if (isNullishType(t)) nullable = true;
22
+ else members.add(t);
23
+ };
24
+ visit(type, 0);
25
+ return {
26
+ members: [...members],
27
+ nullable
28
+ };
29
+ }
30
+ /**
31
+ * The one value member of a union (`T | null` → `T`), or `undefined` when
32
+ * `type` is no union or has several value members. A column of a nullable
33
+ * union is the column `T` alone would get (its type tags, size, precision).
34
+ */
35
+ function soleUnionMember(type) {
36
+ if (type.type.kind !== "union") return void 0;
37
+ const { members } = unionValueMembers(type);
38
+ return members.length === 1 ? members[0] : void 0;
39
+ }
40
+ /**
41
+ * The `T` of a field typed `T | null` whose db annotations (`@db.default.now`
42
+ * of `number.timestamp.created`) describe the field's column like those of a
43
+ * field typed `T` — {@link soleUnionMember}, except for a reference to another
44
+ * field (`closedAt: Order.closedAt`): it shares that field's union, whose db
45
+ * annotations stay with the referenced field.
46
+ */
47
+ function columnUnionBase(type) {
48
+ return type.ref?.field ? void 0 : soleUnionMember(type);
49
+ }
50
+ const RANK = {
51
+ required: 0,
52
+ nullable: 1,
53
+ partial: 2
54
+ };
55
+ function weaker(a, b) {
56
+ return RANK[a] >= RANK[b] ? a : b;
57
+ }
58
+ /**
59
+ * The {@link TPathPresence} of the dot-path `path` below the object type
60
+ * `root`, or `undefined` when the path crosses an array or a tuple (its
61
+ * elements' fields are no columns of their own on relational storage;
62
+ * callers keep the field's own `optional` there).
63
+ */
64
+ function pathPresence(root, path) {
65
+ return presenceAt(root, path.split("."), 0);
66
+ }
67
+ function presenceAt(type, segs, i) {
68
+ const def = type.type;
69
+ switch (def.kind) {
70
+ case "union": {
71
+ let result = "required";
72
+ for (const item of def.items) {
73
+ if (isNullishType(item)) {
74
+ result = weaker(result, "nullable");
75
+ continue;
76
+ }
77
+ const member = presenceAt(item, segs, i);
78
+ if (member === void 0) return void 0;
79
+ result = weaker(result, member);
80
+ }
81
+ return result;
82
+ }
83
+ case "intersection": {
84
+ let result = "partial";
85
+ for (const item of def.items) {
86
+ const member = presenceAt(item, segs, i);
87
+ if (member === void 0) return void 0;
88
+ if (RANK[member] < RANK[result]) result = member;
89
+ }
90
+ return result;
91
+ }
92
+ case "object": {
93
+ if (i === segs.length) return "required";
94
+ const prop = def.props.get(segs[i]);
95
+ if (!prop) return "partial";
96
+ const rest = presenceAt(prop, segs, i + 1);
97
+ if (rest === void 0) return void 0;
98
+ return prop.optional ? weaker(rest, "nullable") : rest;
99
+ }
100
+ case "array":
101
+ case "tuple": return i === segs.length ? "required" : void 0;
102
+ default:
103
+ if (i < segs.length) return "partial";
104
+ return isNullishType(type) ? "nullable" : "required";
105
+ }
106
+ }
107
+ //#endregion
4
108
  //#region src/patch/patch-types.ts
5
109
  /**
6
110
  * Extracts `@expect.array.key` properties from an array-of-objects type.
@@ -50,6 +154,73 @@ function markComplexItems(def) {
50
154
  if (kind === "union" || kind === "tuple" || kind === "intersection") for (const item of def.type.items) complexItems.add(item);
51
155
  }
52
156
  /**
157
+ * Per validation context: the paths (array indices as `*`) of the tuples and
158
+ * the unions of several types the validator visited. Which item a field
159
+ * below one belongs to is not known when defaults are applied, so its
160
+ * default is not filled (`AtscriptDbTable._applyDefaults`) — and the field
161
+ * is not optional on insert either.
162
+ */
163
+ const ambiguousPaths = /* @__PURE__ */ new WeakMap();
164
+ /** `items.3.at` → `items.*.at` */
165
+ function schemaPath(path) {
166
+ return path.replace(/(^|\.)\d+(?=\.|$)/g, "$1*");
167
+ }
168
+ function markAmbiguous(dbCtx, def, path) {
169
+ const kind = def.type.kind;
170
+ if (kind === "tuple" || kind === "union" && !soleUnionMember(def)) {
171
+ let paths = ambiguousPaths.get(dbCtx);
172
+ if (!paths) ambiguousPaths.set(dbCtx, paths = /* @__PURE__ */ new Set());
173
+ paths.add(schemaPath(path));
174
+ }
175
+ }
176
+ /** Whether `path` lies below a tuple or a union of several types (see {@link ambiguousPaths}). */
177
+ function belowAmbiguous(dbCtx, path) {
178
+ const paths = ambiguousPaths.get(dbCtx);
179
+ if (!paths) return false;
180
+ const schema = schemaPath(path);
181
+ for (let dot = schema.lastIndexOf("."); dot !== -1; dot = schema.lastIndexOf(".", dot - 1)) if (paths.has(schema.slice(0, dot))) return true;
182
+ return false;
183
+ }
184
+ /** Per validated root type: schema path → whether it lies below a tuple / union of several types. */
185
+ const ambiguousByRoot = /* @__PURE__ */ new WeakMap();
186
+ /**
187
+ * {@link belowAmbiguous} read from the type itself: walks `path` from `root`
188
+ * (the validator's own type) and answers whether a tuple or a union of
189
+ * several types lies on the way. The visit-based answer misses a union the
190
+ * plugin was not consulted for — a caller that runs this plugin only for
191
+ * absent values (a form's server-managed check) never shows it the union
192
+ * holding the value.
193
+ */
194
+ function pathBelowAmbiguous(root, path) {
195
+ let cache = ambiguousByRoot.get(root);
196
+ if (!cache) ambiguousByRoot.set(root, cache = /* @__PURE__ */ new Map());
197
+ const schema = schemaPath(path);
198
+ let answer = cache.get(schema);
199
+ if (answer === void 0) {
200
+ answer = false;
201
+ let type = root;
202
+ for (const seg of schema.split(".")) {
203
+ if (type.type.kind === "union") {
204
+ const member = soleUnionMember(type);
205
+ if (!member) {
206
+ answer = true;
207
+ break;
208
+ }
209
+ type = member;
210
+ }
211
+ const kind = type.type.kind;
212
+ if (kind === "tuple") {
213
+ answer = true;
214
+ break;
215
+ }
216
+ type = kind === "array" ? seg === "*" ? type.type.of : void 0 : kind === "object" ? type.type.props.get(seg) : void 0;
217
+ if (!type) break;
218
+ }
219
+ cache.set(schema, answer);
220
+ }
221
+ return answer;
222
+ }
223
+ /**
53
224
  * Validator plugin for database operations.
54
225
  *
55
226
  * Handles navigation field constraints and delegates to the standard validator
@@ -65,6 +236,7 @@ function createDbValidatorPlugin() {
65
236
  const dbCtx = ctx.context;
66
237
  if (!dbCtx) return;
67
238
  markComplexItems(def);
239
+ markAmbiguous(dbCtx, def, ctx.path);
68
240
  if (def.type.tags?.has("geoPoint") && value !== void 0 && value !== null) {
69
241
  if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "number" || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
70
242
  ctx.error("Expected a [lng, lat] tuple of two finite numbers (GeoJSON order)");
@@ -86,10 +258,13 @@ function createDbValidatorPlugin() {
86
258
  if (isTo || isFrom || isVia) return handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia);
87
259
  if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace") && !complexItems.has(def)) {
88
260
  const meta = def.metadata;
89
- const serverManaged = meta.has("db.default") || meta.has("db.default.increment") || meta.has("db.default.uuid") || meta.has("db.default.now") || meta.has("db.column.version") || meta.has("db.column.derived");
261
+ const baseMeta = columnUnionBase(def)?.metadata;
262
+ const defaulted = !(belowAmbiguous(dbCtx, ctx.path) || ctx.root !== void 0 && pathBelowAmbiguous(ctx.root, ctx.path));
263
+ const has = (key) => defaulted && (meta.has(key) || baseMeta?.has(key) === true);
264
+ const serverManaged = has("db.default") || has("db.default.increment") || has("db.default.uuid") || has("db.default.now") || dbCtx.mode === "replace" && has("db.onUpdate.now") || meta.has("db.column.version") || meta.has("db.column.derived");
90
265
  const hasFK = meta.has("db.rel.FK");
91
266
  if (serverManaged || hasFK) {
92
- if (dbCtx.mode === "replace" && meta.has("meta.id") && !isInsideNavField(ctx.path, dbCtx.navFields)) return;
267
+ if (dbCtx.mode === "replace" && meta.has("meta.id") && !ctx.path.includes(".")) return;
93
268
  return true;
94
269
  }
95
270
  }
@@ -149,13 +324,6 @@ function isFieldOpAllowed(path, flatMap, navFields) {
149
324
  }
150
325
  return true;
151
326
  }
152
- /** Returns true if the given path is nested inside a navigation field. */
153
- function isInsideNavField(path, navFields) {
154
- if (!navFields) return false;
155
- let pos = path.length;
156
- while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) if (navFields.has(path.slice(0, pos))) return true;
157
- return false;
158
- }
159
327
  function handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia) {
160
328
  const dotIdx = ctx.path.lastIndexOf(".");
161
329
  const fieldName = dotIdx === -1 ? ctx.path : ctx.path.slice(dotIdx + 1);
@@ -437,4 +605,4 @@ function buildValidationContext(type) {
437
605
  };
438
606
  }
439
607
  //#endregion
440
- export { forceNavNonOptional as a, getKeyProps as c, dbPlugin as i, buildPatchPartial as n, isNavRelation as o, buildValidationContext as r, createDbValidatorPlugin as s, buildDbValidator as t };
608
+ export { forceNavNonOptional as a, getKeyProps as c, soleUnionMember as d, unionValueMembers as f, dbPlugin as i, columnUnionBase as l, buildPatchPartial as n, isNavRelation as o, buildValidationContext as r, createDbValidatorPlugin as s, buildDbValidator as t, pathPresence as u };
@@ -1,6 +1,110 @@
1
1
  const require_db_error = require("./db-error-JcfJqqpk.cjs");
2
2
  const require_ops = require("./ops.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
+ //#region src/shared/union-shape.ts
5
+ /** `null` / `undefined` as a type: a union member that only adds NULL. */
6
+ function isNullishType(type) {
7
+ const def = type.type;
8
+ return def.kind === "" && (def.designType === "null" || def.designType === "undefined");
9
+ }
10
+ /**
11
+ * The value members of a union — nested unions expanded (a flat-union entry
12
+ * of `flattenAnnotatedType` holds the prop's own union besides its members,
13
+ * an alias member may be a union itself), `null` / `undefined` left out,
14
+ * each member once. `nullable`: a `null` / `undefined` member was left out.
15
+ */
16
+ function unionValueMembers(type) {
17
+ const members = /* @__PURE__ */ new Set();
18
+ let nullable = false;
19
+ const visit = (t, depth) => {
20
+ if (t.type.kind === "union" && depth < 8) for (const item of t.type.items) visit(item, depth + 1);
21
+ else if (isNullishType(t)) nullable = true;
22
+ else members.add(t);
23
+ };
24
+ visit(type, 0);
25
+ return {
26
+ members: [...members],
27
+ nullable
28
+ };
29
+ }
30
+ /**
31
+ * The one value member of a union (`T | null` → `T`), or `undefined` when
32
+ * `type` is no union or has several value members. A column of a nullable
33
+ * union is the column `T` alone would get (its type tags, size, precision).
34
+ */
35
+ function soleUnionMember(type) {
36
+ if (type.type.kind !== "union") return void 0;
37
+ const { members } = unionValueMembers(type);
38
+ return members.length === 1 ? members[0] : void 0;
39
+ }
40
+ /**
41
+ * The `T` of a field typed `T | null` whose db annotations (`@db.default.now`
42
+ * of `number.timestamp.created`) describe the field's column like those of a
43
+ * field typed `T` — {@link soleUnionMember}, except for a reference to another
44
+ * field (`closedAt: Order.closedAt`): it shares that field's union, whose db
45
+ * annotations stay with the referenced field.
46
+ */
47
+ function columnUnionBase(type) {
48
+ return type.ref?.field ? void 0 : soleUnionMember(type);
49
+ }
50
+ const RANK = {
51
+ required: 0,
52
+ nullable: 1,
53
+ partial: 2
54
+ };
55
+ function weaker(a, b) {
56
+ return RANK[a] >= RANK[b] ? a : b;
57
+ }
58
+ /**
59
+ * The {@link TPathPresence} of the dot-path `path` below the object type
60
+ * `root`, or `undefined` when the path crosses an array or a tuple (its
61
+ * elements' fields are no columns of their own on relational storage;
62
+ * callers keep the field's own `optional` there).
63
+ */
64
+ function pathPresence(root, path) {
65
+ return presenceAt(root, path.split("."), 0);
66
+ }
67
+ function presenceAt(type, segs, i) {
68
+ const def = type.type;
69
+ switch (def.kind) {
70
+ case "union": {
71
+ let result = "required";
72
+ for (const item of def.items) {
73
+ if (isNullishType(item)) {
74
+ result = weaker(result, "nullable");
75
+ continue;
76
+ }
77
+ const member = presenceAt(item, segs, i);
78
+ if (member === void 0) return void 0;
79
+ result = weaker(result, member);
80
+ }
81
+ return result;
82
+ }
83
+ case "intersection": {
84
+ let result = "partial";
85
+ for (const item of def.items) {
86
+ const member = presenceAt(item, segs, i);
87
+ if (member === void 0) return void 0;
88
+ if (RANK[member] < RANK[result]) result = member;
89
+ }
90
+ return result;
91
+ }
92
+ case "object": {
93
+ if (i === segs.length) return "required";
94
+ const prop = def.props.get(segs[i]);
95
+ if (!prop) return "partial";
96
+ const rest = presenceAt(prop, segs, i + 1);
97
+ if (rest === void 0) return void 0;
98
+ return prop.optional ? weaker(rest, "nullable") : rest;
99
+ }
100
+ case "array":
101
+ case "tuple": return i === segs.length ? "required" : void 0;
102
+ default:
103
+ if (i < segs.length) return "partial";
104
+ return isNullishType(type) ? "nullable" : "required";
105
+ }
106
+ }
107
+ //#endregion
4
108
  //#region src/patch/patch-types.ts
5
109
  /**
6
110
  * Extracts `@expect.array.key` properties from an array-of-objects type.
@@ -50,6 +154,73 @@ function markComplexItems(def) {
50
154
  if (kind === "union" || kind === "tuple" || kind === "intersection") for (const item of def.type.items) complexItems.add(item);
51
155
  }
52
156
  /**
157
+ * Per validation context: the paths (array indices as `*`) of the tuples and
158
+ * the unions of several types the validator visited. Which item a field
159
+ * below one belongs to is not known when defaults are applied, so its
160
+ * default is not filled (`AtscriptDbTable._applyDefaults`) — and the field
161
+ * is not optional on insert either.
162
+ */
163
+ const ambiguousPaths = /* @__PURE__ */ new WeakMap();
164
+ /** `items.3.at` → `items.*.at` */
165
+ function schemaPath(path) {
166
+ return path.replace(/(^|\.)\d+(?=\.|$)/g, "$1*");
167
+ }
168
+ function markAmbiguous(dbCtx, def, path) {
169
+ const kind = def.type.kind;
170
+ if (kind === "tuple" || kind === "union" && !soleUnionMember(def)) {
171
+ let paths = ambiguousPaths.get(dbCtx);
172
+ if (!paths) ambiguousPaths.set(dbCtx, paths = /* @__PURE__ */ new Set());
173
+ paths.add(schemaPath(path));
174
+ }
175
+ }
176
+ /** Whether `path` lies below a tuple or a union of several types (see {@link ambiguousPaths}). */
177
+ function belowAmbiguous(dbCtx, path) {
178
+ const paths = ambiguousPaths.get(dbCtx);
179
+ if (!paths) return false;
180
+ const schema = schemaPath(path);
181
+ for (let dot = schema.lastIndexOf("."); dot !== -1; dot = schema.lastIndexOf(".", dot - 1)) if (paths.has(schema.slice(0, dot))) return true;
182
+ return false;
183
+ }
184
+ /** Per validated root type: schema path → whether it lies below a tuple / union of several types. */
185
+ const ambiguousByRoot = /* @__PURE__ */ new WeakMap();
186
+ /**
187
+ * {@link belowAmbiguous} read from the type itself: walks `path` from `root`
188
+ * (the validator's own type) and answers whether a tuple or a union of
189
+ * several types lies on the way. The visit-based answer misses a union the
190
+ * plugin was not consulted for — a caller that runs this plugin only for
191
+ * absent values (a form's server-managed check) never shows it the union
192
+ * holding the value.
193
+ */
194
+ function pathBelowAmbiguous(root, path) {
195
+ let cache = ambiguousByRoot.get(root);
196
+ if (!cache) ambiguousByRoot.set(root, cache = /* @__PURE__ */ new Map());
197
+ const schema = schemaPath(path);
198
+ let answer = cache.get(schema);
199
+ if (answer === void 0) {
200
+ answer = false;
201
+ let type = root;
202
+ for (const seg of schema.split(".")) {
203
+ if (type.type.kind === "union") {
204
+ const member = soleUnionMember(type);
205
+ if (!member) {
206
+ answer = true;
207
+ break;
208
+ }
209
+ type = member;
210
+ }
211
+ const kind = type.type.kind;
212
+ if (kind === "tuple") {
213
+ answer = true;
214
+ break;
215
+ }
216
+ type = kind === "array" ? seg === "*" ? type.type.of : void 0 : kind === "object" ? type.type.props.get(seg) : void 0;
217
+ if (!type) break;
218
+ }
219
+ cache.set(schema, answer);
220
+ }
221
+ return answer;
222
+ }
223
+ /**
53
224
  * Validator plugin for database operations.
54
225
  *
55
226
  * Handles navigation field constraints and delegates to the standard validator
@@ -65,6 +236,7 @@ function createDbValidatorPlugin() {
65
236
  const dbCtx = ctx.context;
66
237
  if (!dbCtx) return;
67
238
  markComplexItems(def);
239
+ markAmbiguous(dbCtx, def, ctx.path);
68
240
  if (def.type.tags?.has("geoPoint") && value !== void 0 && value !== null) {
69
241
  if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "number" || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
70
242
  ctx.error("Expected a [lng, lat] tuple of two finite numbers (GeoJSON order)");
@@ -86,10 +258,13 @@ function createDbValidatorPlugin() {
86
258
  if (isTo || isFrom || isVia) return handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia);
87
259
  if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace") && !complexItems.has(def)) {
88
260
  const meta = def.metadata;
89
- const serverManaged = meta.has("db.default") || meta.has("db.default.increment") || meta.has("db.default.uuid") || meta.has("db.default.now") || meta.has("db.column.version") || meta.has("db.column.derived");
261
+ const baseMeta = columnUnionBase(def)?.metadata;
262
+ const defaulted = !(belowAmbiguous(dbCtx, ctx.path) || ctx.root !== void 0 && pathBelowAmbiguous(ctx.root, ctx.path));
263
+ const has = (key) => defaulted && (meta.has(key) || baseMeta?.has(key) === true);
264
+ const serverManaged = has("db.default") || has("db.default.increment") || has("db.default.uuid") || has("db.default.now") || dbCtx.mode === "replace" && has("db.onUpdate.now") || meta.has("db.column.version") || meta.has("db.column.derived");
90
265
  const hasFK = meta.has("db.rel.FK");
91
266
  if (serverManaged || hasFK) {
92
- if (dbCtx.mode === "replace" && meta.has("meta.id") && !isInsideNavField(ctx.path, dbCtx.navFields)) return;
267
+ if (dbCtx.mode === "replace" && meta.has("meta.id") && !ctx.path.includes(".")) return;
93
268
  return true;
94
269
  }
95
270
  }
@@ -149,13 +324,6 @@ function isFieldOpAllowed(path, flatMap, navFields) {
149
324
  }
150
325
  return true;
151
326
  }
152
- /** Returns true if the given path is nested inside a navigation field. */
153
- function isInsideNavField(path, navFields) {
154
- if (!navFields) return false;
155
- let pos = path.length;
156
- while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) if (navFields.has(path.slice(0, pos))) return true;
157
- return false;
158
- }
159
327
  function handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia) {
160
328
  const dotIdx = ctx.path.lastIndexOf(".");
161
329
  const fieldName = dotIdx === -1 ? ctx.path : ctx.path.slice(dotIdx + 1);
@@ -455,6 +623,12 @@ Object.defineProperty(exports, "buildValidationContext", {
455
623
  return buildValidationContext;
456
624
  }
457
625
  });
626
+ Object.defineProperty(exports, "columnUnionBase", {
627
+ enumerable: true,
628
+ get: function() {
629
+ return columnUnionBase;
630
+ }
631
+ });
458
632
  Object.defineProperty(exports, "createDbValidatorPlugin", {
459
633
  enumerable: true,
460
634
  get: function() {
@@ -485,3 +659,21 @@ Object.defineProperty(exports, "isNavRelation", {
485
659
  return isNavRelation;
486
660
  }
487
661
  });
662
+ Object.defineProperty(exports, "pathPresence", {
663
+ enumerable: true,
664
+ get: function() {
665
+ return pathPresence;
666
+ }
667
+ });
668
+ Object.defineProperty(exports, "soleUnionMember", {
669
+ enumerable: true,
670
+ get: function() {
671
+ return soleUnionMember;
672
+ }
673
+ });
674
+ Object.defineProperty(exports, "unionValueMembers", {
675
+ enumerable: true,
676
+ get: function() {
677
+ return unionValueMembers;
678
+ }
679
+ });
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_db_error = require("./db-error-JcfJqqpk.cjs");
3
+ const require_validator = require("./validator-B4YDLUkj.cjs");
3
4
  const require_object = require("./object-Djg28csK.cjs");
4
5
  const require_ops = require("./ops.cjs");
5
- const require_validator = require("./validator-BxWwh3wf.cjs");
6
6
  exports.$cas = require_ops.$cas;
7
7
  exports.$dec = require_ops.$dec;
8
8
  exports.$inc = require_ops.$inc;
@@ -1,5 +1,5 @@
1
1
  import { r as DbError } from "./db-error-eSsQX7rt.mjs";
2
+ import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-B3fPJjS1.mjs";
2
3
  import { a as isPlainObject, i as isEmptyObject } from "./object-TkiJQ-Dp.mjs";
3
4
  import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, isDbFieldOp, reconcileCas, separateCas } from "./ops.mjs";
4
- import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-BACewoRj.mjs";
5
5
  export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, reconcileCas, separateCas };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db",
3
- "version": "0.1.154",
3
+ "version": "0.1.156",
4
4
  "description": "Database adapter utilities for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -71,14 +71,14 @@
71
71
  "access": "public"
72
72
  },
73
73
  "devDependencies": {
74
- "@atscript/core": "^0.1.103",
75
- "@atscript/typescript": "^0.1.103",
74
+ "@atscript/core": "^0.1.106",
75
+ "@atscript/typescript": "^0.1.106",
76
76
  "@uniqu/core": "^0.1.15",
77
- "unplugin-atscript": "^0.1.103"
77
+ "unplugin-atscript": "^0.1.106"
78
78
  },
79
79
  "peerDependencies": {
80
- "@atscript/core": "^0.1.103",
81
- "@atscript/typescript": "^0.1.103",
80
+ "@atscript/core": "^0.1.106",
81
+ "@atscript/typescript": "^0.1.106",
82
82
  "@uniqu/core": "^0.1.15"
83
83
  },
84
84
  "scripts": {