@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.
- package/dist/agg.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
- package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
- package/dist/{buckets-B4T_alUe.d.cts → buckets-B8UOod-v.d.cts} +488 -57
- package/dist/{buckets-8mG67Mtw.d.mts → buckets-CHyspjYP.d.mts} +488 -57
- package/dist/{column-diff-DpeUbhEI.cjs → column-diff-BIGX3sRH.cjs} +853 -232
- package/dist/{column-diff-DR1glykD.mjs → column-diff-BfNIlMwX.mjs} +836 -233
- package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
- package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
- package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
- package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
- package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
- package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
- package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-BNGawZi9.d.cts} +34 -10
- package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-rj9N2QLw.d.mts} +34 -10
- package/dist/index.cjs +15 -11
- package/dist/index.d.cts +24 -21
- package/dist/index.d.mts +24 -21
- package/dist/index.mjs +10 -12
- package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
- package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
- package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
- package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
- package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
- package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
- package/dist/ops.cjs +6 -21
- package/dist/ops.mjs +6 -21
- package/dist/plugin.cjs +263 -323
- package/dist/plugin.mjs +163 -223
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +3 -3
- package/dist/rel.d.mts +3 -3
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-DloMywNs.d.cts} +1 -1
- package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-zwt19E7r.d.mts} +1 -1
- package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
- package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
- package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-B49-RCWd.cjs} +65 -1
- package/dist/{search-fields-CE8cmj9X.mjs → search-fields-BPNsxrCk.mjs} +48 -2
- package/dist/search-fields-CHqqcC3W.d.cts +30 -0
- package/dist/search-fields-CHqqcC3W.d.mts +30 -0
- package/dist/server-stamped-B6XbUQ4p.cjs +33 -0
- package/dist/server-stamped-BoP56K9M.mjs +22 -0
- package/dist/shared.cjs +42 -26
- package/dist/shared.d.cts +98 -4
- package/dist/shared.d.mts +98 -4
- package/dist/shared.mjs +4 -2
- package/dist/sync.cjs +228 -65
- package/dist/sync.d.cts +41 -5
- package/dist/sync.d.mts +41 -5
- package/dist/sync.mjs +228 -66
- package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
- package/dist/validator-DX97RzC2.d.cts +108 -0
- package/dist/validator-DX97RzC2.d.mts +108 -0
- package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
- package/dist/validator.cjs +6 -3
- package/dist/validator.d.cts +5 -4
- package/dist/validator.d.mts +5 -4
- package/dist/validator.mjs +5 -4
- package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-BcgDq7M9.mjs} +364 -20
- package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-BiDLCYMw.cjs} +452 -18
- 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-
|
|
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;
|
package/dist/validator.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_db_error = require("./db-error-
|
|
3
|
-
const require_validator = require("./validator-
|
|
4
|
-
const
|
|
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;
|
package/dist/validator.d.cts
CHANGED
|
@@ -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-
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
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 };
|
package/dist/validator.d.mts
CHANGED
|
@@ -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-
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
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 };
|
package/dist/validator.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { r as DbError } from "./db-error-
|
|
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-
|
|
3
|
-
import {
|
|
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
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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:
|
|
59
|
-
increment:
|
|
60
|
-
now:
|
|
61
|
-
precision:
|
|
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")
|
|
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
|
|
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
|
-
|
|
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 };
|