@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.
Files changed (42) hide show
  1. package/dist/db-error-Cepx-RsQ.mjs +29 -0
  2. package/dist/db-error-D8tQhNgM.cjs +40 -0
  3. package/dist/{db-readable-ktHqF277.d.cts → db-readable-DtrC0BGF.d.cts} +122 -115
  4. package/dist/{db-readable-GBjlsJXR.d.mts → db-readable-RTtqL7H4.d.mts} +123 -116
  5. package/dist/{db-space-DB0MT6B3.d.cts → db-space-BqVLHVq7.d.cts} +14 -1
  6. package/dist/{db-space-DpaXOdRp.d.mts → db-space-D3QSKx2B.d.mts} +14 -1
  7. package/dist/{db-validator-plugin-Cz4QoDWg.d.cts → db-validator-plugin-DDvYyv5t.d.mts} +10 -0
  8. package/dist/{db-validator-plugin-KC4aNIQq.d.mts → db-validator-plugin-DRGMCEn3.d.cts} +10 -0
  9. package/dist/{db-view-DjDKgytJ.cjs → db-view-DIGN4079.cjs} +81 -19
  10. package/dist/{db-view-B1j_IKSf.mjs → db-view-DNwX6_4x.mjs} +72 -10
  11. package/dist/index.cjs +5 -4
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.mts +6 -6
  14. package/dist/index.mjs +4 -3
  15. package/dist/{nested-writer-CNDyhg2L.mjs → nested-writer-CT2rLURx.mjs} +9 -16
  16. package/dist/{nested-writer-BIQ6EfaR.cjs → nested-writer-v_LPR1yJ.cjs} +14 -27
  17. package/dist/ops.d.mts +1 -1
  18. package/dist/plugin.cjs +99 -9
  19. package/dist/plugin.mjs +99 -9
  20. package/dist/rel.cjs +1 -1
  21. package/dist/rel.d.cts +2 -2
  22. package/dist/rel.d.mts +2 -2
  23. package/dist/rel.mjs +1 -1
  24. package/dist/shared.cjs +1 -1
  25. package/dist/shared.mjs +1 -1
  26. package/dist/sync.cjs +5 -5
  27. package/dist/sync.d.cts +2 -2
  28. package/dist/sync.d.mts +2 -2
  29. package/dist/sync.mjs +5 -5
  30. package/dist/{validator-D_7Fqzs4.mjs → validator-BB5h1Le3.mjs} +42 -0
  31. package/dist/{validator-0iGuvGOD.cjs → validator-BIuw_T0k.cjs} +42 -0
  32. package/dist/validator.cjs +1 -1
  33. package/dist/validator.d.cts +1 -1
  34. package/dist/validator.d.mts +3 -3
  35. package/dist/validator.mjs +1 -1
  36. package/package.json +6 -6
  37. /package/dist/{control-D1QdBO21.cjs → control-CDnwVj4q.cjs} +0 -0
  38. /package/dist/{control-DBd_ff5-.mjs → control-ExEKWYBE.mjs} +0 -0
  39. /package/dist/{ops-DcHDxrjX.d.mts → ops-C61kelof.d.mts} +0 -0
  40. /package/dist/{validation-utils-BiG3pLP0.cjs → validation-utils-B9WJv9aH.cjs} +0 -0
  41. /package/dist/{validation-utils-aNrgK-cj.mjs → validation-utils-Bh7RVrVl.mjs} +0 -0
  42. /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 (error instanceof _atscript_typescript_utils.ValidatorError && items.length > 1) throw new _atscript_typescript_utils.ValidatorError(error.errors.map((err) => ({
93
- ...err,
94
- path: `[${i}].${err.path}`
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-DcHDxrjX.mjs";
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-BiG3pLP0.cjs");
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 constraint on this field. The field must use a chain reference type (e.g., `User.id`) to specify the FK target.\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",
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-aNrgK-cj.mjs";
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 constraint on this field. The field must use a chain reference type (e.g., `User.id`) to specify the FK target.\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",
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 { A as TDbForeignKey, L as TDbRelation, Q as TTableResolver, et as TWriteTableResolver, l as TGenericLogger, o as BaseDbAdapter, s as TableMetadata } from "./db-readable-ktHqF277.cjs";
2
- import { t as DbValidationContext } from "./db-validator-plugin-Cz4QoDWg.cjs";
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 { A as TDbForeignKey, L as TDbRelation, Q as TTableResolver, et as TWriteTableResolver, l as TGenericLogger, o as BaseDbAdapter, s as TableMetadata } from "./db-readable-GBjlsJXR.mjs";
2
- import { t as DbValidationContext } from "./db-validator-plugin-KC4aNIQq.mjs";
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-BiG3pLP0.cjs");
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-aNrgK-cj.mjs";
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("./nested-writer-BIQ6EfaR.cjs");
3
- const require_db_view = require("./db-view-DjDKgytJ.cjs");
4
- require("./validator-0iGuvGOD.cjs");
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-D1QdBO21.cjs"));
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-D1QdBO21.cjs"));
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 { A as TDbForeignKey, B as TExistingColumn, D as TDbDefaultValue, R as TDbStorageType, V as TExistingTableOption, Z as TTableOptionDiff, k as TDbFieldMeta, l as TGenericLogger, t as AtscriptDbReadable, w as TColumnDiff } from "./db-readable-ktHqF277.cjs";
2
- import { r as AtscriptDbView, t as DbSpace } from "./db-space-DB0MT6B3.cjs";
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 { A as TDbForeignKey, B as TExistingColumn, D as TDbDefaultValue, R as TDbStorageType, V as TExistingTableOption, Z as TTableOptionDiff, k as TDbFieldMeta, l as TGenericLogger, t as AtscriptDbReadable, w as TColumnDiff } from "./db-readable-GBjlsJXR.mjs";
2
- import { r as AtscriptDbView, t as DbSpace } from "./db-space-DpaXOdRp.mjs";
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 "./nested-writer-CNDyhg2L.mjs";
2
- import { h as NoopLogger } from "./db-view-B1j_IKSf.mjs";
3
- import "./validator-D_7Fqzs4.mjs";
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-DBd_ff5-.mjs");
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-DBd_ff5-.mjs");
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. */