@atscript/db 0.1.161 → 0.1.163

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 (65) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
  6. package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
  7. package/dist/{buckets-B4T_alUe.d.cts → buckets-B8UOod-v.d.cts} +488 -57
  8. package/dist/{buckets-8mG67Mtw.d.mts → buckets-CHyspjYP.d.mts} +488 -57
  9. package/dist/{column-diff-DpeUbhEI.cjs → column-diff-BIGX3sRH.cjs} +853 -232
  10. package/dist/{column-diff-DR1glykD.mjs → column-diff-BfNIlMwX.mjs} +836 -233
  11. package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
  12. package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
  13. package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
  14. package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
  15. package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
  16. package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
  17. package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-BNGawZi9.d.cts} +34 -10
  18. package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-rj9N2QLw.d.mts} +34 -10
  19. package/dist/index.cjs +15 -11
  20. package/dist/index.d.cts +24 -21
  21. package/dist/index.d.mts +24 -21
  22. package/dist/index.mjs +10 -12
  23. package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
  24. package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
  25. package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
  26. package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
  27. package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
  28. package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
  29. package/dist/ops.cjs +6 -21
  30. package/dist/ops.mjs +6 -21
  31. package/dist/plugin.cjs +263 -323
  32. package/dist/plugin.mjs +163 -223
  33. package/dist/rel.cjs +2 -2
  34. package/dist/rel.d.cts +3 -3
  35. package/dist/rel.d.mts +3 -3
  36. package/dist/rel.mjs +2 -2
  37. package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-DloMywNs.d.cts} +1 -1
  38. package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-zwt19E7r.d.mts} +1 -1
  39. package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
  40. package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
  41. package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-B49-RCWd.cjs} +65 -1
  42. package/dist/{search-fields-CE8cmj9X.mjs → search-fields-BPNsxrCk.mjs} +48 -2
  43. package/dist/search-fields-CHqqcC3W.d.cts +30 -0
  44. package/dist/search-fields-CHqqcC3W.d.mts +30 -0
  45. package/dist/server-stamped-B6XbUQ4p.cjs +33 -0
  46. package/dist/server-stamped-BoP56K9M.mjs +22 -0
  47. package/dist/shared.cjs +42 -26
  48. package/dist/shared.d.cts +98 -4
  49. package/dist/shared.d.mts +98 -4
  50. package/dist/shared.mjs +4 -2
  51. package/dist/sync.cjs +228 -65
  52. package/dist/sync.d.cts +41 -5
  53. package/dist/sync.d.mts +41 -5
  54. package/dist/sync.mjs +228 -66
  55. package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
  56. package/dist/validator-DX97RzC2.d.cts +108 -0
  57. package/dist/validator-DX97RzC2.d.mts +108 -0
  58. package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
  59. package/dist/validator.cjs +6 -3
  60. package/dist/validator.d.cts +5 -4
  61. package/dist/validator.d.mts +5 -4
  62. package/dist/validator.mjs +5 -4
  63. package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-BcgDq7M9.mjs} +364 -20
  64. package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-BiDLCYMw.cjs} +452 -18
  65. package/package.json +6 -6
