@atscript/db 0.1.54 → 0.1.55
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/db-error-Cepx-RsQ.mjs +29 -0
- package/dist/db-error-D8tQhNgM.cjs +40 -0
- package/dist/{db-readable-ktHqF277.d.cts → db-readable-DtrC0BGF.d.cts} +122 -115
- package/dist/{db-readable-GBjlsJXR.d.mts → db-readable-RTtqL7H4.d.mts} +123 -116
- package/dist/{db-space-DB0MT6B3.d.cts → db-space-BqVLHVq7.d.cts} +14 -1
- package/dist/{db-space-DpaXOdRp.d.mts → db-space-D3QSKx2B.d.mts} +14 -1
- package/dist/{db-validator-plugin-Cz4QoDWg.d.cts → db-validator-plugin-DDvYyv5t.d.mts} +10 -0
- package/dist/{db-validator-plugin-KC4aNIQq.d.mts → db-validator-plugin-DRGMCEn3.d.cts} +10 -0
- package/dist/{db-view-DjDKgytJ.cjs → db-view-DIGN4079.cjs} +81 -19
- package/dist/{db-view-B1j_IKSf.mjs → db-view-DNwX6_4x.mjs} +72 -10
- package/dist/index.cjs +5 -4
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +4 -3
- package/dist/{nested-writer-CNDyhg2L.mjs → nested-writer-CT2rLURx.mjs} +9 -16
- package/dist/{nested-writer-BIQ6EfaR.cjs → nested-writer-v_LPR1yJ.cjs} +14 -27
- package/dist/ops.d.mts +1 -1
- package/dist/plugin.cjs +99 -9
- package/dist/plugin.mjs +99 -9
- package/dist/rel.cjs +1 -1
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/rel.mjs +1 -1
- package/dist/shared.cjs +1 -1
- package/dist/shared.mjs +1 -1
- package/dist/sync.cjs +5 -5
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +5 -5
- package/dist/{validator-D_7Fqzs4.mjs → validator-BB5h1Le3.mjs} +42 -0
- package/dist/{validator-0iGuvGOD.cjs → validator-BIuw_T0k.cjs} +42 -0
- package/dist/validator.cjs +1 -1
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +3 -3
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
- /package/dist/{control-D1QdBO21.cjs → control-CDnwVj4q.cjs} +0 -0
- /package/dist/{control-DBd_ff5-.mjs → control-ExEKWYBE.mjs} +0 -0
- /package/dist/{ops-DcHDxrjX.d.mts → ops-C61kelof.d.mts} +0 -0
- /package/dist/{validation-utils-BiG3pLP0.cjs → validation-utils-B9WJv9aH.cjs} +0 -0
- /package/dist/{validation-utils-aNrgK-cj.mjs → validation-utils-Bh7RVrVl.mjs} +0 -0
- /package/dist/{validator-BeXlQISk.d.mts → validator-DttN2e5_.d.mts} +0 -0
|
@@ -1,16 +1,6 @@
|
|
|
1
|
+
const require_db_error = require("./db-error-D8tQhNgM.cjs");
|
|
1
2
|
const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
|
|
2
3
|
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
3
|
-
//#region src/db-error.ts
|
|
4
|
-
var DbError = class extends Error {
|
|
5
|
-
name = "DbError";
|
|
6
|
-
constructor(code, errors, message) {
|
|
7
|
-
super(message ?? errors[0]?.message ?? "Database error");
|
|
8
|
-
this.code = code;
|
|
9
|
-
this.errors = errors;
|
|
10
|
-
this.stack = void 0;
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
4
|
//#region src/table/error-utils.ts
|
|
15
5
|
/**
|
|
16
6
|
* Prefixes error paths with a nav field context.
|
|
@@ -31,7 +21,7 @@ async function wrapNestedError(navField, fn) {
|
|
|
31
21
|
return await fn();
|
|
32
22
|
} catch (error) {
|
|
33
23
|
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _atscript_typescript_utils.ValidatorError(prefixErrorPaths(error.errors, navField));
|
|
34
|
-
if (error instanceof DbError) throw new DbError(error.code, prefixErrorPaths(error.errors, navField));
|
|
24
|
+
if (error instanceof require_db_error.DbError) throw new require_db_error.DbError(error.code, prefixErrorPaths(error.errors, navField));
|
|
35
25
|
throw error;
|
|
36
26
|
}
|
|
37
27
|
}
|
|
@@ -44,14 +34,14 @@ async function enrichFkViolation(meta, fn) {
|
|
|
44
34
|
try {
|
|
45
35
|
return await fn();
|
|
46
36
|
} catch (error) {
|
|
47
|
-
if (error instanceof DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
|
|
37
|
+
if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
|
|
48
38
|
const msg = error.errors[0]?.message ?? error.message;
|
|
49
39
|
const errors = [];
|
|
50
40
|
for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
|
|
51
41
|
path: field,
|
|
52
42
|
message: msg
|
|
53
43
|
});
|
|
54
|
-
throw new DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
|
|
44
|
+
throw new require_db_error.DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
|
|
55
45
|
}
|
|
56
46
|
throw error;
|
|
57
47
|
}
|
|
@@ -64,7 +54,7 @@ async function remapDeleteFkViolation(tableName, fn) {
|
|
|
64
54
|
try {
|
|
65
55
|
return await fn();
|
|
66
56
|
} catch (error) {
|
|
67
|
-
if (error instanceof DbError && error.code === "FK_VIOLATION") throw new DbError("CONFLICT", [{
|
|
57
|
+
if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION") throw new require_db_error.DbError("CONFLICT", [{
|
|
68
58
|
path: tableName,
|
|
69
59
|
message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
|
|
70
60
|
}]);
|
|
@@ -89,10 +79,13 @@ function validateBatch(validator, items, ctx) {
|
|
|
89
79
|
for (let i = 0; i < items.length; i++) try {
|
|
90
80
|
validator.validate(items[i], false, ctx);
|
|
91
81
|
} catch (error) {
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
if (items.length > 1) {
|
|
83
|
+
if (error instanceof _atscript_typescript_utils.ValidatorError) throw new _atscript_typescript_utils.ValidatorError(error.errors.map((err) => ({
|
|
84
|
+
...err,
|
|
85
|
+
path: `[${i}].${err.path}`
|
|
86
|
+
})));
|
|
87
|
+
if (error instanceof require_db_error.DepthLimitExceededError) throw new require_db_error.DepthLimitExceededError(`[${i}].${error.field}`, error.declared, error.actual);
|
|
88
|
+
}
|
|
96
89
|
throw error;
|
|
97
90
|
}
|
|
98
91
|
}
|
|
@@ -321,13 +314,13 @@ async function batchPatchNestedTo(host, items, maxDepth, depth) {
|
|
|
321
314
|
filter: pkFilter,
|
|
322
315
|
controls: {}
|
|
323
316
|
});
|
|
324
|
-
if (!current) throw new DbError("NOT_FOUND", [{
|
|
317
|
+
if (!current) throw new require_db_error.DbError("NOT_FOUND", [{
|
|
325
318
|
path: navField,
|
|
326
319
|
message: `Cannot patch relation '${navField}' — source record not found`
|
|
327
320
|
}]);
|
|
328
321
|
fkValue = fk.localFields.length === 1 ? current[fk.localFields[0]] : void 0;
|
|
329
322
|
}
|
|
330
|
-
if (fkValue === null || fkValue === void 0) throw new DbError("FK_VIOLATION", [{
|
|
323
|
+
if (fkValue === null || fkValue === void 0) throw new require_db_error.DbError("FK_VIOLATION", [{
|
|
331
324
|
path: fk.localFields[0],
|
|
332
325
|
message: `Cannot patch relation '${navField}' — foreign key '${fk.localFields[0]}' is null`
|
|
333
326
|
}]);
|
|
@@ -621,12 +614,6 @@ async function viaReplace(targetTable, junctionTable, targets, parentPK, targetP
|
|
|
621
614
|
}
|
|
622
615
|
}
|
|
623
616
|
//#endregion
|
|
624
|
-
Object.defineProperty(exports, "DbError", {
|
|
625
|
-
enumerable: true,
|
|
626
|
-
get: function() {
|
|
627
|
-
return DbError;
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
617
|
Object.defineProperty(exports, "batchInsertNestedFrom", {
|
|
631
618
|
enumerable: true,
|
|
632
619
|
get: function() {
|
package/dist/ops.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-
|
|
1
|
+
import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-C61kelof.mjs";
|
|
2
2
|
export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, TDbFieldOp, TFieldOps, getDbFieldOp, isDbFieldOp, separateFieldOps };
|
package/dist/plugin.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_validation_utils = require("./validation-utils-
|
|
5
|
+
const require_validation_utils = require("./validation-utils-B9WJv9aH.cjs");
|
|
6
6
|
let _atscript_core = require("@atscript/core");
|
|
7
7
|
//#region src/plugin/annotations/agg.ts
|
|
8
8
|
const dbAggAnnotations = { agg: {
|
|
@@ -155,7 +155,9 @@ const dbColumnAnnotations = {
|
|
|
155
155
|
validate(token, _args, doc) {
|
|
156
156
|
return require_validation_utils.validateFieldBaseType(token, doc, "@db.column.measure", ["number", "decimal"]);
|
|
157
157
|
}
|
|
158
|
-
})
|
|
158
|
+
}),
|
|
159
|
+
filterable: columnCapability("filterable", "filtering"),
|
|
160
|
+
sortable: columnCapability("sortable", "sorting")
|
|
159
161
|
},
|
|
160
162
|
default: {
|
|
161
163
|
$self: new _atscript_core.AnnotationSpec({
|
|
@@ -226,6 +228,30 @@ const dbColumnAnnotations = {
|
|
|
226
228
|
}
|
|
227
229
|
})
|
|
228
230
|
};
|
|
231
|
+
function columnCapability(capability, verb) {
|
|
232
|
+
const example = capability === "filterable" ? " @db.column.filterable\n email: string\n" : " @db.column.sortable\n createdAt: number.timestamp\n";
|
|
233
|
+
return new _atscript_core.AnnotationSpec({
|
|
234
|
+
description: `Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on all columns (default-open, back-compat).
|
|
235
|
+
|
|
236
|
+
**Example:**
|
|
237
|
+
\`\`\`atscript
|
|
238
|
+
@db.table "users"
|
|
239
|
+
@db.table.${capability} "manual"\nexport interface User {
|
|
240
|
+
` + example + "}\n```\n",
|
|
241
|
+
nodeType: ["prop"],
|
|
242
|
+
multiple: false,
|
|
243
|
+
validate(token, _args, _doc) {
|
|
244
|
+
const errors = [];
|
|
245
|
+
const owner = require_validation_utils.getDbTableOwner(token);
|
|
246
|
+
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
247
|
+
message: `@db.column.${capability} is only valid on fields of a @db.table interface`,
|
|
248
|
+
severity: 1,
|
|
249
|
+
range: token.range
|
|
250
|
+
});
|
|
251
|
+
return errors;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
229
255
|
//#endregion
|
|
230
256
|
//#region src/plugin/annotations/index-ann.ts
|
|
231
257
|
const dbIndexAnnotations = { index: {
|
|
@@ -281,7 +307,7 @@ const dbIndexAnnotations = { index: {
|
|
|
281
307
|
//#region src/plugin/annotations/rel.ts
|
|
282
308
|
const dbRelAnnotations = { rel: {
|
|
283
309
|
FK: new _atscript_core.AnnotationSpec({
|
|
284
|
-
description: "Declares a foreign key
|
|
310
|
+
description: "Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (stamped by its readable controller) supplies the picker URL.\n\n**Example:**\n```atscript\n@db.rel.FK\nauthorId: User.id\n\n// With alias (required when multiple FKs point to the same type)\n@db.rel.FK \"author\"\nauthorId: User.id\n```\n",
|
|
285
311
|
nodeType: ["prop"],
|
|
286
312
|
argument: {
|
|
287
313
|
optional: true,
|
|
@@ -293,12 +319,6 @@ const dbRelAnnotations = { rel: {
|
|
|
293
319
|
const errors = [];
|
|
294
320
|
const field = token.parentNode;
|
|
295
321
|
const alias = args[0]?.text;
|
|
296
|
-
const owner = require_validation_utils.getDbTableOwner(token);
|
|
297
|
-
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
298
|
-
message: "@db.rel.FK is only valid on fields of a @db.table interface",
|
|
299
|
-
severity: 1,
|
|
300
|
-
range: token.range
|
|
301
|
-
});
|
|
302
322
|
if (field.countAnnotations("db.rel.to") > 0 || field.countAnnotations("db.rel.from") > 0) errors.push({
|
|
303
323
|
message: "A field cannot be both a foreign key and a navigational property",
|
|
304
324
|
severity: 1,
|
|
@@ -705,6 +725,8 @@ const dbSearchAnnotations = { search: {
|
|
|
705
725
|
//#region src/plugin/annotations/table.ts
|
|
706
726
|
const dbTableAnnotations = {
|
|
707
727
|
table: {
|
|
728
|
+
filterable: tableCapability("filterable"),
|
|
729
|
+
sortable: tableCapability("sortable"),
|
|
708
730
|
$self: new _atscript_core.AnnotationSpec({
|
|
709
731
|
description: "Marks an interface as a database-persisted entity (table in SQL, collection in MongoDB). If the name argument is omitted, the adapter derives the table name from the interface name.\n\n**Example:**\n```atscript\n@db.table \"users\"\nexport interface User { ... }\n```\n",
|
|
710
732
|
nodeType: ["interface"],
|
|
@@ -771,8 +793,75 @@ const dbTableAnnotations = {
|
|
|
771
793
|
description: "Sync method: \"drop\" (lossy) or \"recreate\" (lossless with data copy).",
|
|
772
794
|
values: ["drop", "recreate"]
|
|
773
795
|
}
|
|
796
|
+
}) },
|
|
797
|
+
depth: { limit: new _atscript_core.AnnotationSpec({
|
|
798
|
+
description: "Security guard on nested-write payloads. `N` is a non-negative integer declaring the maximum depth a client may nest `@db.rel.from` children in insert, replace, or patch payloads. Writes deeper than `N` are rejected at the server boundary with HTTP 400 before any DB access.\n\n**Default when absent:** `0` — any nested-write payload is rejected. Authors opt in explicitly to `N >= 1` when they want the server to accept deep writes. This is a security / blast-radius control, not a performance knob.\n\n**Scope:** affects only write acceptance. Has no effect on `/meta` serialization, read/query paths, or wire shape — the meta endpoint always ships FK refs as the shallow `{ id, metadata }` shape regardless of this annotation.\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.depth.limit 2\nexport interface Author { ... }\n```\n",
|
|
799
|
+
nodeType: ["interface"],
|
|
800
|
+
multiple: false,
|
|
801
|
+
argument: {
|
|
802
|
+
name: "depth",
|
|
803
|
+
type: "number",
|
|
804
|
+
description: "Non-negative integer: maximum nesting depth accepted for nested writes."
|
|
805
|
+
},
|
|
806
|
+
validate(token, args, _doc) {
|
|
807
|
+
const errors = [];
|
|
808
|
+
const owner = token.parentNode;
|
|
809
|
+
if (owner.countAnnotations("db.table") === 0) errors.push({
|
|
810
|
+
message: "@db.depth.limit is only valid on @db.table interfaces",
|
|
811
|
+
severity: 1,
|
|
812
|
+
range: token.range
|
|
813
|
+
});
|
|
814
|
+
if (owner.countAnnotations("db.depth.limit") > 1) errors.push({
|
|
815
|
+
message: "Multiple @db.depth.limit annotations on the same interface",
|
|
816
|
+
severity: 1,
|
|
817
|
+
range: token.range
|
|
818
|
+
});
|
|
819
|
+
const raw = args[0]?.text;
|
|
820
|
+
if (raw !== void 0) {
|
|
821
|
+
const num = Number(raw);
|
|
822
|
+
if (!Number.isFinite(num) || !Number.isInteger(num) || num < 0) errors.push({
|
|
823
|
+
message: `@db.depth.limit depth must be a non-negative integer, got '${raw}'`,
|
|
824
|
+
severity: 1,
|
|
825
|
+
range: args[0].range
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return errors;
|
|
829
|
+
}
|
|
774
830
|
}) }
|
|
775
831
|
};
|
|
832
|
+
function tableCapability(capability) {
|
|
833
|
+
const example = capability === "filterable" ? " @db.column.filterable\n email: string\n // other fields not filterable via the controller\n" : " @db.column.sortable\n createdAt: number.timestamp\n // other fields not sortable via the controller\n";
|
|
834
|
+
const verb = capability === "filterable" ? "filter" : "sort";
|
|
835
|
+
return new _atscript_core.AnnotationSpec({
|
|
836
|
+
description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column is ${capability}.\n- **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
|
|
837
|
+
|
|
838
|
+
Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
|
|
839
|
+
|
|
840
|
+
**Example:**
|
|
841
|
+
\`\`\`atscript
|
|
842
|
+
@db.table "users"
|
|
843
|
+
@db.table.${capability} "manual"\nexport interface User {
|
|
844
|
+
` + example + "}\n```\n",
|
|
845
|
+
nodeType: ["interface"],
|
|
846
|
+
multiple: false,
|
|
847
|
+
argument: {
|
|
848
|
+
optional: true,
|
|
849
|
+
name: "mode",
|
|
850
|
+
type: "string",
|
|
851
|
+
description: `${verb[0].toUpperCase()}${verb.slice(1)}-gating mode: 'auto' (default) or 'manual'.`,
|
|
852
|
+
values: ["auto", "manual"]
|
|
853
|
+
},
|
|
854
|
+
validate(token, _args, _doc) {
|
|
855
|
+
const errors = [];
|
|
856
|
+
if (token.parentNode.countAnnotations("db.table") === 0) errors.push({
|
|
857
|
+
message: `@db.table.${capability} requires @db.table on the same interface`,
|
|
858
|
+
severity: 1,
|
|
859
|
+
range: token.range
|
|
860
|
+
});
|
|
861
|
+
return errors;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
776
865
|
//#endregion
|
|
777
866
|
//#region src/plugin/annotations/view.ts
|
|
778
867
|
const dbViewAnnotations = { view: {
|
|
@@ -964,6 +1053,7 @@ const dbPlugin = () => ({
|
|
|
964
1053
|
ignore: dbColumnAnnotations.ignore,
|
|
965
1054
|
http: dbTableAnnotations.http,
|
|
966
1055
|
sync: dbTableAnnotations.sync,
|
|
1056
|
+
depth: dbTableAnnotations.depth,
|
|
967
1057
|
rel: dbRelAnnotations.rel,
|
|
968
1058
|
view: dbViewAnnotations.view,
|
|
969
1059
|
agg: dbAggAnnotations.agg,
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as getAnnotationAlias, c as getParentStruct, d as validateFieldBaseType, i as validateRefArgument, l as getParentTypeName, n as hasAnyViewAnnotation, o as getDbTableOwner, r as validateQueryScope, s as getNavTargetTypeName, t as findFKFieldsPointingTo, u as refActionAnnotation } from "./validation-utils-
|
|
1
|
+
import { a as getAnnotationAlias, c as getParentStruct, d as validateFieldBaseType, i as validateRefArgument, l as getParentTypeName, n as hasAnyViewAnnotation, o as getDbTableOwner, r as validateQueryScope, s as getNavTargetTypeName, t as findFKFieldsPointingTo, u as refActionAnnotation } from "./validation-utils-Bh7RVrVl.mjs";
|
|
2
2
|
import { AnnotationSpec, isArray, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
|
|
3
3
|
//#region src/plugin/annotations/agg.ts
|
|
4
4
|
const dbAggAnnotations = { agg: {
|
|
@@ -151,7 +151,9 @@ const dbColumnAnnotations = {
|
|
|
151
151
|
validate(token, _args, doc) {
|
|
152
152
|
return validateFieldBaseType(token, doc, "@db.column.measure", ["number", "decimal"]);
|
|
153
153
|
}
|
|
154
|
-
})
|
|
154
|
+
}),
|
|
155
|
+
filterable: columnCapability("filterable", "filtering"),
|
|
156
|
+
sortable: columnCapability("sortable", "sorting")
|
|
155
157
|
},
|
|
156
158
|
default: {
|
|
157
159
|
$self: new AnnotationSpec({
|
|
@@ -222,6 +224,30 @@ const dbColumnAnnotations = {
|
|
|
222
224
|
}
|
|
223
225
|
})
|
|
224
226
|
};
|
|
227
|
+
function columnCapability(capability, verb) {
|
|
228
|
+
const example = capability === "filterable" ? " @db.column.filterable\n email: string\n" : " @db.column.sortable\n createdAt: number.timestamp\n";
|
|
229
|
+
return new AnnotationSpec({
|
|
230
|
+
description: `Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on all columns (default-open, back-compat).
|
|
231
|
+
|
|
232
|
+
**Example:**
|
|
233
|
+
\`\`\`atscript
|
|
234
|
+
@db.table "users"
|
|
235
|
+
@db.table.${capability} "manual"\nexport interface User {
|
|
236
|
+
` + example + "}\n```\n",
|
|
237
|
+
nodeType: ["prop"],
|
|
238
|
+
multiple: false,
|
|
239
|
+
validate(token, _args, _doc) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
const owner = getDbTableOwner(token);
|
|
242
|
+
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
243
|
+
message: `@db.column.${capability} is only valid on fields of a @db.table interface`,
|
|
244
|
+
severity: 1,
|
|
245
|
+
range: token.range
|
|
246
|
+
});
|
|
247
|
+
return errors;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
225
251
|
//#endregion
|
|
226
252
|
//#region src/plugin/annotations/index-ann.ts
|
|
227
253
|
const dbIndexAnnotations = { index: {
|
|
@@ -277,7 +303,7 @@ const dbIndexAnnotations = { index: {
|
|
|
277
303
|
//#region src/plugin/annotations/rel.ts
|
|
278
304
|
const dbRelAnnotations = { rel: {
|
|
279
305
|
FK: new AnnotationSpec({
|
|
280
|
-
description: "Declares a foreign key
|
|
306
|
+
description: "Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (stamped by its readable controller) supplies the picker URL.\n\n**Example:**\n```atscript\n@db.rel.FK\nauthorId: User.id\n\n// With alias (required when multiple FKs point to the same type)\n@db.rel.FK \"author\"\nauthorId: User.id\n```\n",
|
|
281
307
|
nodeType: ["prop"],
|
|
282
308
|
argument: {
|
|
283
309
|
optional: true,
|
|
@@ -289,12 +315,6 @@ const dbRelAnnotations = { rel: {
|
|
|
289
315
|
const errors = [];
|
|
290
316
|
const field = token.parentNode;
|
|
291
317
|
const alias = args[0]?.text;
|
|
292
|
-
const owner = getDbTableOwner(token);
|
|
293
|
-
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
294
|
-
message: "@db.rel.FK is only valid on fields of a @db.table interface",
|
|
295
|
-
severity: 1,
|
|
296
|
-
range: token.range
|
|
297
|
-
});
|
|
298
318
|
if (field.countAnnotations("db.rel.to") > 0 || field.countAnnotations("db.rel.from") > 0) errors.push({
|
|
299
319
|
message: "A field cannot be both a foreign key and a navigational property",
|
|
300
320
|
severity: 1,
|
|
@@ -701,6 +721,8 @@ const dbSearchAnnotations = { search: {
|
|
|
701
721
|
//#region src/plugin/annotations/table.ts
|
|
702
722
|
const dbTableAnnotations = {
|
|
703
723
|
table: {
|
|
724
|
+
filterable: tableCapability("filterable"),
|
|
725
|
+
sortable: tableCapability("sortable"),
|
|
704
726
|
$self: new AnnotationSpec({
|
|
705
727
|
description: "Marks an interface as a database-persisted entity (table in SQL, collection in MongoDB). If the name argument is omitted, the adapter derives the table name from the interface name.\n\n**Example:**\n```atscript\n@db.table \"users\"\nexport interface User { ... }\n```\n",
|
|
706
728
|
nodeType: ["interface"],
|
|
@@ -767,8 +789,75 @@ const dbTableAnnotations = {
|
|
|
767
789
|
description: "Sync method: \"drop\" (lossy) or \"recreate\" (lossless with data copy).",
|
|
768
790
|
values: ["drop", "recreate"]
|
|
769
791
|
}
|
|
792
|
+
}) },
|
|
793
|
+
depth: { limit: new AnnotationSpec({
|
|
794
|
+
description: "Security guard on nested-write payloads. `N` is a non-negative integer declaring the maximum depth a client may nest `@db.rel.from` children in insert, replace, or patch payloads. Writes deeper than `N` are rejected at the server boundary with HTTP 400 before any DB access.\n\n**Default when absent:** `0` — any nested-write payload is rejected. Authors opt in explicitly to `N >= 1` when they want the server to accept deep writes. This is a security / blast-radius control, not a performance knob.\n\n**Scope:** affects only write acceptance. Has no effect on `/meta` serialization, read/query paths, or wire shape — the meta endpoint always ships FK refs as the shallow `{ id, metadata }` shape regardless of this annotation.\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.depth.limit 2\nexport interface Author { ... }\n```\n",
|
|
795
|
+
nodeType: ["interface"],
|
|
796
|
+
multiple: false,
|
|
797
|
+
argument: {
|
|
798
|
+
name: "depth",
|
|
799
|
+
type: "number",
|
|
800
|
+
description: "Non-negative integer: maximum nesting depth accepted for nested writes."
|
|
801
|
+
},
|
|
802
|
+
validate(token, args, _doc) {
|
|
803
|
+
const errors = [];
|
|
804
|
+
const owner = token.parentNode;
|
|
805
|
+
if (owner.countAnnotations("db.table") === 0) errors.push({
|
|
806
|
+
message: "@db.depth.limit is only valid on @db.table interfaces",
|
|
807
|
+
severity: 1,
|
|
808
|
+
range: token.range
|
|
809
|
+
});
|
|
810
|
+
if (owner.countAnnotations("db.depth.limit") > 1) errors.push({
|
|
811
|
+
message: "Multiple @db.depth.limit annotations on the same interface",
|
|
812
|
+
severity: 1,
|
|
813
|
+
range: token.range
|
|
814
|
+
});
|
|
815
|
+
const raw = args[0]?.text;
|
|
816
|
+
if (raw !== void 0) {
|
|
817
|
+
const num = Number(raw);
|
|
818
|
+
if (!Number.isFinite(num) || !Number.isInteger(num) || num < 0) errors.push({
|
|
819
|
+
message: `@db.depth.limit depth must be a non-negative integer, got '${raw}'`,
|
|
820
|
+
severity: 1,
|
|
821
|
+
range: args[0].range
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return errors;
|
|
825
|
+
}
|
|
770
826
|
}) }
|
|
771
827
|
};
|
|
828
|
+
function tableCapability(capability) {
|
|
829
|
+
const example = capability === "filterable" ? " @db.column.filterable\n email: string\n // other fields not filterable via the controller\n" : " @db.column.sortable\n createdAt: number.timestamp\n // other fields not sortable via the controller\n";
|
|
830
|
+
const verb = capability === "filterable" ? "filter" : "sort";
|
|
831
|
+
return new AnnotationSpec({
|
|
832
|
+
description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column is ${capability}.\n- **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
|
|
833
|
+
|
|
834
|
+
Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
|
|
835
|
+
|
|
836
|
+
**Example:**
|
|
837
|
+
\`\`\`atscript
|
|
838
|
+
@db.table "users"
|
|
839
|
+
@db.table.${capability} "manual"\nexport interface User {
|
|
840
|
+
` + example + "}\n```\n",
|
|
841
|
+
nodeType: ["interface"],
|
|
842
|
+
multiple: false,
|
|
843
|
+
argument: {
|
|
844
|
+
optional: true,
|
|
845
|
+
name: "mode",
|
|
846
|
+
type: "string",
|
|
847
|
+
description: `${verb[0].toUpperCase()}${verb.slice(1)}-gating mode: 'auto' (default) or 'manual'.`,
|
|
848
|
+
values: ["auto", "manual"]
|
|
849
|
+
},
|
|
850
|
+
validate(token, _args, _doc) {
|
|
851
|
+
const errors = [];
|
|
852
|
+
if (token.parentNode.countAnnotations("db.table") === 0) errors.push({
|
|
853
|
+
message: `@db.table.${capability} requires @db.table on the same interface`,
|
|
854
|
+
severity: 1,
|
|
855
|
+
range: token.range
|
|
856
|
+
});
|
|
857
|
+
return errors;
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
772
861
|
//#endregion
|
|
773
862
|
//#region src/plugin/annotations/view.ts
|
|
774
863
|
const dbViewAnnotations = { view: {
|
|
@@ -960,6 +1049,7 @@ const dbPlugin = () => ({
|
|
|
960
1049
|
ignore: dbColumnAnnotations.ignore,
|
|
961
1050
|
http: dbTableAnnotations.http,
|
|
962
1051
|
sync: dbTableAnnotations.sync,
|
|
1052
|
+
depth: dbTableAnnotations.depth,
|
|
963
1053
|
rel: dbRelAnnotations.rel,
|
|
964
1054
|
view: dbViewAnnotations.view,
|
|
965
1055
|
agg: dbAggAnnotations.agg,
|
package/dist/rel.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_relation_loader = require("./relation-loader-CRC5LcqM.cjs");
|
|
3
|
-
const require_nested_writer = require("./nested-writer-BIQ6EfaR.cjs");
|
|
4
3
|
const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
|
|
4
|
+
const require_nested_writer = require("./nested-writer-v_LPR1yJ.cjs");
|
|
5
5
|
exports.batchInsertNestedFrom = require_nested_writer.batchInsertNestedFrom;
|
|
6
6
|
exports.batchInsertNestedTo = require_nested_writer.batchInsertNestedTo;
|
|
7
7
|
exports.batchInsertNestedVia = require_nested_writer.batchInsertNestedVia;
|
package/dist/rel.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as DbValidationContext } from "./db-validator-plugin-
|
|
1
|
+
import { D as TDbForeignKey, P as TDbRelation, Y as TTableResolver, Z as TWriteTableResolver, it as TGenericLogger, nt as TableMetadata, o as BaseDbAdapter } from "./db-readable-DtrC0BGF.cjs";
|
|
2
|
+
import { t as DbValidationContext } from "./db-validator-plugin-DRGMCEn3.cjs";
|
|
3
3
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
4
4
|
import { Validator } from "@atscript/typescript/utils";
|
|
5
5
|
|
package/dist/rel.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as DbValidationContext } from "./db-validator-plugin-
|
|
1
|
+
import { D as TDbForeignKey, P as TDbRelation, Y as TTableResolver, Z as TWriteTableResolver, it as TGenericLogger, nt as TableMetadata, o as BaseDbAdapter } from "./db-readable-RTtqL7H4.mjs";
|
|
2
|
+
import { t as DbValidationContext } from "./db-validator-plugin-DDvYyv5t.mjs";
|
|
3
3
|
import { Validator } from "@atscript/typescript/utils";
|
|
4
4
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
5
5
|
|
package/dist/rel.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { t as loadRelationsImpl } from "./relation-loader-BEOTXNcq.mjs";
|
|
2
|
-
import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, n as batchInsertNestedTo, o as batchPatchNestedVia, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CNDyhg2L.mjs";
|
|
3
2
|
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-CLasawQq.mjs";
|
|
3
|
+
import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, n as batchInsertNestedTo, o as batchPatchNestedVia, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CT2rLURx.mjs";
|
|
4
4
|
export { batchInsertNestedFrom, batchInsertNestedTo, batchInsertNestedVia, batchPatchNestedFrom, batchPatchNestedTo, batchPatchNestedVia, batchReplaceNestedFrom, batchReplaceNestedTo, batchReplaceNestedVia, checkDepthOverflow, findFKForRelation, findRemoteFK, loadRelationsImpl, preValidateNestedFrom, resolveRelationTargetTable, validateBatch };
|
package/dist/shared.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_validation_utils = require("./validation-utils-
|
|
2
|
+
const require_validation_utils = require("./validation-utils-B9WJv9aH.cjs");
|
|
3
3
|
exports.findFKFieldsPointingTo = require_validation_utils.findFKFieldsPointingTo;
|
|
4
4
|
exports.getAnnotationAlias = require_validation_utils.getAnnotationAlias;
|
|
5
5
|
exports.getDbTableOwner = require_validation_utils.getDbTableOwner;
|
package/dist/shared.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as getAnnotationAlias, c as getParentStruct, d as validateFieldBaseType, i as validateRefArgument, l as getParentTypeName, n as hasAnyViewAnnotation, o as getDbTableOwner, r as validateQueryScope, s as getNavTargetTypeName, t as findFKFieldsPointingTo, u as refActionAnnotation } from "./validation-utils-
|
|
1
|
+
import { a as getAnnotationAlias, c as getParentStruct, d as validateFieldBaseType, i as validateRefArgument, l as getParentTypeName, n as hasAnyViewAnnotation, o as getDbTableOwner, r as validateQueryScope, s as getNavTargetTypeName, t as findFKFieldsPointingTo, u as refActionAnnotation } from "./validation-utils-Bh7RVrVl.mjs";
|
|
2
2
|
export { findFKFieldsPointingTo, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, refActionAnnotation, validateFieldBaseType, validateQueryScope, validateRefArgument };
|
package/dist/sync.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
require("./
|
|
3
|
-
|
|
4
|
-
require("./
|
|
2
|
+
const require_db_view = require("./db-view-DIGN4079.cjs");
|
|
3
|
+
require("./validator-BIuw_T0k.cjs");
|
|
4
|
+
require("./nested-writer-v_LPR1yJ.cjs");
|
|
5
5
|
//#region src/schema/schema-hash.ts
|
|
6
6
|
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
7
7
|
function extractFieldSnapshots(fields, typeMapper) {
|
|
@@ -285,7 +285,7 @@ var SyncStore = class {
|
|
|
285
285
|
}
|
|
286
286
|
async ensureControlTable() {
|
|
287
287
|
if (!this.controlTable) {
|
|
288
|
-
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-
|
|
288
|
+
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-CDnwVj4q.cjs"));
|
|
289
289
|
this.controlTable = this.space.getTable(AtscriptControl);
|
|
290
290
|
}
|
|
291
291
|
await this.controlTable.ensureTable();
|
|
@@ -421,7 +421,7 @@ var SyncStore = class {
|
|
|
421
421
|
}
|
|
422
422
|
};
|
|
423
423
|
async function readStoredSnapshot(space, tableName, _asView) {
|
|
424
|
-
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-
|
|
424
|
+
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-CDnwVj4q.cjs"));
|
|
425
425
|
const table = space.getTable(AtscriptControl);
|
|
426
426
|
await table.ensureTable();
|
|
427
427
|
const value = (await table.findOne({
|
package/dist/sync.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as AtscriptDbView, t as DbSpace } from "./db-space-
|
|
1
|
+
import { D as TDbForeignKey, E as TDbFieldMeta, F as TDbStorageType, J as TTableOptionDiff, L as TExistingColumn, R as TExistingTableOption, it as TGenericLogger, t as AtscriptDbReadable, w as TDbDefaultValue, x as TColumnDiff } from "./db-readable-DtrC0BGF.cjs";
|
|
2
|
+
import { r as AtscriptDbView, t as DbSpace } from "./db-space-BqVLHVq7.cjs";
|
|
3
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
5
5
|
//#region src/schema/sync-entry.d.ts
|
package/dist/sync.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as AtscriptDbView, t as DbSpace } from "./db-space-
|
|
1
|
+
import { D as TDbForeignKey, E as TDbFieldMeta, F as TDbStorageType, J as TTableOptionDiff, L as TExistingColumn, R as TExistingTableOption, it as TGenericLogger, t as AtscriptDbReadable, w as TDbDefaultValue, x as TColumnDiff } from "./db-readable-RTtqL7H4.mjs";
|
|
2
|
+
import { r as AtscriptDbView, t as DbSpace } from "./db-space-D3QSKx2B.mjs";
|
|
3
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
5
5
|
//#region src/schema/sync-entry.d.ts
|
package/dist/sync.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import
|
|
3
|
-
import "./
|
|
1
|
+
import { h as NoopLogger } from "./db-view-DNwX6_4x.mjs";
|
|
2
|
+
import "./validator-BB5h1Le3.mjs";
|
|
3
|
+
import "./nested-writer-CT2rLURx.mjs";
|
|
4
4
|
//#region src/schema/schema-hash.ts
|
|
5
5
|
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
6
6
|
function extractFieldSnapshots(fields, typeMapper) {
|
|
@@ -284,7 +284,7 @@ var SyncStore = class {
|
|
|
284
284
|
}
|
|
285
285
|
async ensureControlTable() {
|
|
286
286
|
if (!this.controlTable) {
|
|
287
|
-
const { AtscriptControl } = await import("./control-
|
|
287
|
+
const { AtscriptControl } = await import("./control-ExEKWYBE.mjs");
|
|
288
288
|
this.controlTable = this.space.getTable(AtscriptControl);
|
|
289
289
|
}
|
|
290
290
|
await this.controlTable.ensureTable();
|
|
@@ -420,7 +420,7 @@ var SyncStore = class {
|
|
|
420
420
|
}
|
|
421
421
|
};
|
|
422
422
|
async function readStoredSnapshot(space, tableName, _asView) {
|
|
423
|
-
const { AtscriptControl } = await import("./control-
|
|
423
|
+
const { AtscriptControl } = await import("./control-ExEKWYBE.mjs");
|
|
424
424
|
const table = space.getTable(AtscriptControl);
|
|
425
425
|
await table.ensureTable();
|
|
426
426
|
const value = (await table.findOne({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as DepthLimitExceededError } from "./db-error-Cepx-RsQ.mjs";
|
|
1
2
|
import { isDbFieldOp } from "./ops.mjs";
|
|
2
3
|
import { flattenAnnotatedType } from "@atscript/typescript/utils";
|
|
3
4
|
//#region src/patch/patch-types.ts
|
|
@@ -124,6 +125,11 @@ function handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia) {
|
|
|
124
125
|
return false;
|
|
125
126
|
}
|
|
126
127
|
if (value === void 0) return true;
|
|
128
|
+
if (isFrom && dbCtx.depthCheck) {
|
|
129
|
+
const { limit, fromDepthMap } = dbCtx.depthCheck;
|
|
130
|
+
const depth = fromDepthMap.get(stripNumericSegments(ctx.path));
|
|
131
|
+
if (depth !== void 0 && depth > limit) throw new DepthLimitExceededError(dotPathToBracket(ctx.path), limit, depth);
|
|
132
|
+
}
|
|
127
133
|
if (dbCtx.mode === "patch") {
|
|
128
134
|
if (isFrom || isVia) {
|
|
129
135
|
if (isPatchOperatorObject(value)) return validateNavPatchOps(ctx, def, value, fieldName);
|
|
@@ -233,6 +239,42 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
|
|
|
233
239
|
}
|
|
234
240
|
return true;
|
|
235
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Returns `true` when every char in `[start, end)` is an ASCII digit.
|
|
244
|
+
* Matches segments that represent array indices in dot-notation paths.
|
|
245
|
+
*/
|
|
246
|
+
function isNumericSegment(path, start, end) {
|
|
247
|
+
if (start >= end) return false;
|
|
248
|
+
for (let i = start; i < end; i++) {
|
|
249
|
+
const c = path.charCodeAt(i);
|
|
250
|
+
if (c < 48 || c > 57) return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Strips numeric (array-index) segments from a dot-notation path.
|
|
256
|
+
* `"children.0.grandchildren.10.foo"` → `"children.grandchildren.foo"`.
|
|
257
|
+
*/
|
|
258
|
+
function stripNumericSegments(path) {
|
|
259
|
+
if (!path) return path;
|
|
260
|
+
let out = "";
|
|
261
|
+
let segStart = 0;
|
|
262
|
+
for (let i = 0; i <= path.length; i++) if (i === path.length || path.charCodeAt(i) === 46) {
|
|
263
|
+
if (!isNumericSegment(path, segStart, i)) {
|
|
264
|
+
if (out) out += ".";
|
|
265
|
+
out += path.slice(segStart, i);
|
|
266
|
+
}
|
|
267
|
+
segStart = i + 1;
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Converts dot-notation array-index paths to bracket notation:
|
|
273
|
+
* `"children.0.grandchildren.1.foo"` → `"children[0].grandchildren[1].foo"`.
|
|
274
|
+
*/
|
|
275
|
+
function dotPathToBracket(path) {
|
|
276
|
+
return path.replace(/\.(\d+)/g, "[$1]");
|
|
277
|
+
}
|
|
236
278
|
//#endregion
|
|
237
279
|
//#region src/validator.ts
|
|
238
280
|
/** Singleton db validator plugin — stateless, safe to share across all validators. */
|