@atscript/db 0.1.149 → 0.1.150
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.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/{buckets-DJiYlMXc.d.cts → buckets-24Ac4sIS.d.cts} +80 -5
- package/dist/{buckets-CNdTOnei.d.mts → buckets-XOz0Bw5x.d.mts} +80 -5
- package/dist/{column-diff-CUU4GvYg.cjs → column-diff-CKKBVDvx.cjs} +646 -341
- package/dist/{column-diff-Cp6ZoyRE.mjs → column-diff-CKLDWBbo.mjs} +522 -253
- package/dist/{fk-diff-BQ4krij8.d.cts → fk-diff-CCLiJ9sJ.d.cts} +2 -1
- package/dist/{fk-diff-DsaIijVX.d.mts → fk-diff-R8eU7gq-.d.mts} +2 -1
- package/dist/index.cjs +10 -3
- package/dist/index.d.cts +66 -4
- package/dist/index.d.mts +66 -4
- package/dist/index.mjs +3 -3
- package/dist/plugin.cjs +143 -36
- package/dist/plugin.mjs +138 -31
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/{relation-helpers-kX7jjgME.d.cts → relation-helpers-DjVJN2ng.d.cts} +1 -1
- package/dist/{relation-helpers-Ba0v49sn.d.mts → relation-helpers-H7UEVkHs.d.mts} +1 -1
- package/dist/search-fields-DBIZKwyb.mjs +117 -0
- package/dist/search-fields-DwrsJUC1.cjs +158 -0
- package/dist/shared.cjs +3 -1
- package/dist/shared.d.cts +19 -1
- package/dist/shared.d.mts +19 -1
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +1 -1
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +1 -1
- package/dist/{validation-utils-Dq0uZ7ef.mjs → validation-utils-DqZAH6aT.mjs} +44 -2
- package/dist/{validation-utils-Da2GjobR.cjs → validation-utils-debIYpq0.cjs} +56 -2
- package/package.json +6 -6
- package/dist/derived-rules-0sKn4f5C.mjs +0 -44
- package/dist/derived-rules-YstgIxG-.cjs +0 -67
- package/dist/numeric-operand-B1jKH7x5.mjs +0 -21
- package/dist/numeric-operand-DKfiRLYp.cjs +0 -26
package/dist/plugin.cjs
CHANGED
|
@@ -34,10 +34,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
34
34
|
}) : target, mod));
|
|
35
35
|
//#endregion
|
|
36
36
|
const require_aggregate_fns = require("./aggregate-fns-C-UJRobm.cjs");
|
|
37
|
-
const
|
|
38
|
-
const require_numeric_operand = require("./numeric-operand-DKfiRLYp.cjs");
|
|
37
|
+
const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
|
|
39
38
|
require("./consts-BzRfCcH2.cjs");
|
|
40
|
-
const require_validation_utils = require("./validation-utils-
|
|
39
|
+
const require_validation_utils = require("./validation-utils-debIYpq0.cjs");
|
|
41
40
|
let node_path = require("node:path");
|
|
42
41
|
node_path = __toESM(node_path, 1);
|
|
43
42
|
let node_url = require("node:url");
|
|
@@ -383,7 +382,7 @@ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
|
|
|
383
382
|
const errors = [];
|
|
384
383
|
const owner = token.parentNode;
|
|
385
384
|
const target = args[0]?.text;
|
|
386
|
-
for (const name of
|
|
385
|
+
for (const name of require_search_fields.DB_ENTITY_ANNOTATIONS) if (owner.countAnnotations(name) > 0) {
|
|
387
386
|
errors.push({
|
|
388
387
|
message: `A @db.alias type cannot carry @${name} — it names a join scope over "${target ?? "…"}", not a table or view`,
|
|
389
388
|
severity: 1,
|
|
@@ -509,7 +508,7 @@ function viewProps(owner) {
|
|
|
509
508
|
function validateJsonChain(fieldName, ref, doc, range) {
|
|
510
509
|
const info = jsonChainInfo(ref, doc);
|
|
511
510
|
if (!info.resolved || info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) return [];
|
|
512
|
-
if (info.leafType !== void 0 &&
|
|
511
|
+
if (info.leafType !== void 0 && require_search_fields.JSON_LEAF_TYPES.has(info.leafType)) return [];
|
|
513
512
|
return [{
|
|
514
513
|
message: `Field "${fieldName}" reads "${info.typeName}.${info.chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
|
|
515
514
|
severity: 1,
|
|
@@ -552,6 +551,68 @@ function validateViewInterface(owner, doc) {
|
|
|
552
551
|
}
|
|
553
552
|
//#endregion
|
|
554
553
|
//#region src/plugin/annotations/column.ts
|
|
554
|
+
/** Nav-field annotations: a navigation field has no column on this table. */
|
|
555
|
+
const NAV_ANNOTATIONS = [
|
|
556
|
+
"db.rel.to",
|
|
557
|
+
"db.rel.from",
|
|
558
|
+
"db.rel.via"
|
|
559
|
+
];
|
|
560
|
+
/**
|
|
561
|
+
* Compile-time placement rules of `@db.column.version.exempt` (E1–E5, W1, W2).
|
|
562
|
+
* E6 (derived) lives in `DERIVED_INCOMPATIBLE`. The runtime mirror is
|
|
563
|
+
* `TableMetadata._finalizeVersionExempt`.
|
|
564
|
+
* @since 0.1.150
|
|
565
|
+
*/
|
|
566
|
+
function validateVersionExempt(token) {
|
|
567
|
+
const errors = [];
|
|
568
|
+
const field = token.parentNode;
|
|
569
|
+
const fail = (message, severity = 1) => {
|
|
570
|
+
errors.push({
|
|
571
|
+
message,
|
|
572
|
+
severity,
|
|
573
|
+
range: token.range
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
const tag = "@db.column.version.exempt";
|
|
577
|
+
if (field.countAnnotations("db.column.version") > 0) fail(`${tag} cannot mark the version column itself`);
|
|
578
|
+
if (field.countAnnotations("meta.id") > 0) fail(`${tag} cannot mark a primary key — it identifies the row and is never patched`);
|
|
579
|
+
if (NAV_ANNOTATIONS.some((n) => field.countAnnotations(n) > 0)) fail(`${tag} cannot mark a navigation field — related rows follow their own table's versioning`);
|
|
580
|
+
if (field.countAnnotations("db.ignore") > 0) fail(`${tag} has no effect on an ignored field`, 2);
|
|
581
|
+
let node = field;
|
|
582
|
+
let tableOwner;
|
|
583
|
+
let reported = false;
|
|
584
|
+
while (node.ownerNode && (0, _atscript_core.isStructure)(node.ownerNode)) {
|
|
585
|
+
let up = node.ownerNode.ownerNode;
|
|
586
|
+
let viaArray = false;
|
|
587
|
+
while (up && (0, _atscript_core.isArray)(up)) {
|
|
588
|
+
viaArray = true;
|
|
589
|
+
up = up.ownerNode;
|
|
590
|
+
}
|
|
591
|
+
if (!up) break;
|
|
592
|
+
if ((0, _atscript_core.isInterface)(up)) {
|
|
593
|
+
tableOwner = up;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
if (!(0, _atscript_core.isProp)(up)) break;
|
|
597
|
+
if (!reported && viaArray) {
|
|
598
|
+
fail(`${tag} cannot sit inside an array — mark the array field '${up.id ?? ""}' itself; array elements are not separate columns`);
|
|
599
|
+
reported = true;
|
|
600
|
+
} else if (!reported && up.countAnnotations("db.json") > 0) {
|
|
601
|
+
fail(`${tag} cannot sit inside a @db.json field — mark the @db.json field '${up.id ?? ""}' itself; a JSON column is written as one value`);
|
|
602
|
+
reported = true;
|
|
603
|
+
}
|
|
604
|
+
node = up;
|
|
605
|
+
}
|
|
606
|
+
if (tableOwner && (0, _atscript_core.isInterface)(tableOwner) && tableOwner.countAnnotations("db.table") > 0) {
|
|
607
|
+
const struct = tableOwner.getDefinition();
|
|
608
|
+
let hasVersion = false;
|
|
609
|
+
if (struct && (0, _atscript_core.isStructure)(struct)) {
|
|
610
|
+
for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) hasVersion = true;
|
|
611
|
+
}
|
|
612
|
+
if (!hasVersion) fail(`${tag} has no effect — table '${tableOwner.id ?? ""}' declares no @db.column.version`, 2);
|
|
613
|
+
}
|
|
614
|
+
return errors;
|
|
615
|
+
}
|
|
555
616
|
const dbColumnAnnotations = {
|
|
556
617
|
patch: { strategy: new _atscript_core.AnnotationSpec({
|
|
557
618
|
description: "Defines the **patching strategy** for updating nested objects.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@db.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
|
|
@@ -658,7 +719,7 @@ const dbColumnAnnotations = {
|
|
|
658
719
|
fail("@db.column.derived is only valid on a top-level field of a @db.table interface");
|
|
659
720
|
return errors;
|
|
660
721
|
}
|
|
661
|
-
for (const [name, why] of
|
|
722
|
+
for (const [name, why] of require_search_fields.DERIVED_INCOMPATIBLE) if (field.countAnnotations(name) > 0) fail(`@db.column.derived cannot coexist with @${name} — ${why}`);
|
|
662
723
|
const definition = field.getDefinition();
|
|
663
724
|
if (!definition || !(0, _atscript_core.isRef)(definition) || !definition.hasChain) {
|
|
664
725
|
fail("@db.column.derived requires a chain reference into a @db.json field of the same table (e.g. `customerId: Order.payload.customer.id`)");
|
|
@@ -690,7 +751,7 @@ const dbColumnAnnotations = {
|
|
|
690
751
|
fail(`@db.column.derived path '${path}' reads inside a @db.encrypted field — ciphertext cannot be extracted`);
|
|
691
752
|
return errors;
|
|
692
753
|
}
|
|
693
|
-
if (info.leafType === void 0 || !
|
|
754
|
+
if (info.leafType === void 0 || !require_search_fields.JSON_LEAF_TYPES.has(info.leafType)) {
|
|
694
755
|
fail(`@db.column.derived path '${path}' must end at a string, number or boolean leaf` + (info.leafType ? ` (got '${info.leafType}')` : ""));
|
|
695
756
|
return errors;
|
|
696
757
|
}
|
|
@@ -714,38 +775,55 @@ const dbColumnAnnotations = {
|
|
|
714
775
|
filterable: columnCapability("filterable", "filtering"),
|
|
715
776
|
sortable: columnCapability("sortable", "sorting"),
|
|
716
777
|
searchable: new _atscript_core.AnnotationSpec({
|
|
717
|
-
description: "Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String
|
|
778
|
+
description: "Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String and integer columns only: an integer column (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) matches when the term is a substring of the number's decimal text (`2946` finds `29461277`). Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.table \"jobs\"\nexport interface Job {\n @db.column.searchable\n jobName: string\n @db.column.searchable\n description: string\n @db.column.searchable\n refNo: number.int\n}\n```\n",
|
|
718
779
|
nodeType: ["prop"],
|
|
719
780
|
passedWhenReferred: false,
|
|
720
781
|
multiple: false,
|
|
721
782
|
validate(token, _args, doc) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
|
|
727
|
-
nodeType: ["prop"],
|
|
728
|
-
passedWhenReferred: false,
|
|
729
|
-
validate(token, _args, doc) {
|
|
730
|
-
const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.column.version", "number");
|
|
731
|
-
if (token.parentNode.has("optional")) errors.push({
|
|
732
|
-
message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
|
|
783
|
+
const verdict = require_validation_utils.searchFieldVerdict(token.parentNode, doc);
|
|
784
|
+
if (!("problem" in verdict)) return [];
|
|
785
|
+
return [{
|
|
786
|
+
message: `@db.column.searchable needs a string or an integer field — "${token.parentNode.id}" ${verdict.problem}`,
|
|
733
787
|
severity: 1,
|
|
734
788
|
range: token.range
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
789
|
+
}];
|
|
790
|
+
}
|
|
791
|
+
}),
|
|
792
|
+
version: {
|
|
793
|
+
$self: new _atscript_core.AnnotationSpec({
|
|
794
|
+
description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected. Fields marked `@db.column.version.exempt` do not bump it.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
|
|
795
|
+
nodeType: ["prop"],
|
|
796
|
+
passedWhenReferred: false,
|
|
797
|
+
validate(token, _args, doc) {
|
|
798
|
+
const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.column.version", "number");
|
|
799
|
+
if (token.parentNode.has("optional")) errors.push({
|
|
800
|
+
message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
|
|
742
801
|
severity: 1,
|
|
743
802
|
range: token.range
|
|
744
803
|
});
|
|
804
|
+
const struct = require_validation_utils.getParentStruct(token);
|
|
805
|
+
if (struct) {
|
|
806
|
+
let count = 0;
|
|
807
|
+
for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
|
|
808
|
+
if (count > 1) errors.push({
|
|
809
|
+
message: "At most one @db.column.version per table",
|
|
810
|
+
severity: 1,
|
|
811
|
+
range: token.range
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return errors;
|
|
745
815
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
816
|
+
}),
|
|
817
|
+
exempt: new _atscript_core.AnnotationSpec({
|
|
818
|
+
description: "Marks a field as **version-exempt**: a patch that writes ONLY exempt fields (`updateOne` / `bulkUpdate` without `$cas`, `updateMany`) leaves `@db.column.version` unchanged and adds no version check. A patch touching any other field bumps as usual; `$cas`, replace and `touchMany` always bump. Use it for derived/reporting columns refreshed in the background (scores, counters, caches) so they do not invalidate versions held by editors. On an object field it covers every nested field.\n\n**Example:**\n```atscript\n@db.column.version\nversion: number.int\n@db.column.version.exempt\nscore: number\n```\n",
|
|
819
|
+
nodeType: ["prop"],
|
|
820
|
+
passedWhenReferred: false,
|
|
821
|
+
multiple: false,
|
|
822
|
+
validate(token) {
|
|
823
|
+
return validateVersionExempt(token);
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
}
|
|
749
827
|
},
|
|
750
828
|
default: {
|
|
751
829
|
$self: new _atscript_core.AnnotationSpec({
|
|
@@ -858,17 +936,23 @@ const dbColumnAnnotations = {
|
|
|
858
936
|
}
|
|
859
937
|
}),
|
|
860
938
|
writeOnly: new _atscript_core.AnnotationSpec({
|
|
861
|
-
description: "Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one.\n\n**Example:**\n```atscript\n@db.writeOnly\n@db.encrypted\ncredentials?: string\n```\n",
|
|
939
|
+
description: "Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one. It cannot be combined with `@db.index.fulltext` or `@db.column.searchable`: a search hit would reveal the sealed value.\n\n**Example:**\n```atscript\n@db.writeOnly\n@db.encrypted\ncredentials?: string\n```\n",
|
|
862
940
|
nodeType: ["prop"],
|
|
863
941
|
passedWhenReferred: false,
|
|
864
942
|
multiple: false,
|
|
865
943
|
validate(token, _args, _doc) {
|
|
866
944
|
const errors = [];
|
|
867
|
-
|
|
945
|
+
const field = token.parentNode;
|
|
946
|
+
if (field.countAnnotations("meta.id") > 0) errors.push({
|
|
868
947
|
message: `@db.writeOnly cannot coexist with @meta.id — the primary key must be readable`,
|
|
869
948
|
severity: 1,
|
|
870
949
|
range: token.range
|
|
871
950
|
});
|
|
951
|
+
for (const name of ["db.index.fulltext", "db.column.searchable"]) if (field.countAnnotations(name) > 0) errors.push({
|
|
952
|
+
message: `@db.writeOnly cannot coexist with @${name} — search results would reveal the sealed value`,
|
|
953
|
+
severity: 1,
|
|
954
|
+
range: token.range
|
|
955
|
+
});
|
|
872
956
|
return errors;
|
|
873
957
|
}
|
|
874
958
|
})
|
|
@@ -931,7 +1015,7 @@ function operandProblem(prop, doc) {
|
|
|
931
1015
|
const leaf = def && (0, _atscript_core.isRef)(def) ? doc.unwindType(def.id, def.chain)?.def : def;
|
|
932
1016
|
const base = require_validation_utils.primitiveBaseType(leaf);
|
|
933
1017
|
if (base === void 0) return void 0;
|
|
934
|
-
return
|
|
1018
|
+
return require_search_fields.numericTypeProblem({
|
|
935
1019
|
base,
|
|
936
1020
|
tags: (0, _atscript_core.isPrimitive)(leaf) ? leaf.tags : void 0
|
|
937
1021
|
});
|
|
@@ -1051,7 +1135,30 @@ const dbIndexAnnotations = { index: {
|
|
|
1051
1135
|
}
|
|
1052
1136
|
}),
|
|
1053
1137
|
fulltext: new _atscript_core.AnnotationSpec({
|
|
1054
|
-
description: "Full-text search index. Fields sharing the same index name form a composite full-text index.\n\n**Example:**\n```atscript\n@db.index.fulltext \"ft_content\"\ntitle: string\n\n@db.index.fulltext \"ft_content\", 5\nbio: string\n```\n",
|
|
1138
|
+
description: "Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.index.fulltext \"ft_content\"\ntitle: string\n\n@db.index.fulltext \"ft_content\", 5\nbio: string\n\n@db.index.fulltext \"ft_content\"\n@db.index.unique\nrefNo: number.int\n```\n",
|
|
1139
|
+
validate(token, args, doc) {
|
|
1140
|
+
const field = token.parentNode;
|
|
1141
|
+
const verdict = require_validation_utils.searchFieldVerdict(field, doc);
|
|
1142
|
+
if ("problem" in verdict) return [{
|
|
1143
|
+
message: `@db.index.fulltext needs a string or an integer field — "${field.id}" ${verdict.problem}`,
|
|
1144
|
+
severity: 1,
|
|
1145
|
+
range: token.range
|
|
1146
|
+
}];
|
|
1147
|
+
if (verdict.kind !== "integer") return [];
|
|
1148
|
+
const errors = [];
|
|
1149
|
+
const missing = require_validation_utils.integerMemberIndexProblem(token);
|
|
1150
|
+
if (missing) errors.push({
|
|
1151
|
+
message: missing,
|
|
1152
|
+
severity: 1,
|
|
1153
|
+
range: token.range
|
|
1154
|
+
});
|
|
1155
|
+
if (args[1]) errors.push({
|
|
1156
|
+
message: "the weight is ignored on an integer member (it is matched by exact number, not by text)",
|
|
1157
|
+
severity: 2,
|
|
1158
|
+
range: args[1].range
|
|
1159
|
+
});
|
|
1160
|
+
return errors;
|
|
1161
|
+
},
|
|
1055
1162
|
nodeType: ["prop"],
|
|
1056
1163
|
passedWhenReferred: false,
|
|
1057
1164
|
multiple: true,
|
|
@@ -1065,7 +1172,7 @@ const dbIndexAnnotations = { index: {
|
|
|
1065
1172
|
optional: true,
|
|
1066
1173
|
name: "weight",
|
|
1067
1174
|
type: "number",
|
|
1068
|
-
description: "Field importance in search results (higher = more relevant). Defaults to `1`. Supported by databases with weighted fulltext (e.g., MongoDB, PostgreSQL)."
|
|
1175
|
+
description: "Field importance in search results (higher = more relevant). Defaults to `1`. Supported by databases with weighted fulltext (e.g., MongoDB, PostgreSQL). Ignored on integer members."
|
|
1069
1176
|
}]
|
|
1070
1177
|
}),
|
|
1071
1178
|
geo: new _atscript_core.AnnotationSpec({
|
|
@@ -1110,7 +1217,7 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
|
|
|
1110
1217
|
}
|
|
1111
1218
|
const dbRelAnnotations = { rel: {
|
|
1112
1219
|
FK: new _atscript_core.AnnotationSpec({
|
|
1113
|
-
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` (
|
|
1220
|
+
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` (published by its canonical readable controller) supplies the picker URL.\n- In `/meta` (and `/meta/form/:name`) the marker is inherited through reference chains: a field declared as `code: Issue.code` where `Issue.code: Dict.code` carries `@db.rel.FK` is served with `ref` pointing at the terminal field (`Dict.code`) and `db.rel.FK: true`, so view fields get the dictionary picker. Runtime metadata is untouched.\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",
|
|
1114
1221
|
nodeType: ["prop"],
|
|
1115
1222
|
passedWhenReferred: false,
|
|
1116
1223
|
argument: {
|
|
@@ -1702,7 +1809,7 @@ const dbTableAnnotations = {
|
|
|
1702
1809
|
}
|
|
1703
1810
|
}),
|
|
1704
1811
|
http: { path: new _atscript_core.AnnotationSpec({
|
|
1705
|
-
description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields.
|
|
1812
|
+
description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields. After app.init() the app publishes the canonical controller route here at runtime; a model mounted on several routes needs `canonical: true` on one of its controllers (or declare this annotation and mount that controller without an explicit prefix).\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.http.path \"/authors\"\nexport interface Author { ... }\n```\n",
|
|
1706
1813
|
nodeType: ["interface"],
|
|
1707
1814
|
passedWhenReferred: false,
|
|
1708
1815
|
argument: {
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, o as SUPPORTED_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-CyaZyb9I.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { t as numericTypeProblem } from "./numeric-operand-B1jKH7x5.mjs";
|
|
2
|
+
import { a as DERIVED_INCOMPATIBLE, i as DB_ENTITY_ANNOTATIONS, o as JSON_LEAF_TYPES, r as numericTypeProblem } from "./search-fields-DBIZKwyb.mjs";
|
|
4
3
|
import "./consts-C_-5_pFq.mjs";
|
|
5
|
-
import { S as
|
|
4
|
+
import { C as validateFieldBaseType, S as validateExclusiveWith, _ as getParentTypeName, a as hasAnyViewAnnotation, b as refActionAnnotation, d as viewJoins, f as viewScopeTypes, g as getParentStruct, h as getNavTargetTypeName, i as forEachFieldRef, l as validateQueryScope, m as getDbTableOwner, n as findFKFieldsPointingTo, o as isAliasDecl, p as getAnnotationAlias, r as findViewCycle, s as isDbSourceDecl, t as earlierJoinTargets, u as validateRefArgument, v as integerMemberIndexProblem, w as validateSiblingStringField, x as searchFieldVerdict, y as primitiveBaseType } from "./validation-utils-DqZAH6aT.mjs";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { AnnotationSpec, DEFAULT_FORMAT, getFieldsForType, isArray, isInterface, isPrimitive, isProp, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
|
|
@@ -516,6 +515,68 @@ function validateViewInterface(owner, doc) {
|
|
|
516
515
|
}
|
|
517
516
|
//#endregion
|
|
518
517
|
//#region src/plugin/annotations/column.ts
|
|
518
|
+
/** Nav-field annotations: a navigation field has no column on this table. */
|
|
519
|
+
const NAV_ANNOTATIONS = [
|
|
520
|
+
"db.rel.to",
|
|
521
|
+
"db.rel.from",
|
|
522
|
+
"db.rel.via"
|
|
523
|
+
];
|
|
524
|
+
/**
|
|
525
|
+
* Compile-time placement rules of `@db.column.version.exempt` (E1–E5, W1, W2).
|
|
526
|
+
* E6 (derived) lives in `DERIVED_INCOMPATIBLE`. The runtime mirror is
|
|
527
|
+
* `TableMetadata._finalizeVersionExempt`.
|
|
528
|
+
* @since 0.1.150
|
|
529
|
+
*/
|
|
530
|
+
function validateVersionExempt(token) {
|
|
531
|
+
const errors = [];
|
|
532
|
+
const field = token.parentNode;
|
|
533
|
+
const fail = (message, severity = 1) => {
|
|
534
|
+
errors.push({
|
|
535
|
+
message,
|
|
536
|
+
severity,
|
|
537
|
+
range: token.range
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
const tag = "@db.column.version.exempt";
|
|
541
|
+
if (field.countAnnotations("db.column.version") > 0) fail(`${tag} cannot mark the version column itself`);
|
|
542
|
+
if (field.countAnnotations("meta.id") > 0) fail(`${tag} cannot mark a primary key — it identifies the row and is never patched`);
|
|
543
|
+
if (NAV_ANNOTATIONS.some((n) => field.countAnnotations(n) > 0)) fail(`${tag} cannot mark a navigation field — related rows follow their own table's versioning`);
|
|
544
|
+
if (field.countAnnotations("db.ignore") > 0) fail(`${tag} has no effect on an ignored field`, 2);
|
|
545
|
+
let node = field;
|
|
546
|
+
let tableOwner;
|
|
547
|
+
let reported = false;
|
|
548
|
+
while (node.ownerNode && isStructure(node.ownerNode)) {
|
|
549
|
+
let up = node.ownerNode.ownerNode;
|
|
550
|
+
let viaArray = false;
|
|
551
|
+
while (up && isArray(up)) {
|
|
552
|
+
viaArray = true;
|
|
553
|
+
up = up.ownerNode;
|
|
554
|
+
}
|
|
555
|
+
if (!up) break;
|
|
556
|
+
if (isInterface(up)) {
|
|
557
|
+
tableOwner = up;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (!isProp(up)) break;
|
|
561
|
+
if (!reported && viaArray) {
|
|
562
|
+
fail(`${tag} cannot sit inside an array — mark the array field '${up.id ?? ""}' itself; array elements are not separate columns`);
|
|
563
|
+
reported = true;
|
|
564
|
+
} else if (!reported && up.countAnnotations("db.json") > 0) {
|
|
565
|
+
fail(`${tag} cannot sit inside a @db.json field — mark the @db.json field '${up.id ?? ""}' itself; a JSON column is written as one value`);
|
|
566
|
+
reported = true;
|
|
567
|
+
}
|
|
568
|
+
node = up;
|
|
569
|
+
}
|
|
570
|
+
if (tableOwner && isInterface(tableOwner) && tableOwner.countAnnotations("db.table") > 0) {
|
|
571
|
+
const struct = tableOwner.getDefinition();
|
|
572
|
+
let hasVersion = false;
|
|
573
|
+
if (struct && isStructure(struct)) {
|
|
574
|
+
for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) hasVersion = true;
|
|
575
|
+
}
|
|
576
|
+
if (!hasVersion) fail(`${tag} has no effect — table '${tableOwner.id ?? ""}' declares no @db.column.version`, 2);
|
|
577
|
+
}
|
|
578
|
+
return errors;
|
|
579
|
+
}
|
|
519
580
|
const dbColumnAnnotations = {
|
|
520
581
|
patch: { strategy: new AnnotationSpec({
|
|
521
582
|
description: "Defines the **patching strategy** for updating nested objects.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@db.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
|
|
@@ -678,38 +739,55 @@ const dbColumnAnnotations = {
|
|
|
678
739
|
filterable: columnCapability("filterable", "filtering"),
|
|
679
740
|
sortable: columnCapability("sortable", "sorting"),
|
|
680
741
|
searchable: new AnnotationSpec({
|
|
681
|
-
description: "Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String
|
|
742
|
+
description: "Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String and integer columns only: an integer column (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) matches when the term is a substring of the number's decimal text (`2946` finds `29461277`). Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.table \"jobs\"\nexport interface Job {\n @db.column.searchable\n jobName: string\n @db.column.searchable\n description: string\n @db.column.searchable\n refNo: number.int\n}\n```\n",
|
|
682
743
|
nodeType: ["prop"],
|
|
683
744
|
passedWhenReferred: false,
|
|
684
745
|
multiple: false,
|
|
685
746
|
validate(token, _args, doc) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
|
|
691
|
-
nodeType: ["prop"],
|
|
692
|
-
passedWhenReferred: false,
|
|
693
|
-
validate(token, _args, doc) {
|
|
694
|
-
const errors = validateFieldBaseType(token, doc, "@db.column.version", "number");
|
|
695
|
-
if (token.parentNode.has("optional")) errors.push({
|
|
696
|
-
message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
|
|
747
|
+
const verdict = searchFieldVerdict(token.parentNode, doc);
|
|
748
|
+
if (!("problem" in verdict)) return [];
|
|
749
|
+
return [{
|
|
750
|
+
message: `@db.column.searchable needs a string or an integer field — "${token.parentNode.id}" ${verdict.problem}`,
|
|
697
751
|
severity: 1,
|
|
698
752
|
range: token.range
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
753
|
+
}];
|
|
754
|
+
}
|
|
755
|
+
}),
|
|
756
|
+
version: {
|
|
757
|
+
$self: new AnnotationSpec({
|
|
758
|
+
description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected. Fields marked `@db.column.version.exempt` do not bump it.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
|
|
759
|
+
nodeType: ["prop"],
|
|
760
|
+
passedWhenReferred: false,
|
|
761
|
+
validate(token, _args, doc) {
|
|
762
|
+
const errors = validateFieldBaseType(token, doc, "@db.column.version", "number");
|
|
763
|
+
if (token.parentNode.has("optional")) errors.push({
|
|
764
|
+
message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
|
|
706
765
|
severity: 1,
|
|
707
766
|
range: token.range
|
|
708
767
|
});
|
|
768
|
+
const struct = getParentStruct(token);
|
|
769
|
+
if (struct) {
|
|
770
|
+
let count = 0;
|
|
771
|
+
for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
|
|
772
|
+
if (count > 1) errors.push({
|
|
773
|
+
message: "At most one @db.column.version per table",
|
|
774
|
+
severity: 1,
|
|
775
|
+
range: token.range
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return errors;
|
|
709
779
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
780
|
+
}),
|
|
781
|
+
exempt: new AnnotationSpec({
|
|
782
|
+
description: "Marks a field as **version-exempt**: a patch that writes ONLY exempt fields (`updateOne` / `bulkUpdate` without `$cas`, `updateMany`) leaves `@db.column.version` unchanged and adds no version check. A patch touching any other field bumps as usual; `$cas`, replace and `touchMany` always bump. Use it for derived/reporting columns refreshed in the background (scores, counters, caches) so they do not invalidate versions held by editors. On an object field it covers every nested field.\n\n**Example:**\n```atscript\n@db.column.version\nversion: number.int\n@db.column.version.exempt\nscore: number\n```\n",
|
|
783
|
+
nodeType: ["prop"],
|
|
784
|
+
passedWhenReferred: false,
|
|
785
|
+
multiple: false,
|
|
786
|
+
validate(token) {
|
|
787
|
+
return validateVersionExempt(token);
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
}
|
|
713
791
|
},
|
|
714
792
|
default: {
|
|
715
793
|
$self: new AnnotationSpec({
|
|
@@ -822,17 +900,23 @@ const dbColumnAnnotations = {
|
|
|
822
900
|
}
|
|
823
901
|
}),
|
|
824
902
|
writeOnly: new AnnotationSpec({
|
|
825
|
-
description: "Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one.\n\n**Example:**\n```atscript\n@db.writeOnly\n@db.encrypted\ncredentials?: string\n```\n",
|
|
903
|
+
description: "Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one. It cannot be combined with `@db.index.fulltext` or `@db.column.searchable`: a search hit would reveal the sealed value.\n\n**Example:**\n```atscript\n@db.writeOnly\n@db.encrypted\ncredentials?: string\n```\n",
|
|
826
904
|
nodeType: ["prop"],
|
|
827
905
|
passedWhenReferred: false,
|
|
828
906
|
multiple: false,
|
|
829
907
|
validate(token, _args, _doc) {
|
|
830
908
|
const errors = [];
|
|
831
|
-
|
|
909
|
+
const field = token.parentNode;
|
|
910
|
+
if (field.countAnnotations("meta.id") > 0) errors.push({
|
|
832
911
|
message: `@db.writeOnly cannot coexist with @meta.id — the primary key must be readable`,
|
|
833
912
|
severity: 1,
|
|
834
913
|
range: token.range
|
|
835
914
|
});
|
|
915
|
+
for (const name of ["db.index.fulltext", "db.column.searchable"]) if (field.countAnnotations(name) > 0) errors.push({
|
|
916
|
+
message: `@db.writeOnly cannot coexist with @${name} — search results would reveal the sealed value`,
|
|
917
|
+
severity: 1,
|
|
918
|
+
range: token.range
|
|
919
|
+
});
|
|
836
920
|
return errors;
|
|
837
921
|
}
|
|
838
922
|
})
|
|
@@ -1015,7 +1099,30 @@ const dbIndexAnnotations = { index: {
|
|
|
1015
1099
|
}
|
|
1016
1100
|
}),
|
|
1017
1101
|
fulltext: new AnnotationSpec({
|
|
1018
|
-
description: "Full-text search index. Fields sharing the same index name form a composite full-text index.\n\n**Example:**\n```atscript\n@db.index.fulltext \"ft_content\"\ntitle: string\n\n@db.index.fulltext \"ft_content\", 5\nbio: string\n```\n",
|
|
1102
|
+
description: "Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.index.fulltext \"ft_content\"\ntitle: string\n\n@db.index.fulltext \"ft_content\", 5\nbio: string\n\n@db.index.fulltext \"ft_content\"\n@db.index.unique\nrefNo: number.int\n```\n",
|
|
1103
|
+
validate(token, args, doc) {
|
|
1104
|
+
const field = token.parentNode;
|
|
1105
|
+
const verdict = searchFieldVerdict(field, doc);
|
|
1106
|
+
if ("problem" in verdict) return [{
|
|
1107
|
+
message: `@db.index.fulltext needs a string or an integer field — "${field.id}" ${verdict.problem}`,
|
|
1108
|
+
severity: 1,
|
|
1109
|
+
range: token.range
|
|
1110
|
+
}];
|
|
1111
|
+
if (verdict.kind !== "integer") return [];
|
|
1112
|
+
const errors = [];
|
|
1113
|
+
const missing = integerMemberIndexProblem(token);
|
|
1114
|
+
if (missing) errors.push({
|
|
1115
|
+
message: missing,
|
|
1116
|
+
severity: 1,
|
|
1117
|
+
range: token.range
|
|
1118
|
+
});
|
|
1119
|
+
if (args[1]) errors.push({
|
|
1120
|
+
message: "the weight is ignored on an integer member (it is matched by exact number, not by text)",
|
|
1121
|
+
severity: 2,
|
|
1122
|
+
range: args[1].range
|
|
1123
|
+
});
|
|
1124
|
+
return errors;
|
|
1125
|
+
},
|
|
1019
1126
|
nodeType: ["prop"],
|
|
1020
1127
|
passedWhenReferred: false,
|
|
1021
1128
|
multiple: true,
|
|
@@ -1029,7 +1136,7 @@ const dbIndexAnnotations = { index: {
|
|
|
1029
1136
|
optional: true,
|
|
1030
1137
|
name: "weight",
|
|
1031
1138
|
type: "number",
|
|
1032
|
-
description: "Field importance in search results (higher = more relevant). Defaults to `1`. Supported by databases with weighted fulltext (e.g., MongoDB, PostgreSQL)."
|
|
1139
|
+
description: "Field importance in search results (higher = more relevant). Defaults to `1`. Supported by databases with weighted fulltext (e.g., MongoDB, PostgreSQL). Ignored on integer members."
|
|
1033
1140
|
}]
|
|
1034
1141
|
}),
|
|
1035
1142
|
geo: new AnnotationSpec({
|
|
@@ -1074,7 +1181,7 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
|
|
|
1074
1181
|
}
|
|
1075
1182
|
const dbRelAnnotations = { rel: {
|
|
1076
1183
|
FK: new AnnotationSpec({
|
|
1077
|
-
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` (
|
|
1184
|
+
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` (published by its canonical readable controller) supplies the picker URL.\n- In `/meta` (and `/meta/form/:name`) the marker is inherited through reference chains: a field declared as `code: Issue.code` where `Issue.code: Dict.code` carries `@db.rel.FK` is served with `ref` pointing at the terminal field (`Dict.code`) and `db.rel.FK: true`, so view fields get the dictionary picker. Runtime metadata is untouched.\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",
|
|
1078
1185
|
nodeType: ["prop"],
|
|
1079
1186
|
passedWhenReferred: false,
|
|
1080
1187
|
argument: {
|
|
@@ -1666,7 +1773,7 @@ const dbTableAnnotations = {
|
|
|
1666
1773
|
}
|
|
1667
1774
|
}),
|
|
1668
1775
|
http: { path: new AnnotationSpec({
|
|
1669
|
-
description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields.
|
|
1776
|
+
description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields. After app.init() the app publishes the canonical controller route here at runtime; a model mounted on several routes needs `canonical: true` on one of its controllers (or declare this annotation and mount that controller without an explicit prefix).\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.http.path \"/authors\"\nexport interface Author { ... }\n```\n",
|
|
1670
1777
|
nodeType: ["interface"],
|
|
1671
1778
|
passedWhenReferred: false,
|
|
1672
1779
|
argument: {
|
package/dist/rel.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Hn as TGenericLogger, Pt as TTableResolver, U as TDbForeignKey, Wt as TableMetadata, Yt as BaseDbAdapter, et as TDbRelation, h as AtscriptDbWritable, m as AtscriptDbTableLike, zt as TWriteTableResolver } from "./buckets-24Ac4sIS.cjs";
|
|
2
2
|
import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
|
|
3
|
-
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-
|
|
3
|
+
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-DjVJN2ng.cjs";
|
|
4
4
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
5
5
|
import { Validator } from "@atscript/typescript/utils";
|
|
6
6
|
|
package/dist/rel.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Hn as TGenericLogger, Pt as TTableResolver, U as TDbForeignKey, Wt as TableMetadata, Yt as BaseDbAdapter, et as TDbRelation, h as AtscriptDbWritable, m as AtscriptDbTableLike, zt as TWriteTableResolver } from "./buckets-XOz0Bw5x.mjs";
|
|
2
2
|
import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
|
|
3
|
-
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-
|
|
3
|
+
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-H7UEVkHs.mjs";
|
|
4
4
|
import { Validator } from "@atscript/typescript/utils";
|
|
5
5
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
6
6
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as TDbForeignKey, et as TDbRelation } from "./buckets-
|
|
1
|
+
import { U as TDbForeignKey, et as TDbRelation } from "./buckets-24Ac4sIS.cjs";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/rel/relation-helpers.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as TDbForeignKey, et as TDbRelation } from "./buckets-
|
|
1
|
+
import { U as TDbForeignKey, et as TDbRelation } from "./buckets-XOz0Bw5x.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/rel/relation-helpers.d.ts
|