@@ -0,0 +1,108 @@
1
+ import { TAtscriptAnnotatedType, TAtscriptTypeArray, TAtscriptTypeObject, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
2
+
3
+ //#region src/patch/patch-types.d.ts
4
+ interface TArrayPatch<A extends readonly unknown[]> {
5
+ $replace?: A;
6
+ $insert?: A;
7
+ $upsert?: A;
8
+ $update?: Array<Partial<TArrayElement<A>>>;
9
+ $remove?: Array<Partial<TArrayElement<A>>>;
10
+ }
11
+ type TArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends ReadonlyArray<infer ElementType> ? ElementType : never;
12
+ /**
13
+ * Maps each property of T into a patch payload:
14
+ * - Array properties become `TArrayPatch<T[K]>`
15
+ * - Non-array properties become `Partial<T[K]>`
16
+ */
17
+ type TDbPatch<T> = { [K in keyof T]?: T[K] extends Array<infer _> ? TArrayPatch<T[K]> : Partial<T[K]> };
18
+ /**
19
+ * Extracts `@expect.array.key` properties from an array-of-objects type.
20
+ * These keys uniquely identify an element inside the array and are used
21
+ * for `$update`, `$remove`, and `$upsert` operations.
22
+ *
23
+ * @param def - Atscript array type definition.
24
+ * @returns Set of property names marked as keys; empty set if none.
25
+ */
26
+ declare function getKeyProps(def: TAtscriptAnnotatedType<TAtscriptTypeArray>): Set<string>;
27
+ //#endregion
28
+ //#region src/shared/server-stamped.d.ts
29
+ /**
30
+ * The annotations whose field the server writes on every update — the write
31
+ * sets `@db.onUpdate.now` to the current time and increments
32
+ * `@db.onUpdate.inc` — so a client value is overwritten (now) or dropped
33
+ * (inc). Dependency-free: UI and plugin code can import it from
34
+ * `@atscript/db/validator` without pulling the compiler into a bundle.
35
+ * @since 0.1.163
36
+ */
37
+ declare const SERVER_STAMPED_ON_UPDATE_KEYS: ReadonlySet<string>;
38
+ /**
39
+ * Whether a field's metadata marks it as stamped by the server on every
40
+ * update (`@db.onUpdate.now` or `@db.onUpdate.inc`) — e.g. to render it
41
+ * read-only in an edit form.
42
+ * @since 0.1.163
43
+ */
44
+ declare function isServerStampedOnUpdate(metadata: {
45
+ has(key: string): boolean;
46
+ }): boolean;
47
+ //#endregion
48
+ //#region src/validator.d.ts
49
+ /** Write operation mode for validator configuration. */
50
+ type ValidatorMode = "insert" | "patch" | "replace";
51
+ /** Singleton db validator plugin — stateless, safe to share across all validators. */
52
+ declare const dbPlugin: TValidatorPlugin;
53
+ /**
54
+ * Builds a validator for a given write mode.
55
+ *
56
+ * Shared between server-side (`AtscriptDbTable`) and client-side (`ClientValidator`).
57
+ * Both use the same plugin, same `replace`, same `partial` logic.
58
+ *
59
+ * @param type - The annotated type to validate against.
60
+ * @param mode - The write operation mode.
61
+ * @param extraPlugins - Additional adapter-specific plugins (prepended before the db plugin).
62
+ * @param opts - `unknownProps` overrides the validator's unknown-property policy
63
+ * (used by db-client's `lenientWrites`, where the served type may be a
64
+ * projection of the full server-side type).
65
+ */
66
+ declare function buildDbValidator(type: TAtscriptAnnotatedType, mode: ValidatorMode, extraPlugins?: TValidatorPlugin[], opts?: {
67
+ unknownProps?: "strip" | "ignore" | "error";
68
+ }): Validator<any>;
69
+ /**
70
+ * Path-aware `partial` callback for patch/update validation — the single
71
+ * implementation shared by the server's `bulkUpdate` validator and the
72
+ * client's patch preflight:
73
+ *
74
+ * - the root object is partial (a patch names only the fields it touches)
75
+ * - nav-relation subtrees are partial (nested data is validated by its own table)
76
+ * - `@db.patch.strategy "merge"` blocks are partial (the decomposer merges
77
+ * per leaf, so absent keys survive server-side by design)
78
+ *
79
+ * Every other nested object stays strict: non-merge blocks are `$set` as a
80
+ * whole (replace semantics), so their required fields really are required.
81
+ */
82
+ declare function buildPatchPartial(navPaths: ReadonlySet<string>): (def: TAtscriptAnnotatedType<TAtscriptTypeObject>, path: string) => boolean;
83
+ /** Returns true if the annotated type is a navigation relation (TO/FROM/VIA). */
84
+ declare function isNavRelation(type: TAtscriptAnnotatedType): boolean;
85
+ /**
86
+ * Forces nav fields non-optional so the plugin handles null/undefined checks.
87
+ * The Validator caches replace results internally, so this allocates at most once per type node.
88
+ */
89
+ declare function forceNavNonOptional(type: TAtscriptAnnotatedType): TAtscriptAnnotatedType;
90
+ /** Result of {@link buildValidationContext}. */
91
+ interface ValidationContext {
92
+ /** Flat map of dotted field paths → annotated types (same shape the server builds). */
93
+ flatMap: Map<string, TAtscriptAnnotatedType>;
94
+ /** Set of field paths that are navigation relations (TO/FROM/VIA). */
95
+ navFields: ReadonlySet<string>;
96
+ }
97
+ /**
98
+ * Builds a lightweight validation context from a deserialized Atscript type.
99
+ *
100
+ * This is the client-side equivalent of the server's `TableMetadata.build()`,
101
+ * without any adapter-specific processing (physical columns, index resolution, etc.).
102
+ *
103
+ * @param type - A deserialized annotated type (from `deserializeAnnotatedType(meta.type)`).
104
+ * @returns `flatMap` and `navFields` suitable for passing to `createDbValidatorPlugin` via `DbValidationContext`.
105
+ */
106
+ declare function buildValidationContext(type: TAtscriptAnnotatedType<TAtscriptTypeObject>): ValidationContext;
107
+ //#endregion
108
+ export { buildValidationContext as a, isNavRelation as c, TArrayPatch as d, TDbPatch as f, buildPatchPartial as i, SERVER_STAMPED_ON_UPDATE_KEYS as l, ValidatorMode as n, dbPlugin as o, getKeyProps as p, buildDbValidator as r, forceNavNonOptional as s, ValidationContext as t, isServerStampedOnUpdate as u };
@@ -1,4 +1,4 @@
1
- import { i as DepthLimitExceededError } from "./db-error-eSsQX7rt.mjs";
1
+ import { i as DepthLimitExceededError } from "./db-error-hraC12B9.mjs";
2
2
  import { getDbFieldOp } from "./ops.mjs";
3
3
  import { createAnnotatedTypeNode, flattenAnnotatedType } from "@atscript/typescript/utils";
4
4
  //#region src/shared/union-shape.ts
@@ -261,8 +261,9 @@ function createDbValidatorPlugin() {
261
261
  const baseMeta = columnUnionBase(def)?.metadata;
262
262
  const defaulted = !(belowAmbiguous(dbCtx, ctx.path) || ctx.root !== void 0 && pathBelowAmbiguous(ctx.root, ctx.path));
263
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");
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.onUpdate.inc") || meta.has("db.column.derived");
265
265
  const hasFK = meta.has("db.rel.FK");
266
+ if (dbCtx.keyless && meta.has("meta.id") && !ctx.path.includes(".")) return true;
266
267
  if (serverManaged || hasFK) {
267
268
  if (dbCtx.mode === "replace" && meta.has("meta.id") && !ctx.path.includes(".")) return;
268
269
  return true;
@@ -1,7 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-JcfJqqpk.cjs");
3
- const require_validator = require("./validator-B4YDLUkj.cjs");
4
- const require_object = require("./object-Djg28csK.cjs");
2
+ const require_db_error = require("./db-error-DmF1_xrz.cjs");
3
+ const require_validator = require("./validator-DK9Ijpo7.cjs");
4
+ const require_server_stamped = require("./server-stamped-B6XbUQ4p.cjs");
5
+ const require_object = require("./object-GjsuPNqo.cjs");
5
6
  const require_ops = require("./ops.cjs");
6
7
  exports.$cas = require_ops.$cas;
7
8
  exports.$dec = require_ops.$dec;
@@ -13,6 +14,7 @@ exports.$replace = require_ops.$replace;
13
14
  exports.$update = require_ops.$update;
14
15
  exports.$upsert = require_ops.$upsert;
15
16
  exports.DbError = require_db_error.DbError;
17
+ exports.SERVER_STAMPED_ON_UPDATE_KEYS = require_server_stamped.SERVER_STAMPED_ON_UPDATE_KEYS;
16
18
  exports.buildDbValidator = require_validator.buildDbValidator;
17
19
  exports.buildPatchPartial = require_validator.buildPatchPartial;
18
20
  exports.buildValidationContext = require_validator.buildValidationContext;
@@ -24,5 +26,6 @@ exports.isDbFieldOp = require_ops.isDbFieldOp;
24
26
  exports.isEmptyObject = require_object.isEmptyObject;
25
27
  exports.isNavRelation = require_validator.isNavRelation;
26
28
  exports.isPlainObject = require_object.isPlainObject;
29
+ exports.isServerStampedOnUpdate = require_server_stamped.isServerStampedOnUpdate;
27
30
  exports.reconcileCas = require_ops.reconcileCas;
28
31
  exports.separateCas = require_ops.separateCas;
@@ -1,5 +1,6 @@
1
+ import { r as DbError } from "./db-error-B_DRL1E9.cjs";
1
2
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, 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-AqhV7s9o.cjs";
2
- import { a as isPlainObject, i as isEmptyObject } from "./object-CtPYTIvy.cjs";
3
- import { a as buildValidationContext, c as isNavRelation, d as getKeyProps, i as buildPatchPartial, l as TArrayPatch, m as DbError, n as ValidatorMode, o as dbPlugin, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as TDbPatch } from "./validator-B3xlbrWf.cjs";
4
- import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
5
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, reconcileCas, separateCas };
3
+ import { a as isPlainObject, i as isEmptyObject } from "./object-Czia_qD4.cjs";
4
+ import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.cjs";
5
+ import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, i as buildPatchPartial, l as SERVER_STAMPED_ON_UPDATE_KEYS, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isServerStampedOnUpdate } from "./validator-DX97RzC2.cjs";
6
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, SERVER_STAMPED_ON_UPDATE_KEYS, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, isServerStampedOnUpdate, reconcileCas, separateCas };
@@ -1,5 +1,6 @@
1
+ import { r as DbError } from "./db-error-B_DRL1E9.mjs";
1
2
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, 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-AqhV7s9o.mjs";
2
- import { a as isPlainObject, i as isEmptyObject } from "./object-CtPYTIvy.mjs";
3
- import { a as buildValidationContext, c as isNavRelation, d as getKeyProps, i as buildPatchPartial, l as TArrayPatch, m as DbError, n as ValidatorMode, o as dbPlugin, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as TDbPatch } from "./validator-B3xlbrWf.mjs";
4
- import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
5
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, reconcileCas, separateCas };
3
+ import { a as isPlainObject, i as isEmptyObject } from "./object-Czia_qD4.mjs";
4
+ import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.mjs";
5
+ import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, i as buildPatchPartial, l as SERVER_STAMPED_ON_UPDATE_KEYS, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isServerStampedOnUpdate } from "./validator-DX97RzC2.mjs";
6
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, SERVER_STAMPED_ON_UPDATE_KEYS, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, isServerStampedOnUpdate, reconcileCas, separateCas };
@@ -1,5 +1,6 @@
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";
3
- import { a as isPlainObject, i as isEmptyObject } from "./object-TkiJQ-Dp.mjs";
1
+ import { r as DbError } from "./db-error-hraC12B9.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-DuDWsIdQ.mjs";
3
+ import { n as isServerStampedOnUpdate, t as SERVER_STAMPED_ON_UPDATE_KEYS } from "./server-stamped-BoP56K9M.mjs";
4
+ import { a as isPlainObject, i as isEmptyObject } from "./object-PQ1J8jri.mjs";
4
5
  import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, isDbFieldOp, reconcileCas, separateCas } from "./ops.mjs";
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 };
6
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, SERVER_STAMPED_ON_UPDATE_KEYS, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, isServerStampedOnUpdate, reconcileCas, separateCas };
@@ -1,16 +1,59 @@
1
- import { i as DB_ENTITY_ANNOTATIONS, t as searchKindOf } from "./search-fields-CE8cmj9X.mjs";
2
- import { AnnotationSpec, isArray, isInterface, isPrimitive, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
1
+ import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, o as SUPPORTED_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-DsS9UQVB.mjs";
2
+ import { i as searchKindOf, l as JSON_LEAF_TYPES, n as ENCRYPTED_SEARCH_FEATURES, r as SEARCH_PROBLEM, s as DB_ENTITY_ANNOTATIONS } from "./search-fields-BPNsxrCk.mjs";
3
+ import { AnnotationSpec, annotationExample, isArray, isInterface, isPrimitive, isProp, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
3
4
  //#region src/shared/annotation-utils.ts
4
- /** Asserts the field carrying this annotation does not also carry any of `others`. */
5
- function validateExclusiveWith(token, selfName, others) {
5
+ /**
6
+ * Names of a field's effective annotations: its own plus the ones it inherits
7
+ * through its type ref (`price: Order.price`), with the `passedWhenReferred:
8
+ * false` ones dropped — the set codegen writes into the field's runtime
9
+ * metadata (`AtscriptDoc.evalAnnotationsForNode`). Compile checks that mirror
10
+ * a runtime metadata check read this, not the own-only `countAnnotations`.
11
+ * @since 0.1.162
12
+ */
13
+ function effectiveAnnotationNames(field, doc) {
14
+ const names = new Set(field.annotations?.map((a) => a.name));
15
+ for (const a of doc?.evalAnnotationsForNode(field) ?? []) names.add(a.name);
16
+ return names;
17
+ }
18
+ /**
19
+ * Asserts the field carrying this annotation does not also carry any of
20
+ * `others` — its own or inherited through its type ref (since 0.1.162).
21
+ */
22
+ function validateExclusiveWith(token, selfName, others, doc) {
6
23
  const errors = [];
7
24
  const field = token.parentNode;
8
- for (const { key, displayName } of others) if (field.countAnnotations(key) > 0) errors.push({
9
- message: `${selfName} cannot coexist with ${displayName ?? `@${key}`} on the same field — pick one form, not both`,
25
+ const effective = effectiveAnnotationNames(field, doc);
26
+ for (const { key, displayName } of others) {
27
+ if (!effective.has(key)) continue;
28
+ const inherited = field.countAnnotations(key) === 0;
29
+ errors.push({
30
+ message: inherited ? `${selfName} cannot coexist with ${displayName ?? `@${key}`}, which this field inherits from its type reference — pick one form, not both` : `${selfName} cannot coexist with ${displayName ?? `@${key}`} on the same field — pick one form, not both`,
31
+ severity: 1,
32
+ range: token.range
33
+ });
34
+ }
35
+ return errors;
36
+ }
37
+ /**
38
+ * Reports each of `pairs` (`[annotation, why]`) the field carrying `token`
39
+ * also carries — its own or inherited through its type ref (the field's
40
+ * effective annotations, what the runtime mirror reads off the metadata):
41
+ * `${tag} cannot coexist with @${annotation} — ${why}`.
42
+ * @since 0.1.162
43
+ */
44
+ function validateIncompatible(token, tag, pairs, doc) {
45
+ const effective = effectiveAnnotationNames(token.parentNode, doc);
46
+ return pairs.filter(([name]) => effective.has(name)).map(([name, why]) => ({
47
+ message: `${tag} cannot coexist with @${name} — ${why}`,
10
48
  severity: 1,
11
49
  range: token.range
12
- });
13
- return errors;
50
+ }));
51
+ }
52
+ /** Whether a prop is a field of a view interface (`@db.view` / `@db.view.for`). */
53
+ function isViewField(field) {
54
+ const struct = field.ownerNode;
55
+ const owner = struct && isStructure(struct) ? struct.ownerNode : void 0;
56
+ return !!owner && isInterface(owner) && (owner.countAnnotations("db.view") > 0 || owner.countAnnotations("db.view.for") > 0);
14
57
  }
15
58
  /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
16
59
  function primitiveBaseType(def) {
@@ -52,15 +95,19 @@ function searchFieldVerdict(field, doc) {
52
95
  const leaf = doc.unwindType(def.id, def.chain)?.def;
53
96
  const base = primitiveBaseType(leaf);
54
97
  if (base === void 0) return { kind: "other" };
98
+ const effective = effectiveAnnotationNames(field, doc);
55
99
  const kind = searchKindOf({
56
100
  base,
57
101
  tags: isPrimitive(leaf) ? leaf.tags : void 0,
58
- expectInt: field.countAnnotations("expect.int") > 0,
59
- increment: field.countAnnotations("db.default.increment") > 0,
60
- now: field.countAnnotations("db.default.now") > 0,
61
- precision: field.countAnnotations("db.column.precision") > 0
102
+ expectInt: effective.has("expect.int"),
103
+ increment: effective.has("db.default.increment"),
104
+ now: effective.has("db.default.now"),
105
+ precision: effective.has("db.column.precision")
62
106
  });
63
- if (typeof kind === "object") return kind;
107
+ if (typeof kind === "object") {
108
+ if (kind.problem === SEARCH_PROBLEM.float && isViewField(field)) return { problem: SEARCH_PROBLEM.floatViewField };
109
+ return kind;
110
+ }
64
111
  return { kind: kind === "integer" ? "integer" : base === "string" ? "string" : "other" };
65
112
  }
66
113
  /**
@@ -173,12 +220,7 @@ function getAnnotationAlias(prop, annotationName) {
173
220
  */
174
221
  function refActionAnnotation(name) {
175
222
  return new AnnotationSpec({
176
- description: `Referential action when the target ${name === "onDelete" ? "row is deleted" : "key is updated"}. Only valid on @db.rel.FK fields.\n\n**Example:**
177
- \`\`\`atscript
178
- @db.rel.FK
179
- @db.rel.${name} "cascade"\nauthorId: User.id
180
- \`\`\`
181
- `,
223
+ description: annotationExample(`Referential action when the target ${name === "onDelete" ? "row is deleted" : "key is updated"}. Only valid on @db.rel.FK fields.`, "@db.rel.FK", `@db.rel.${name} "cascade"`, "authorId: User.id"),
182
224
  nodeType: ["prop"],
183
225
  passedWhenReferred: false,
184
226
  argument: {
@@ -468,4 +510,306 @@ function forEachFieldRef(expr, fn) {
468
510
  }
469
511
  }
470
512
  //#endregion
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 };
513
+ //#region src/shared/view-validation.ts
514
+ /** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
515
+ const NULL_SAFE_AGG_ANNOTATIONS = SUPPORTED_AGGREGATE_FNS.filter((fn) => !NULL_WHEN_EMPTY_AGGREGATE_FNS.has(fn)).map((fn) => `db.agg.${fn}`);
516
+ /**
517
+ * Walks a chain ref step by step (`Order.payload`, `Order.payload.customer`, …)
518
+ * and reports the JSON root, arrays, encryption, optionality and the leaf type.
519
+ * A chain shorter than 2 is not a field path (`resolved: false`).
520
+ * @since 0.1.141
521
+ */
522
+ function jsonChainInfo(ref, doc) {
523
+ const typeName = ref.id ?? "";
524
+ const chain = ref.chain.map((t) => t.text);
525
+ const info = {
526
+ typeName,
527
+ chain,
528
+ resolved: false,
529
+ viaArray: false,
530
+ viaEncrypted: false,
531
+ optional: false
532
+ };
533
+ if (!typeName || chain.length < 2) return info;
534
+ for (let i = 1; i <= chain.length; i++) {
535
+ const step = doc.unwindType(typeName, chain.slice(0, i));
536
+ if (!step) return info;
537
+ const node = step.node;
538
+ const prop = node && isProp(node) ? node : void 0;
539
+ if (prop?.has("optional")) info.optional = true;
540
+ if (prop && prop.countAnnotations("db.encrypted") > 0) info.viaEncrypted = true;
541
+ const isJsonProp = prop !== void 0 && prop.countAnnotations("db.json") > 0;
542
+ const isArr = isArray(step.def);
543
+ if (isArr) info.viaArray = true;
544
+ if (info.jsonRoot === void 0 && (isJsonProp || isArr)) info.jsonRoot = i;
545
+ if (i === chain.length) info.leafType = primitiveBaseType(step.def);
546
+ }
547
+ info.resolved = true;
548
+ return info;
549
+ }
550
+ /**
551
+ * The read seals a view field takes from the source field it reads, as the
552
+ * compiler sees them (compile-time mirror of the runtime `sourceFieldSeals`,
553
+ * which stamps them onto the view column — these annotations are
554
+ * `passedWhenReferred: false`, so they are not in the field's own metadata):
555
+ * - `writeOnly` — the source path or an ancestor object is `@db.writeOnly`;
556
+ * - `encrypted` — the source path or an ancestor object is `@db.encrypted`.
557
+ *
558
+ * (A `@db.json` source is no seal: a view reads a primitive JSON column's
559
+ * typed value, and an object one needs its own `@db.json`.)
560
+ *
561
+ * The source is the chain ref (`pin?: Account.pin`), or else the entry table's
562
+ * same-named field (or an aggregate's field) of a `@db.view.for` view. A source
563
+ * field that is itself a view field passes on its own seals. Computed fields
564
+ * and fields of anything but a view report no seal.
565
+ * @since 0.1.162
566
+ */
567
+ function viewFieldSeals(field, doc, depth = 0) {
568
+ const seals = {
569
+ writeOnly: false,
570
+ encrypted: false
571
+ };
572
+ if (depth > 8 || !isViewField(field) || field.countAnnotations("db.compute") > 0) return seals;
573
+ let typeName;
574
+ let path;
575
+ const def = field.getDefinition();
576
+ if (def && isRef(def) && def.hasChain) {
577
+ typeName = def.id;
578
+ path = def.chain.map((t) => t.text);
579
+ } else {
580
+ const view = field.ownerNode.ownerNode;
581
+ typeName = getAnnotationAlias(view, "db.view.for");
582
+ const agg = field.annotations?.find((a) => AGG_ANNOTATIONS.includes(a.name));
583
+ const aggField = agg?.args[0]?.text;
584
+ if (agg && (!aggField || aggField === "*")) return seals;
585
+ path = (aggField ?? field.id ?? "").split(".");
586
+ }
587
+ if (!typeName || !path.length || !path[0]) return seals;
588
+ let step = doc.unwindType(typeName, [path[0]]);
589
+ for (let i = 1; step; i++) {
590
+ const node = step.node;
591
+ if (node && isProp(node)) {
592
+ if (node.countAnnotations("db.writeOnly") > 0) seals.writeOnly = true;
593
+ if (node.countAnnotations("db.encrypted") > 0) seals.encrypted = true;
594
+ const inner = viewFieldSeals(node, step.doc, depth + 1);
595
+ seals.writeOnly ||= inner.writeOnly;
596
+ seals.encrypted ||= inner.encrypted;
597
+ }
598
+ if (i === path.length) break;
599
+ step = propStep(step, path[i]);
600
+ }
601
+ return seals;
602
+ }
603
+ /** The step to the `name` prop of `step`'s object definition — its type ref resolved. */
604
+ function propStep(step, name) {
605
+ const def = step.def;
606
+ const prop = isInterface(def) || isStructure(def) ? def.props.get(name) : void 0;
607
+ const propDef = prop?.getDefinition();
608
+ if (!prop || !propDef) return void 0;
609
+ if (!isRef(propDef)) return {
610
+ doc: step.doc,
611
+ node: prop,
612
+ def: propDef
613
+ };
614
+ const unwound = step.doc.unwindType(propDef.id, propDef.chain);
615
+ return unwound && {
616
+ doc: unwound.doc,
617
+ node: prop,
618
+ def: unwound.def
619
+ };
620
+ }
621
+ /** {@link viewFieldSeals} of `field`, computed on first use. */
622
+ function lazySeals(field, doc) {
623
+ let seals;
624
+ return () => seals ??= viewFieldSeals(field, doc);
625
+ }
626
+ /**
627
+ * A search annotation (`@db.column.searchable` / `@db.index.fulltext`) on a
628
+ * view field whose source is `@db.writeOnly` — a search hit would reveal the
629
+ * sealed value (the runtime refuses it at sync and on every query), or
630
+ * `undefined`. The field's own `@db.writeOnly` is reported by that spec.
631
+ * @since 0.1.162
632
+ */
633
+ function viewSourceSealProblem(token, doc, annotation, seals = lazySeals(token.parentNode, doc)) {
634
+ const field = token.parentNode;
635
+ if (field.countAnnotations("db.writeOnly") > 0) return void 0;
636
+ if (!seals().writeOnly) return void 0;
637
+ return {
638
+ message: `${annotation} on "${field.id}", which reads a @db.writeOnly source field — search results would reveal the sealed value`,
639
+ severity: 1,
640
+ range: token.range
641
+ };
642
+ }
643
+ /**
644
+ * A search / index feature (`@db.column.searchable`, `@db.index.fulltext`,
645
+ * `@db.index.geo`, `@db.search.vector`, `@db.search.filter`) on a field that
646
+ * would read ciphertext: a field inside an `@db.encrypted` object, or a view
647
+ * field over an encrypted source — or `undefined`. The field's own
648
+ * `@db.encrypted` is reported by that spec.
649
+ * @since 0.1.162
650
+ */
651
+ function encryptedSearchProblem(token, doc, annotation) {
652
+ return encryptedProblem(token, annotation, lazySeals(token.parentNode, doc));
653
+ }
654
+ function encryptedProblem(token, annotation, seals) {
655
+ const field = token.parentNode;
656
+ if (field.countAnnotations("db.encrypted") > 0) return void 0;
657
+ const where = encryptedAncestorName(field);
658
+ const why = where !== void 0 ? `it is inside the @db.encrypted field "${where}"` : seals().encrypted ? "it reads a @db.encrypted source field" : void 0;
659
+ if (!why) return void 0;
660
+ return {
661
+ message: `${annotation} on "${field.id}" — ${why}; search over ciphertext is impossible`,
662
+ severity: 1,
663
+ range: token.range
664
+ };
665
+ }
666
+ /**
667
+ * {@link viewSourceSealProblem}, else {@link encryptedSearchProblem} — the
668
+ * view field's source seals computed once.
669
+ */
670
+ function sealedSearchProblem(token, doc, annotation) {
671
+ const seals = lazySeals(token.parentNode, doc);
672
+ return viewSourceSealProblem(token, doc, annotation, seals) ?? encryptedProblem(token, annotation, seals);
673
+ }
674
+ /**
675
+ * An annotation's `validate` refusing it on a field that reads ciphertext
676
+ * ({@link encryptedSearchProblem}) — for the search features of sibling
677
+ * adapter plugins too.
678
+ * @since 0.1.162
679
+ */
680
+ function refuseCiphertext(annotation) {
681
+ return (token, _args, doc) => {
682
+ const problem = encryptedSearchProblem(token, doc, annotation);
683
+ return problem ? [problem] : [];
684
+ };
685
+ }
686
+ /**
687
+ * Navigation relation annotations — a navigation field holds another table's
688
+ * rows, never part of this table's stored value.
689
+ * @since 0.1.162
690
+ */
691
+ const NAV_ANNOTATIONS = [
692
+ "db.rel.to",
693
+ "db.rel.from",
694
+ "db.rel.via"
695
+ ];
696
+ /** Whether a prop is a navigation field ({@link NAV_ANNOTATIONS}). */
697
+ function isNavProp(prop) {
698
+ return NAV_ANNOTATIONS.some((key) => prop.countAnnotations(key) > 0);
699
+ }
700
+ /**
701
+ * Walks the fields stored in `root` (an interface, a structure or a type ref)
702
+ * depth-first — nested objects and type refs included, navigation fields and
703
+ * arrays skipped — and returns the first non-`undefined` result of `visit`.
704
+ * `path` is the field's dotted path below `prefix`; `viaRef` tells whether
705
+ * a type ref was followed to reach it.
706
+ * @since 0.1.162
707
+ */
708
+ function findStoredField(root, doc, visit, prefix = "") {
709
+ const seen = /* @__PURE__ */ new Set();
710
+ const walk = (def, defDoc, viaRef, prefix) => {
711
+ if (!def || seen.has(def)) return void 0;
712
+ seen.add(def);
713
+ if (isRef(def)) {
714
+ const unwound = defDoc.unwindType(def.id, def.chain);
715
+ return unwound ? walk(unwound.def, unwound.doc, true, prefix) : void 0;
716
+ }
717
+ const props = isInterface(def) || isStructure(def) ? def.props : void 0;
718
+ if (!props) return void 0;
719
+ for (const [name, prop] of props) {
720
+ if (isNavProp(prop)) continue;
721
+ const path = prefix ? `${prefix}.${name}` : name;
722
+ const found = visit(prop, path, viaRef) ?? walk(prop.getDefinition(), defDoc, viaRef, path);
723
+ if (found !== void 0) return found;
724
+ }
725
+ };
726
+ return walk(root, doc, false, prefix);
727
+ }
728
+ /**
729
+ * A search feature ({@link ENCRYPTED_SEARCH_FEATURES}) declared inside the
730
+ * TYPE of the `@db.encrypted` field carrying `token`, reached through a type
731
+ * ref (`@db.encrypted profile: Address`, where `Address` has
732
+ * `@db.index.fulltext city`) — the shared type's own annotations cannot see
733
+ * that this use encrypts them. Fields declared inline are reported by their
734
+ * own annotation (see {@link encryptedSearchProblem}).
735
+ * @since 0.1.162
736
+ */
737
+ function encryptedTypeSearchProblem(token, doc) {
738
+ const field = token.parentNode;
739
+ const found = findStoredField(field.getDefinition(), doc, (prop, path, viaRef) => {
740
+ if (!viaRef) return void 0;
741
+ const feature = ENCRYPTED_SEARCH_FEATURES.find((key) => prop.countAnnotations(key) > 0);
742
+ return feature ? {
743
+ path,
744
+ feature
745
+ } : void 0;
746
+ }, field.id ?? "");
747
+ if (!found) return void 0;
748
+ return {
749
+ message: `@db.encrypted on "${field.id}" — its type declares @${found.feature} on "${found.path}"; search over ciphertext is impossible`,
750
+ severity: 1,
751
+ range: token.range
752
+ };
753
+ }
754
+ /** Name of the nearest `@db.encrypted` prop enclosing `field` (inline nested objects), if any. */
755
+ function encryptedAncestorName(field) {
756
+ for (let node = field.ownerNode; node; node = node.ownerNode) if (isProp(node) && node.countAnnotations("db.encrypted") > 0) return node.id;
757
+ }
758
+ /** Props of a view interface (its own structure; views don't use `extends`). */
759
+ function viewProps(owner) {
760
+ if (isInterface(owner)) return owner.props;
761
+ const def = owner.getDefinition();
762
+ return def && isStructure(def) ? def.props : void 0;
763
+ }
764
+ /**
765
+ * VW8: a chain ref that passes through a `@db.json` or array node reads a
766
+ * JSON leaf, which views can only extract as a string / number / boolean.
767
+ * Intermediate nodes that `unwindType` can't reach are skipped (sync-time
768
+ * resolution reports them).
769
+ */
770
+ function validateJsonChain(fieldName, ref, doc, range) {
771
+ const info = jsonChainInfo(ref, doc);
772
+ if (!info.resolved || info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) return [];
773
+ if (info.leafType !== void 0 && JSON_LEAF_TYPES.has(info.leafType)) return [];
774
+ return [{
775
+ message: `Field "${fieldName}" reads "${info.typeName}.${info.chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
776
+ severity: 1,
777
+ range
778
+ }];
779
+ }
780
+ /**
781
+ * Whole-view checks, run once per `@db.view.for` interface:
782
+ *
783
+ * - VW7 — a field reading from a left-joined table must be optional (the
784
+ * join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
785
+ * fields are exempt (they count 0, never NULL).
786
+ * - VW8 — a chain ref through a `@db.json` or array node must end at a
787
+ * primitive string / number / boolean leaf.
788
+ * @since 0.1.136
789
+ */
790
+ function validateViewInterface(owner, doc) {
791
+ const errors = [];
792
+ const props = viewProps(owner);
793
+ if (!props) return errors;
794
+ const leftJoined = /* @__PURE__ */ new Set();
795
+ for (const join of viewJoins(owner)) {
796
+ const target = join.args[0]?.text;
797
+ if (target && join.args[2]?.text === "left") leftJoined.add(target);
798
+ }
799
+ for (const [fieldName, prop] of props) {
800
+ const def = prop.getDefinition();
801
+ if (!def || !isRef(def)) continue;
802
+ const ref = def;
803
+ const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
804
+ if (!range) continue;
805
+ if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
806
+ message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
807
+ severity: 1,
808
+ range
809
+ });
810
+ errors.push(...validateJsonChain(fieldName, ref, doc, range));
811
+ }
812
+ return errors;
813
+ }
814
+ //#endregion
815
+ export { getParentTypeName as A, validateSiblingStringField as B, viewJoins as C, getDbTableOwner as D, getAnnotationAlias as E, refActionAnnotation as F, searchFieldVerdict as I, validateExclusiveWith as L, isNullableUnion as M, isViewField as N, getNavTargetTypeName as O, primitiveBaseType as P, validateFieldBaseType as R, validateRefArgument as S, effectiveAnnotationNames as T, hasAnyViewAnnotation as _, findStoredField as a, joinTargets as b, refuseCiphertext as c, viewFieldSeals as d, viewProps as f, forEachFieldRef as g, findViewCycle as h, encryptedTypeSearchProblem as i, integerMemberIndexProblem as j, getParentStruct as k, sealedSearchProblem as l, findFKFieldsPointingTo as m, encryptedAncestorName as n, isNavProp as o, earlierJoinTargets as p, encryptedSearchProblem as r, jsonChainInfo as s, NAV_ANNOTATIONS as t, validateViewInterface as u, isAliasDecl as v, viewScopeTypes as w, validateQueryScope as x, isDbSourceDecl as y, validateIncompatible as z };