@atscript/db 0.1.161 → 0.1.163
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agg.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
- package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
- package/dist/{buckets-B4T_alUe.d.cts → buckets-B8UOod-v.d.cts} +488 -57
- package/dist/{buckets-8mG67Mtw.d.mts → buckets-CHyspjYP.d.mts} +488 -57
- package/dist/{column-diff-DpeUbhEI.cjs → column-diff-BIGX3sRH.cjs} +853 -232
- package/dist/{column-diff-DR1glykD.mjs → column-diff-BfNIlMwX.mjs} +836 -233
- package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
- package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
- package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
- package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
- package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
- package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
- package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-BNGawZi9.d.cts} +34 -10
- package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-rj9N2QLw.d.mts} +34 -10
- package/dist/index.cjs +15 -11
- package/dist/index.d.cts +24 -21
- package/dist/index.d.mts +24 -21
- package/dist/index.mjs +10 -12
- package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
- package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
- package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
- package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
- package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
- package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
- package/dist/ops.cjs +6 -21
- package/dist/ops.mjs +6 -21
- package/dist/plugin.cjs +263 -323
- package/dist/plugin.mjs +163 -223
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +3 -3
- package/dist/rel.d.mts +3 -3
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-DloMywNs.d.cts} +1 -1
- package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-zwt19E7r.d.mts} +1 -1
- package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
- package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
- package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-B49-RCWd.cjs} +65 -1
- package/dist/{search-fields-CE8cmj9X.mjs → search-fields-BPNsxrCk.mjs} +48 -2
- package/dist/search-fields-CHqqcC3W.d.cts +30 -0
- package/dist/search-fields-CHqqcC3W.d.mts +30 -0
- package/dist/server-stamped-B6XbUQ4p.cjs +33 -0
- package/dist/server-stamped-BoP56K9M.mjs +22 -0
- package/dist/shared.cjs +42 -26
- package/dist/shared.d.cts +98 -4
- package/dist/shared.d.mts +98 -4
- package/dist/shared.mjs +4 -2
- package/dist/sync.cjs +228 -65
- package/dist/sync.d.cts +41 -5
- package/dist/sync.d.mts +41 -5
- package/dist/sync.mjs +228 -66
- package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
- package/dist/validator-DX97RzC2.d.cts +108 -0
- package/dist/validator-DX97RzC2.d.mts +108 -0
- package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
- package/dist/validator.cjs +6 -3
- package/dist/validator.d.cts +5 -4
- package/dist/validator.d.mts +5 -4
- package/dist/validator.mjs +5 -4
- package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-BcgDq7M9.mjs} +364 -20
- package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-BiDLCYMw.cjs} +452 -18
- package/package.json +6 -6
package/dist/plugin.cjs
CHANGED
|
@@ -33,14 +33,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
enumerable: true
|
|
34
34
|
}) : target, mod));
|
|
35
35
|
//#endregion
|
|
36
|
-
const require_aggregate_fns = require("./aggregate-fns-
|
|
37
|
-
const require_search_fields = require("./search-fields-
|
|
36
|
+
const require_aggregate_fns = require("./aggregate-fns-DupYzFD7.cjs");
|
|
37
|
+
const require_search_fields = require("./search-fields-B49-RCWd.cjs");
|
|
38
38
|
require("./consts-BzRfCcH2.cjs");
|
|
39
|
-
const
|
|
39
|
+
const require_view_validation = require("./view-validation-BiDLCYMw.cjs");
|
|
40
|
+
let _atscript_core = require("@atscript/core");
|
|
40
41
|
let node_path = require("node:path");
|
|
41
42
|
node_path = __toESM(node_path, 1);
|
|
42
43
|
let node_url = require("node:url");
|
|
43
|
-
let _atscript_core = require("@atscript/core");
|
|
44
44
|
//#region src/plugin/manifest.ts
|
|
45
45
|
/**
|
|
46
46
|
* Renders the model-manifest module: an inventory of every exported
|
|
@@ -70,7 +70,7 @@ async function generateModelManifest(options, output, format, repo) {
|
|
|
70
70
|
let specifier = node_path.default.relative(manifestDir, docPath).split(node_path.default.sep).join("/");
|
|
71
71
|
if (!specifier.startsWith(".")) specifier = `./${specifier}`;
|
|
72
72
|
for (const [exportName, node] of [...doc.exports.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
73
|
-
if (!
|
|
73
|
+
if (!require_view_validation.isDbSourceDecl(node)) continue;
|
|
74
74
|
let alias = exportName;
|
|
75
75
|
for (let n = 1; usedAliases.has(alias); n++) alias = `${exportName}_${n}`;
|
|
76
76
|
usedAliases.add(alias);
|
|
@@ -79,7 +79,7 @@ async function generateModelManifest(options, output, format, repo) {
|
|
|
79
79
|
exportName,
|
|
80
80
|
specifier,
|
|
81
81
|
isView: node.countAnnotations("db.view") > 0 || node.countAnnotations("db.view.for") > 0,
|
|
82
|
-
space:
|
|
82
|
+
space: require_view_validation.getAnnotationAlias(node, "db.space") || "default"
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -134,7 +134,7 @@ async function generateModelManifest(options, output, format, repo) {
|
|
|
134
134
|
*/
|
|
135
135
|
/** The `@db.view.for` entry name of a view node. */
|
|
136
136
|
function viewEntry(view) {
|
|
137
|
-
return
|
|
137
|
+
return require_view_validation.getAnnotationAlias(view, "db.view.for");
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
140
140
|
* `@db.view.filter` — and a conditional `@db.agg.*` of the same view: the
|
|
@@ -144,7 +144,7 @@ function viewEntry(view) {
|
|
|
144
144
|
function viewFilterScope(view) {
|
|
145
145
|
const entry = viewEntry(view);
|
|
146
146
|
return entry ? {
|
|
147
|
-
allowedTypes:
|
|
147
|
+
allowedTypes: require_view_validation.viewScopeTypes(view),
|
|
148
148
|
unqualifiedTarget: entry
|
|
149
149
|
} : void 0;
|
|
150
150
|
}
|
|
@@ -154,7 +154,7 @@ function viewFilterScope(view) {
|
|
|
154
154
|
* view's {@link viewJoins} when the caller has not); an unqualified field
|
|
155
155
|
* belongs to the entry.
|
|
156
156
|
*/
|
|
157
|
-
function viewJoinScope(view, join, earlier =
|
|
157
|
+
function viewJoinScope(view, join, earlier = require_view_validation.earlierJoinTargets(require_view_validation.viewJoins(view), join)) {
|
|
158
158
|
const entry = viewEntry(view);
|
|
159
159
|
const target = join.args[0]?.text;
|
|
160
160
|
if (!entry || !target) return;
|
|
@@ -192,12 +192,12 @@ function joinOrderScope(join) {
|
|
|
192
192
|
* @since 0.1.147
|
|
193
193
|
*/
|
|
194
194
|
function computeScope(propToken) {
|
|
195
|
-
const view =
|
|
195
|
+
const view = require_view_validation.getDbTableOwner(propToken);
|
|
196
196
|
return view ? viewHavingScope(view) : void 0;
|
|
197
197
|
}
|
|
198
198
|
/** The condition of a `@db.agg.*` on a view field: the scope of the view's `@db.view.filter`. */
|
|
199
199
|
function aggConditionScope(propToken) {
|
|
200
|
-
const view =
|
|
200
|
+
const view = require_view_validation.getDbTableOwner(propToken);
|
|
201
201
|
return view ? viewFilterScope(view) : void 0;
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
@@ -208,7 +208,7 @@ function aggConditionScope(propToken) {
|
|
|
208
208
|
function aggFieldScope(propToken) {
|
|
209
209
|
const def = propToken.parentNode?.getDefinition();
|
|
210
210
|
const refType = def && (0, _atscript_core.isRef)(def) && def.hasChain ? def.id : void 0;
|
|
211
|
-
const view = refType ? void 0 :
|
|
211
|
+
const view = refType ? void 0 : require_view_validation.getDbTableOwner(propToken);
|
|
212
212
|
const target = refType ?? (view ? viewEntry(view) : void 0);
|
|
213
213
|
return target ? {
|
|
214
214
|
allowedTypes: [],
|
|
@@ -221,9 +221,9 @@ function aggFieldScope(propToken) {
|
|
|
221
221
|
* unqualified field belongs to the related type.
|
|
222
222
|
*/
|
|
223
223
|
function relFilterScope(field) {
|
|
224
|
-
const target =
|
|
224
|
+
const target = require_view_validation.getNavTargetTypeName(field);
|
|
225
225
|
if (!target) return;
|
|
226
|
-
const junction =
|
|
226
|
+
const junction = require_view_validation.getAnnotationAlias(field, "db.rel.via");
|
|
227
227
|
return {
|
|
228
228
|
allowedTypes: junction ? [target, junction] : [target],
|
|
229
229
|
unqualifiedTarget: target
|
|
@@ -238,12 +238,12 @@ const fieldScopes = {
|
|
|
238
238
|
viewFilter: (arg) => arg.parentNode ? viewFilterScope(arg.parentNode) : void 0,
|
|
239
239
|
viewJoin: (arg) => {
|
|
240
240
|
const view = arg.parentNode;
|
|
241
|
-
const joins = view ?
|
|
241
|
+
const joins = view ? require_view_validation.viewJoins(view) : [];
|
|
242
242
|
const join = joinOfArg(joins, arg);
|
|
243
|
-
return view && join ? viewJoinScope(view, join,
|
|
243
|
+
return view && join ? viewJoinScope(view, join, require_view_validation.earlierJoinTargets(joins, join)) : void 0;
|
|
244
244
|
},
|
|
245
245
|
joinOrder: (arg) => {
|
|
246
|
-
const join = arg.parentNode ? joinOfArg(
|
|
246
|
+
const join = arg.parentNode ? joinOfArg(require_view_validation.viewJoins(arg.parentNode), arg) : void 0;
|
|
247
247
|
return join ? joinOrderScope(join) : void 0;
|
|
248
248
|
},
|
|
249
249
|
viewHaving: (arg) => arg.parentNode ? viewHavingScope(arg.parentNode) : void 0,
|
|
@@ -254,7 +254,7 @@ const fieldScopes = {
|
|
|
254
254
|
};
|
|
255
255
|
/** `refFilter` of the `@db.view.joins` target: a view source or a `@db.alias` of one. */
|
|
256
256
|
function isJoinTarget(decl) {
|
|
257
|
-
return
|
|
257
|
+
return require_view_validation.isDbSourceDecl(decl) || require_view_validation.isAliasDecl(decl);
|
|
258
258
|
}
|
|
259
259
|
/** `refFilter` of `@db.rel.via`: a `@db.table`. */
|
|
260
260
|
function isDbTable(decl) {
|
|
@@ -303,7 +303,7 @@ function validateAggArgs(name, token, args, doc) {
|
|
|
303
303
|
});
|
|
304
304
|
return errors;
|
|
305
305
|
}
|
|
306
|
-
errors.push(...
|
|
306
|
+
errors.push(...require_view_validation.validateQueryScope(condition, scope, doc));
|
|
307
307
|
const prop = token.parentNode;
|
|
308
308
|
if (conditionalIsNullable(name) && prop && !prop.has("optional")) {
|
|
309
309
|
const field = prop.id ?? "field";
|
|
@@ -340,7 +340,7 @@ function aggSpec(name, description, field, types, extra = "") {
|
|
|
340
340
|
}, CONDITION_ARG],
|
|
341
341
|
validate(token, args, doc) {
|
|
342
342
|
const errors = validateAggArgs(name, token, args, doc);
|
|
343
|
-
if (types) errors.push(...
|
|
343
|
+
if (types) errors.push(...require_view_validation.validateFieldBaseType(token, doc, `@db.agg.${name}`, types));
|
|
344
344
|
return errors;
|
|
345
345
|
}
|
|
346
346
|
});
|
|
@@ -360,7 +360,7 @@ const dbAggAnnotations = { agg: {
|
|
|
360
360
|
//#region src/plugin/annotations/alias.ts
|
|
361
361
|
/** The `accept` rule of a view-source ref argument (`@db.view.for`, `@db.alias`). */
|
|
362
362
|
const VIEW_SOURCE_ARGUMENT = {
|
|
363
|
-
accept:
|
|
363
|
+
accept: require_view_validation.isDbSourceDecl,
|
|
364
364
|
expected: "must be a @db.table or a @db.view."
|
|
365
365
|
};
|
|
366
366
|
/**
|
|
@@ -369,14 +369,14 @@ const VIEW_SOURCE_ARGUMENT = {
|
|
|
369
369
|
* @since 0.1.141
|
|
370
370
|
*/
|
|
371
371
|
const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
|
|
372
|
-
description: "Declares a **join alias**: a type alias of a `@db.table` or `@db.view` that a view can join under its own name — to join the same table twice or to self-join the entry table. Only valid on `export type X = Target`, where `Target` is the argument. The alias is a scope name inside view definitions, not a table: it is never synced or registered on a `DbSpace`, and it cannot be the `@db.view.for` entry
|
|
372
|
+
description: (0, _atscript_core.annotationExample)("Declares a **join alias**: a type alias of a `@db.table` or `@db.view` that a view can join under its own name — to join the same table twice or to self-join the entry table. Only valid on `export type X = Target`, where `Target` is the argument. The alias is a scope name inside view definitions, not a table: it is never synced or registered on a `DbSpace`, and it cannot be the `@db.view.for` entry.", "@db.alias Employee", "export type Manager = Employee", "", "@db.view.for Employee", "@db.view.joins Manager, `Manager.id = Employee.managerId`, 'left'", "export interface Staff {", " id: Employee.id", " managerName?: Manager.name", "}"),
|
|
373
373
|
nodeType: ["type"],
|
|
374
374
|
passedWhenReferred: false,
|
|
375
375
|
argument: {
|
|
376
376
|
name: "target",
|
|
377
377
|
type: "ref",
|
|
378
378
|
description: "The aliased table or view type (must have @db.table or @db.view).",
|
|
379
|
-
refFilter:
|
|
379
|
+
refFilter: require_view_validation.isDbSourceDecl
|
|
380
380
|
},
|
|
381
381
|
validate(token, args, doc) {
|
|
382
382
|
const errors = [];
|
|
@@ -397,7 +397,7 @@ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
|
|
|
397
397
|
severity: 1,
|
|
398
398
|
range: token.range
|
|
399
399
|
});
|
|
400
|
-
errors.push(...
|
|
400
|
+
errors.push(...require_view_validation.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
|
|
401
401
|
return errors;
|
|
402
402
|
}
|
|
403
403
|
}) };
|
|
@@ -406,7 +406,7 @@ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
|
|
|
406
406
|
const CURRENCY_CODE_PATTERN = /^[A-Z0-9]{2,10}$/;
|
|
407
407
|
const dbAmountAnnotations = { amount: { currency: {
|
|
408
408
|
$self: new _atscript_core.AnnotationSpec({
|
|
409
|
-
description: "Hard-coded **currency code** for this amount field. Use when a table stores amounts in a single, fixed currency.\n\nFor per-row currency, use `@db.amount.currency.ref 'fieldName'` instead.\n\nOnly valid on `decimal` fields — money in floating-point is wrong
|
|
409
|
+
description: (0, _atscript_core.annotationExample)("Hard-coded **currency code** for this amount field. Use when a table stores amounts in a single, fixed currency.\n\nFor per-row currency, use `@db.amount.currency.ref 'fieldName'` instead.\n\nOnly valid on `decimal` fields — money in floating-point is wrong.", "@db.amount.currency 'EUR'", "amount: decimal"),
|
|
410
410
|
nodeType: ["prop"],
|
|
411
411
|
multiple: false,
|
|
412
412
|
argument: {
|
|
@@ -416,11 +416,11 @@ const dbAmountAnnotations = { amount: { currency: {
|
|
|
416
416
|
},
|
|
417
417
|
validate(token, args, doc) {
|
|
418
418
|
const errors = [];
|
|
419
|
-
errors.push(...
|
|
420
|
-
errors.push(...
|
|
419
|
+
errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.amount.currency", "decimal"));
|
|
420
|
+
errors.push(...require_view_validation.validateExclusiveWith(token, "@db.amount.currency", [{
|
|
421
421
|
key: "db.amount.currency.ref",
|
|
422
422
|
displayName: "@db.amount.currency.ref"
|
|
423
|
-
}]));
|
|
423
|
+
}], doc));
|
|
424
424
|
const code = args[0]?.text;
|
|
425
425
|
if (code && !CURRENCY_CODE_PATTERN.test(code)) errors.push({
|
|
426
426
|
message: `@db.amount.currency '${code}': invalid currency code (expected 2–10 uppercase letters or digits)`,
|
|
@@ -431,7 +431,7 @@ const dbAmountAnnotations = { amount: { currency: {
|
|
|
431
431
|
}
|
|
432
432
|
}),
|
|
433
433
|
ref: new _atscript_core.AnnotationSpec({
|
|
434
|
-
description: "Binds this amount to a **sibling field** that holds its currency code. Use when each row may have a different currency.\n\nThe referenced field should be of type `db.currencyCode` (or at least a `string`).\n\nOnly valid on `decimal` fields
|
|
434
|
+
description: (0, _atscript_core.annotationExample)("Binds this amount to a **sibling field** that holds its currency code. Use when each row may have a different currency.\n\nThe referenced field should be of type `db.currencyCode` (or at least a `string`).\n\nOnly valid on `decimal` fields.", "currency: db.currencyCode", "@db.amount.currency.ref 'currency'", "amount: decimal"),
|
|
435
435
|
nodeType: ["prop"],
|
|
436
436
|
passedWhenReferred: false,
|
|
437
437
|
multiple: false,
|
|
@@ -442,121 +442,21 @@ const dbAmountAnnotations = { amount: { currency: {
|
|
|
442
442
|
},
|
|
443
443
|
validate(token, args, doc) {
|
|
444
444
|
const errors = [];
|
|
445
|
-
errors.push(...
|
|
446
|
-
errors.push(...
|
|
445
|
+
errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.amount.currency.ref", "decimal"));
|
|
446
|
+
errors.push(...require_view_validation.validateExclusiveWith(token, "@db.amount.currency.ref", [{
|
|
447
447
|
key: "db.amount.currency",
|
|
448
448
|
displayName: "@db.amount.currency"
|
|
449
449
|
}, {
|
|
450
450
|
key: "db.unit.ref",
|
|
451
451
|
displayName: "@db.unit.ref"
|
|
452
|
-
}]));
|
|
453
|
-
errors.push(...
|
|
452
|
+
}], doc));
|
|
453
|
+
errors.push(...require_view_validation.validateSiblingStringField(token, args, doc, "@db.amount.currency.ref"));
|
|
454
454
|
return errors;
|
|
455
455
|
}
|
|
456
456
|
})
|
|
457
457
|
} } };
|
|
458
458
|
//#endregion
|
|
459
|
-
//#region src/shared/view-validation.ts
|
|
460
|
-
/** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
|
|
461
|
-
const NULL_SAFE_AGG_ANNOTATIONS = require_aggregate_fns.SUPPORTED_AGGREGATE_FNS.filter((fn) => !require_aggregate_fns.NULL_WHEN_EMPTY_AGGREGATE_FNS.has(fn)).map((fn) => `db.agg.${fn}`);
|
|
462
|
-
/**
|
|
463
|
-
* Walks a chain ref step by step (`Order.payload`, `Order.payload.customer`, …)
|
|
464
|
-
* and reports the JSON root, arrays, encryption, optionality and the leaf type.
|
|
465
|
-
* A chain shorter than 2 is not a field path (`resolved: false`).
|
|
466
|
-
* @since 0.1.141
|
|
467
|
-
*/
|
|
468
|
-
function jsonChainInfo(ref, doc) {
|
|
469
|
-
const typeName = ref.id ?? "";
|
|
470
|
-
const chain = ref.chain.map((t) => t.text);
|
|
471
|
-
const info = {
|
|
472
|
-
typeName,
|
|
473
|
-
chain,
|
|
474
|
-
resolved: false,
|
|
475
|
-
viaArray: false,
|
|
476
|
-
viaEncrypted: false,
|
|
477
|
-
optional: false
|
|
478
|
-
};
|
|
479
|
-
if (!typeName || chain.length < 2) return info;
|
|
480
|
-
for (let i = 1; i <= chain.length; i++) {
|
|
481
|
-
const step = doc.unwindType(typeName, chain.slice(0, i));
|
|
482
|
-
if (!step) return info;
|
|
483
|
-
const node = step.node;
|
|
484
|
-
const prop = node && (0, _atscript_core.isProp)(node) ? node : void 0;
|
|
485
|
-
if (prop?.has("optional")) info.optional = true;
|
|
486
|
-
if (prop && prop.countAnnotations("db.encrypted") > 0) info.viaEncrypted = true;
|
|
487
|
-
const isJsonProp = prop !== void 0 && prop.countAnnotations("db.json") > 0;
|
|
488
|
-
const isArr = (0, _atscript_core.isArray)(step.def);
|
|
489
|
-
if (isArr) info.viaArray = true;
|
|
490
|
-
if (info.jsonRoot === void 0 && (isJsonProp || isArr)) info.jsonRoot = i;
|
|
491
|
-
if (i === chain.length) info.leafType = require_validation_utils.primitiveBaseType(step.def);
|
|
492
|
-
}
|
|
493
|
-
info.resolved = true;
|
|
494
|
-
return info;
|
|
495
|
-
}
|
|
496
|
-
/** Props of a view interface (its own structure; views don't use `extends`). */
|
|
497
|
-
function viewProps(owner) {
|
|
498
|
-
if ((0, _atscript_core.isInterface)(owner)) return owner.props;
|
|
499
|
-
const def = owner.getDefinition();
|
|
500
|
-
return def && (0, _atscript_core.isStructure)(def) ? def.props : void 0;
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* VW8: a chain ref that passes through a `@db.json` or array node reads a
|
|
504
|
-
* JSON leaf, which views can only extract as a string / number / boolean.
|
|
505
|
-
* Intermediate nodes that `unwindType` can't reach are skipped (sync-time
|
|
506
|
-
* resolution reports them).
|
|
507
|
-
*/
|
|
508
|
-
function validateJsonChain(fieldName, ref, doc, range) {
|
|
509
|
-
const info = jsonChainInfo(ref, doc);
|
|
510
|
-
if (!info.resolved || info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) return [];
|
|
511
|
-
if (info.leafType !== void 0 && require_search_fields.JSON_LEAF_TYPES.has(info.leafType)) return [];
|
|
512
|
-
return [{
|
|
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`,
|
|
514
|
-
severity: 1,
|
|
515
|
-
range
|
|
516
|
-
}];
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Whole-view checks, run once per `@db.view.for` interface:
|
|
520
|
-
*
|
|
521
|
-
* - VW7 — a field reading from a left-joined table must be optional (the
|
|
522
|
-
* join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
|
|
523
|
-
* fields are exempt (they count 0, never NULL).
|
|
524
|
-
* - VW8 — a chain ref through a `@db.json` or array node must end at a
|
|
525
|
-
* primitive string / number / boolean leaf.
|
|
526
|
-
* @since 0.1.136
|
|
527
|
-
*/
|
|
528
|
-
function validateViewInterface(owner, doc) {
|
|
529
|
-
const errors = [];
|
|
530
|
-
const props = viewProps(owner);
|
|
531
|
-
if (!props) return errors;
|
|
532
|
-
const leftJoined = /* @__PURE__ */ new Set();
|
|
533
|
-
for (const join of require_validation_utils.viewJoins(owner)) {
|
|
534
|
-
const target = join.args[0]?.text;
|
|
535
|
-
if (target && join.args[2]?.text === "left") leftJoined.add(target);
|
|
536
|
-
}
|
|
537
|
-
for (const [fieldName, prop] of props) {
|
|
538
|
-
const def = prop.getDefinition();
|
|
539
|
-
if (!def || !(0, _atscript_core.isRef)(def)) continue;
|
|
540
|
-
const ref = def;
|
|
541
|
-
const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
|
|
542
|
-
if (!range) continue;
|
|
543
|
-
if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
|
|
544
|
-
message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
|
|
545
|
-
severity: 1,
|
|
546
|
-
range
|
|
547
|
-
});
|
|
548
|
-
errors.push(...validateJsonChain(fieldName, ref, doc, range));
|
|
549
|
-
}
|
|
550
|
-
return errors;
|
|
551
|
-
}
|
|
552
|
-
//#endregion
|
|
553
459
|
//#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
460
|
/**
|
|
561
461
|
* Compile-time placement rules of `@db.column.version.exempt` (E1–E5, W1, W2).
|
|
562
462
|
* E6 (derived) lives in `DERIVED_INCOMPATIBLE`. The runtime mirror is
|
|
@@ -576,7 +476,7 @@ function validateVersionExempt(token) {
|
|
|
576
476
|
const tag = "@db.column.version.exempt";
|
|
577
477
|
if (field.countAnnotations("db.column.version") > 0) fail(`${tag} cannot mark the version column itself`);
|
|
578
478
|
if (field.countAnnotations("meta.id") > 0) fail(`${tag} cannot mark a primary key — it identifies the row and is never patched`);
|
|
579
|
-
if (
|
|
479
|
+
if (require_view_validation.isNavProp(field)) fail(`${tag} cannot mark a navigation field — related rows follow their own table's versioning`);
|
|
580
480
|
if (field.countAnnotations("db.ignore") > 0) fail(`${tag} has no effect on an ignored field`, 2);
|
|
581
481
|
let node = field;
|
|
582
482
|
let tableOwner;
|
|
@@ -613,9 +513,51 @@ function validateVersionExempt(token) {
|
|
|
613
513
|
}
|
|
614
514
|
return errors;
|
|
615
515
|
}
|
|
516
|
+
/** Annotations a `@db.onUpdate.inc` field cannot carry, with the reason. */
|
|
517
|
+
const ON_UPDATE_INC_INCOMPATIBLE = [
|
|
518
|
+
["meta.id", "the primary key identifies the row and is never updated"],
|
|
519
|
+
["db.column.version", "the version column is already bumped on every update"],
|
|
520
|
+
["db.onUpdate.now", "a field is either a time stamp or a counter"],
|
|
521
|
+
["db.default", "the counter starts at 0"],
|
|
522
|
+
["db.default.increment", "the counter starts at 0"],
|
|
523
|
+
["db.default.uuid", "the counter starts at 0"],
|
|
524
|
+
["db.default.now", "the counter starts at 0"],
|
|
525
|
+
["db.json", "a JSON column is written as one value"]
|
|
526
|
+
];
|
|
527
|
+
/**
|
|
528
|
+
* Compile-time rules of `@db.onUpdate.inc` (since 0.1.162). The runtime
|
|
529
|
+
* mirror (top level, encryption) is in `TableMetadata`.
|
|
530
|
+
*/
|
|
531
|
+
function validateOnUpdateInc(token, doc) {
|
|
532
|
+
const tag = "@db.onUpdate.inc";
|
|
533
|
+
const errors = require_view_validation.validateFieldBaseType(token, doc, tag, "number");
|
|
534
|
+
const field = token.parentNode;
|
|
535
|
+
const fail = (message) => {
|
|
536
|
+
errors.push({
|
|
537
|
+
message,
|
|
538
|
+
severity: 1,
|
|
539
|
+
range: token.range
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
if (errors.length === 0 && !isIntegerField(field, doc)) fail(`${tag} requires an integer field — use number.int (or @expect.int)`);
|
|
543
|
+
if (field.has("optional")) fail(`${tag} requires a non-optional field — the counter is always populated (default 0)`);
|
|
544
|
+
const struct = require_view_validation.getParentStruct(token);
|
|
545
|
+
if (!struct?.ownerNode || !(0, _atscript_core.isInterface)(struct.ownerNode)) fail(`${tag} must sit on a top-level field of the table, not inside a nested object`);
|
|
546
|
+
errors.push(...require_view_validation.validateIncompatible(token, tag, ON_UPDATE_INC_INCOMPATIBLE, doc));
|
|
547
|
+
return errors;
|
|
548
|
+
}
|
|
549
|
+
/** Whether a numeric field is an integer: `number.int`, an `integer` base or `@expect.int`. */
|
|
550
|
+
function isIntegerField(field, doc) {
|
|
551
|
+
const def = field.getDefinition();
|
|
552
|
+
if (!def || !(0, _atscript_core.isRef)(def)) return true;
|
|
553
|
+
const leaf = doc.unwindType(def.id, def.chain)?.def;
|
|
554
|
+
if (require_view_validation.primitiveBaseType(leaf) !== "number") return true;
|
|
555
|
+
if (leaf.tags?.has("int")) return true;
|
|
556
|
+
return require_view_validation.effectiveAnnotationNames(field, doc).has("expect.int");
|
|
557
|
+
}
|
|
616
558
|
const dbColumnAnnotations = {
|
|
617
559
|
patch: { strategy: new _atscript_core.AnnotationSpec({
|
|
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)
|
|
560
|
+
description: (0, _atscript_core.annotationExample)("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).", "@db.patch.strategy \"merge\"", "settings: {", " notifications: boolean", " preferences: {", " theme: string", " }", "}"),
|
|
619
561
|
nodeType: ["prop"],
|
|
620
562
|
passedWhenReferred: false,
|
|
621
563
|
multiple: false,
|
|
@@ -645,7 +587,7 @@ const dbColumnAnnotations = {
|
|
|
645
587
|
}) },
|
|
646
588
|
column: {
|
|
647
589
|
$self: new _atscript_core.AnnotationSpec({
|
|
648
|
-
description: "Overrides the physical column name in the database. For nested (flattened) fields, the parent prefix is still prepended automatically. Document storage (MongoDB) renames top-level fields only — a nested field keeps its name there
|
|
590
|
+
description: (0, _atscript_core.annotationExample)("Overrides the physical column name in the database. For nested (flattened) fields, the parent prefix is still prepended automatically. Document storage (MongoDB) renames top-level fields only — a nested field keeps its name there.", "@db.column \"first_name\"", "firstName: string", "// → physical column: first_name", "", "// Nested:", "address: {", " @db.column \"zip_code\"", " zip: string", "}", "// → physical column: address__zip_code"),
|
|
649
591
|
nodeType: ["prop"],
|
|
650
592
|
passedWhenReferred: false,
|
|
651
593
|
argument: {
|
|
@@ -655,7 +597,7 @@ const dbColumnAnnotations = {
|
|
|
655
597
|
}
|
|
656
598
|
}),
|
|
657
599
|
renamed: new _atscript_core.AnnotationSpec({
|
|
658
|
-
description: "Specifies the previous local field name for column rename migration. The sync engine generates ALTER TABLE RENAME COLUMN instead of drop+add (on document storage, top-level fields only)
|
|
600
|
+
description: (0, _atscript_core.annotationExample)("Specifies the previous local field name for column rename migration. The sync engine generates ALTER TABLE RENAME COLUMN instead of drop+add (on document storage, top-level fields only).", "@db.column.renamed \"zip\"", "postalCode: string", "// Renames address__zip → address__postalCode"),
|
|
659
601
|
nodeType: ["prop"],
|
|
660
602
|
passedWhenReferred: false,
|
|
661
603
|
argument: {
|
|
@@ -665,7 +607,7 @@ const dbColumnAnnotations = {
|
|
|
665
607
|
}
|
|
666
608
|
}),
|
|
667
609
|
collate: new _atscript_core.AnnotationSpec({
|
|
668
|
-
description: "Portable collation for string comparison and sorting. Adapters map the generic value to their native collation.\n\n- **\"binary\"** — exact byte comparison (case-sensitive)\n- **\"nocase\"** — case-insensitive comparison\n- **\"unicode\"** — full Unicode-aware sorting\n\nFor adapter-specific collations, use `@db.<engine>.collate` instead
|
|
610
|
+
description: (0, _atscript_core.annotationExample)("Portable collation for string comparison and sorting. Adapters map the generic value to their native collation.\n\n- **\"binary\"** — exact byte comparison (case-sensitive)\n- **\"nocase\"** — case-insensitive comparison\n- **\"unicode\"** — full Unicode-aware sorting\n\nFor adapter-specific collations, use `@db.<engine>.collate` instead.", "@db.column.collate \"nocase\"", "username: string"),
|
|
669
611
|
nodeType: ["prop"],
|
|
670
612
|
passedWhenReferred: false,
|
|
671
613
|
argument: {
|
|
@@ -679,11 +621,11 @@ const dbColumnAnnotations = {
|
|
|
679
621
|
description: "Portable collation mode: \"binary\", \"nocase\", or \"unicode\"."
|
|
680
622
|
},
|
|
681
623
|
validate(token, args, doc) {
|
|
682
|
-
return
|
|
624
|
+
return require_view_validation.validateFieldBaseType(token, doc, "@db.column.collate", "string");
|
|
683
625
|
}
|
|
684
626
|
}),
|
|
685
627
|
precision: new _atscript_core.AnnotationSpec({
|
|
686
|
-
description: "Sets decimal precision and scale for database storage. Adapters map this to their native decimal type (e.g., `DECIMAL(10,2)` in SQL, ignored in MongoDB).\n\nFor `decimal` fields the runtime value is a string; for `number` fields this is a DB storage hint only
|
|
628
|
+
description: (0, _atscript_core.annotationExample)("Sets decimal precision and scale for database storage. Adapters map this to their native decimal type (e.g., `DECIMAL(10,2)` in SQL, ignored in MongoDB).\n\nFor `decimal` fields the runtime value is a string; for `number` fields this is a DB storage hint only.", "@db.column.precision 10, 2", "price: decimal"),
|
|
687
629
|
nodeType: ["prop"],
|
|
688
630
|
passedWhenReferred: false,
|
|
689
631
|
argument: [{
|
|
@@ -696,11 +638,11 @@ const dbColumnAnnotations = {
|
|
|
696
638
|
description: "Number of digits after the decimal point."
|
|
697
639
|
}],
|
|
698
640
|
validate(token, args, doc) {
|
|
699
|
-
return
|
|
641
|
+
return require_view_validation.validateFieldBaseType(token, doc, "@db.column.precision", ["number", "decimal"]);
|
|
700
642
|
}
|
|
701
643
|
}),
|
|
702
644
|
derived: new _atscript_core.AnnotationSpec({
|
|
703
|
-
description: "Declares a **derived column**: a read-only column computed from one `string`, `number` or `boolean` leaf inside a `@db.json` field of the **same table**, so the leaf can be filtered, sorted, grouped and indexed like a real column. The field's type is a chain reference into that field (`customerId: Order.payload.customer.id`); the path may not cross an array or a `@db.encrypted` field. Declare the field optional when the path can be absent (a missing leaf reads as `null`). A value written to it is dropped; `$inc` / `$dec` / `$mul` on it are rejected.\n\nStorage per adapter, schema sync and the compatible annotations: https://atscript.dev/db/api/storage#derived-columns
|
|
645
|
+
description: (0, _atscript_core.annotationExample)("Declares a **derived column**: a read-only column computed from one `string`, `number` or `boolean` leaf inside a `@db.json` field of the **same table**, so the leaf can be filtered, sorted, grouped and indexed like a real column. The field's type is a chain reference into that field (`customerId: Order.payload.customer.id`); the path may not cross an array or a `@db.encrypted` field. Declare the field optional when the path can be absent (a missing leaf reads as `null`). A value written to it is dropped; `$inc` / `$dec` / `$mul` on it are rejected.\n\nStorage per adapter, schema sync and the compatible annotations: https://atscript.dev/db/api/storage#derived-columns", "@db.table 'orders'", "export interface Order {", " @meta.id", " id: number", "", " @db.json", " payload: {", " customer: { id: string, vip: boolean }", " total: number", " }", "", " @db.column.derived", " @db.index.plain", " customerId: Order.payload.customer.id", "", " @db.column.derived", " vip?: Order.payload.customer.vip", "}"),
|
|
704
646
|
nodeType: ["prop"],
|
|
705
647
|
passedWhenReferred: false,
|
|
706
648
|
multiple: false,
|
|
@@ -714,7 +656,7 @@ const dbColumnAnnotations = {
|
|
|
714
656
|
range: token.range
|
|
715
657
|
});
|
|
716
658
|
};
|
|
717
|
-
const owner =
|
|
659
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
718
660
|
if (!owner || !(0, _atscript_core.isInterface)(owner) || owner.countAnnotations("db.table") === 0) {
|
|
719
661
|
fail("@db.column.derived is only valid on a top-level field of a @db.table interface");
|
|
720
662
|
return errors;
|
|
@@ -726,12 +668,12 @@ const dbColumnAnnotations = {
|
|
|
726
668
|
return errors;
|
|
727
669
|
}
|
|
728
670
|
const ref = definition;
|
|
729
|
-
const tableName =
|
|
671
|
+
const tableName = require_view_validation.getParentTypeName(token);
|
|
730
672
|
if (ref.id !== tableName) {
|
|
731
673
|
fail(`@db.column.derived must reference the enclosing table '${tableName ?? ""}', not '${ref.id ?? ""}' — a derived column reads its own row`);
|
|
732
674
|
return errors;
|
|
733
675
|
}
|
|
734
|
-
const info = jsonChainInfo(ref, doc);
|
|
676
|
+
const info = require_view_validation.jsonChainInfo(ref, doc);
|
|
735
677
|
const path = `${info.typeName}.${info.chain.join(".")}`;
|
|
736
678
|
const notInsideJson = `@db.column.derived path '${path}' does not read inside a @db.json field — a flattened or scalar column needs no derived column`;
|
|
737
679
|
if (info.chain.length < 2) {
|
|
@@ -769,18 +711,20 @@ const dbColumnAnnotations = {
|
|
|
769
711
|
nodeType: ["prop"],
|
|
770
712
|
passedWhenReferred: false,
|
|
771
713
|
validate(token, _args, doc) {
|
|
772
|
-
return
|
|
714
|
+
return require_view_validation.validateFieldBaseType(token, doc, "@db.column.measure", ["number", "decimal"]);
|
|
773
715
|
}
|
|
774
716
|
}),
|
|
775
717
|
filterable: columnCapability("filterable", "filtering"),
|
|
776
718
|
sortable: columnCapability("sortable", "sorting"),
|
|
777
719
|
searchable: new _atscript_core.AnnotationSpec({
|
|
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
|
|
720
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.table \"jobs\"", "export interface Job {", " @db.column.searchable", " jobName: string", " @db.column.searchable", " description: string", " @db.column.searchable", " refNo: number.int", "}"),
|
|
779
721
|
nodeType: ["prop"],
|
|
780
722
|
passedWhenReferred: false,
|
|
781
723
|
multiple: false,
|
|
782
724
|
validate(token, _args, doc) {
|
|
783
|
-
const
|
|
725
|
+
const sealed = require_view_validation.sealedSearchProblem(token, doc, "@db.column.searchable");
|
|
726
|
+
if (sealed) return [sealed];
|
|
727
|
+
const verdict = require_view_validation.searchFieldVerdict(token.parentNode, doc);
|
|
784
728
|
if (!("problem" in verdict)) return [];
|
|
785
729
|
return [{
|
|
786
730
|
message: `@db.column.searchable needs a string or an integer field — "${token.parentNode.id}" ${verdict.problem}`,
|
|
@@ -791,17 +735,17 @@ const dbColumnAnnotations = {
|
|
|
791
735
|
}),
|
|
792
736
|
version: {
|
|
793
737
|
$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
|
|
738
|
+
description: (0, _atscript_core.annotationExample)("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`.", "@db.column.version", "version: int"),
|
|
795
739
|
nodeType: ["prop"],
|
|
796
740
|
passedWhenReferred: false,
|
|
797
741
|
validate(token, _args, doc) {
|
|
798
|
-
const errors =
|
|
742
|
+
const errors = require_view_validation.validateFieldBaseType(token, doc, "@db.column.version", "number");
|
|
799
743
|
if (token.parentNode.has("optional")) errors.push({
|
|
800
744
|
message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
|
|
801
745
|
severity: 1,
|
|
802
746
|
range: token.range
|
|
803
747
|
});
|
|
804
|
-
const struct =
|
|
748
|
+
const struct = require_view_validation.getParentStruct(token);
|
|
805
749
|
if (struct) {
|
|
806
750
|
let count = 0;
|
|
807
751
|
for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
|
|
@@ -815,7 +759,7 @@ const dbColumnAnnotations = {
|
|
|
815
759
|
}
|
|
816
760
|
}),
|
|
817
761
|
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
|
|
762
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.column.version", "version: number.int", "@db.column.version.exempt", "score: number"),
|
|
819
763
|
nodeType: ["prop"],
|
|
820
764
|
passedWhenReferred: false,
|
|
821
765
|
multiple: false,
|
|
@@ -827,7 +771,7 @@ const dbColumnAnnotations = {
|
|
|
827
771
|
},
|
|
828
772
|
default: {
|
|
829
773
|
$self: new _atscript_core.AnnotationSpec({
|
|
830
|
-
description: "Sets a static DB-level default value (used in DDL DEFAULT clause). For string fields the value is used as-is; for other types it is parsed as JSON
|
|
774
|
+
description: (0, _atscript_core.annotationExample)("Sets a static DB-level default value (used in DDL DEFAULT clause). For string fields the value is used as-is; for other types it is parsed as JSON.", "@db.default \"active\"", "status: string"),
|
|
831
775
|
nodeType: ["prop"],
|
|
832
776
|
passedWhenReferred: false,
|
|
833
777
|
argument: {
|
|
@@ -837,7 +781,7 @@ const dbColumnAnnotations = {
|
|
|
837
781
|
}
|
|
838
782
|
}),
|
|
839
783
|
increment: new _atscript_core.AnnotationSpec({
|
|
840
|
-
description: "Auto-incrementing integer default. Each adapter maps this to its native mechanism (e.g., `AUTO_INCREMENT` in MySQL, `INTEGER PRIMARY KEY` in SQLite, counter collection in MongoDB)
|
|
784
|
+
description: (0, _atscript_core.annotationExample)("Auto-incrementing integer default. Each adapter maps this to its native mechanism (e.g., `AUTO_INCREMENT` in MySQL, `INTEGER PRIMARY KEY` in SQLite, counter collection in MongoDB).", "@db.default.increment", "id: number.int", "", "// With optional start value:", "@db.default.increment 1000", "id: number.int"),
|
|
841
785
|
nodeType: ["prop"],
|
|
842
786
|
passedWhenReferred: false,
|
|
843
787
|
argument: {
|
|
@@ -847,7 +791,7 @@ const dbColumnAnnotations = {
|
|
|
847
791
|
description: "Starting value for the auto-increment sequence. Adapter-specific behavior; some adapters may ignore this."
|
|
848
792
|
},
|
|
849
793
|
validate(token, args, doc) {
|
|
850
|
-
const errors =
|
|
794
|
+
const errors = require_view_validation.validateFieldBaseType(token, doc, "db.default.increment", "number");
|
|
851
795
|
if (token.parentNode.countAnnotations("meta.id") === 0) errors.push({
|
|
852
796
|
message: `@db.default.increment on a field without @meta.id — auto-increment columns must be primary-key columns for schema sync to manage them`,
|
|
853
797
|
severity: 2,
|
|
@@ -857,39 +801,50 @@ const dbColumnAnnotations = {
|
|
|
857
801
|
}
|
|
858
802
|
}),
|
|
859
803
|
uuid: new _atscript_core.AnnotationSpec({
|
|
860
|
-
description: "UUID generation default. Each adapter maps this to its native mechanism (e.g., `DEFAULT (UUID())` in MySQL, `gen_random_uuid()` in PostgreSQL, app-level in SQLite)
|
|
804
|
+
description: (0, _atscript_core.annotationExample)("UUID generation default. Each adapter maps this to its native mechanism (e.g., `DEFAULT (UUID())` in MySQL, `gen_random_uuid()` in PostgreSQL, app-level in SQLite).", "@db.default.uuid", "id: string.uuid"),
|
|
861
805
|
nodeType: ["prop"],
|
|
862
806
|
passedWhenReferred: false,
|
|
863
807
|
validate(token, args, doc) {
|
|
864
|
-
return
|
|
808
|
+
return require_view_validation.validateFieldBaseType(token, doc, "db.default.uuid", "string");
|
|
865
809
|
}
|
|
866
810
|
}),
|
|
867
811
|
now: new _atscript_core.AnnotationSpec({
|
|
868
|
-
description: "Current timestamp default. Each adapter maps this to its native mechanism (e.g., `DEFAULT CURRENT_TIMESTAMP` in MySQL, `DEFAULT now()` in PostgreSQL)
|
|
812
|
+
description: (0, _atscript_core.annotationExample)("Current timestamp default. Each adapter maps this to its native mechanism (e.g., `DEFAULT CURRENT_TIMESTAMP` in MySQL, `DEFAULT now()` in PostgreSQL).", "@db.default.now", "createdAt: number.timestamp"),
|
|
869
813
|
nodeType: ["prop"],
|
|
870
814
|
passedWhenReferred: false,
|
|
871
815
|
validate(token, args, doc) {
|
|
872
|
-
return
|
|
816
|
+
return require_view_validation.validateFieldBaseType(token, doc, "db.default.now", ["number", "string"]);
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
},
|
|
820
|
+
onUpdate: {
|
|
821
|
+
now: new _atscript_core.AnnotationSpec({
|
|
822
|
+
description: (0, _atscript_core.annotationExample)("Sets the field to the current time (epoch ms) on every update: patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`) and replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`). The value is written by the SDK in the same statement on every adapter and overrides a value the payload carries. Inserts are not affected — combine with `@db.default.now` (`number.timestamp.updated` applies both). Writes outside the SDK (raw SQL) do not set it.", "@db.default.now", "@db.onUpdate.now", "updatedAt: number.timestamp"),
|
|
823
|
+
nodeType: ["prop"],
|
|
824
|
+
passedWhenReferred: false,
|
|
825
|
+
validate(token, args, doc) {
|
|
826
|
+
const errors = require_view_validation.validateFieldBaseType(token, doc, "@db.onUpdate.now", "number");
|
|
827
|
+
const field = token.parentNode;
|
|
828
|
+
for (const [name, why] of [["meta.id", "the primary key identifies the row and is never updated"], ["db.column.version", "the version column is adapter-managed"]]) if (field.countAnnotations(name) > 0) errors.push({
|
|
829
|
+
message: `@db.onUpdate.now cannot coexist with @${name} — ${why}`,
|
|
830
|
+
severity: 1,
|
|
831
|
+
range: token.range
|
|
832
|
+
});
|
|
833
|
+
return errors;
|
|
834
|
+
}
|
|
835
|
+
}),
|
|
836
|
+
inc: new _atscript_core.AnnotationSpec({
|
|
837
|
+
description: (0, _atscript_core.annotationExample)("A per-row **change counter**: `0` on insert (unless the insert supplies a value), then `+1` in the same statement on every SDK update of the row — patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`, including patches that write only `@db.column.version.exempt` fields), replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`) and `touchMany`. A value the payload carries is ignored, so clients cannot set it. An empty patch writes nothing and leaves it unchanged. It has no compare-and-set and does not affect `@db.column.version`. Writes outside the SDK (raw SQL, native `ON DELETE SET NULL` actions) do not move it.\n\n**Constraints:** a non-optional integer field at the top level of the table; not the primary key, the version column, a `@db.onUpdate.now`, `@db.default*`, `@db.json` or `@db.encrypted` field.", "@db.onUpdate.inc", "revision: number.int"),
|
|
838
|
+
nodeType: ["prop"],
|
|
839
|
+
passedWhenReferred: false,
|
|
840
|
+
multiple: false,
|
|
841
|
+
validate(token, _args, doc) {
|
|
842
|
+
return validateOnUpdateInc(token, doc);
|
|
873
843
|
}
|
|
874
844
|
})
|
|
875
845
|
},
|
|
876
|
-
onUpdate: { now: new _atscript_core.AnnotationSpec({
|
|
877
|
-
description: "Sets the field to the current time (epoch ms) on every update: patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`) and replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`). The value is written by the SDK in the same statement on every adapter and overrides a value the payload carries. Inserts are not affected — combine with `@db.default.now` (`number.timestamp.updated` applies both). Writes outside the SDK (raw SQL) do not set it.\n\n**Example:**\n```atscript\n@db.default.now\n@db.onUpdate.now\nupdatedAt: number.timestamp\n```\n",
|
|
878
|
-
nodeType: ["prop"],
|
|
879
|
-
passedWhenReferred: false,
|
|
880
|
-
validate(token, args, doc) {
|
|
881
|
-
const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.onUpdate.now", "number");
|
|
882
|
-
const field = token.parentNode;
|
|
883
|
-
for (const [name, why] of [["meta.id", "the primary key identifies the row and is never updated"], ["db.column.version", "the version column is adapter-managed"]]) if (field.countAnnotations(name) > 0) errors.push({
|
|
884
|
-
message: `@db.onUpdate.now cannot coexist with @${name} — ${why}`,
|
|
885
|
-
severity: 1,
|
|
886
|
-
range: token.range
|
|
887
|
-
});
|
|
888
|
-
return errors;
|
|
889
|
-
}
|
|
890
|
-
}) },
|
|
891
846
|
json: new _atscript_core.AnnotationSpec({
|
|
892
|
-
description: "Forces a field to be stored as a single JSON column instead of being flattened into separate columns. Use on nested object fields that should remain as JSON in the database
|
|
847
|
+
description: (0, _atscript_core.annotationExample)("Forces a field to be stored as a single JSON column instead of being flattened into separate columns. Use on nested object fields that should remain as JSON in the database. On a primitive field (string / number / boolean) it stores the value as JSON too: SQL adapters filter the column only with `$exists` and never sort it — drop `@db.json` to get a typed, sortable scalar column.", "@db.json", "metadata: { key: string, value: string }"),
|
|
893
848
|
nodeType: ["prop"],
|
|
894
849
|
passedWhenReferred: false,
|
|
895
850
|
validate(token, _args, doc) {
|
|
@@ -898,7 +853,7 @@ const dbColumnAnnotations = {
|
|
|
898
853
|
if (definition && (0, _atscript_core.isRef)(definition)) {
|
|
899
854
|
const unwound = doc.unwindType(definition.id, definition.chain);
|
|
900
855
|
if (unwound && (0, _atscript_core.isPrimitive)(unwound.def)) errors.push({
|
|
901
|
-
message: "@db.json on a primitive field
|
|
856
|
+
message: "@db.json on a primitive field stores it as one JSON column — filterable only with $exists and never sortable; drop @db.json for a typed scalar column",
|
|
902
857
|
severity: 2,
|
|
903
858
|
range: token.range
|
|
904
859
|
});
|
|
@@ -907,7 +862,7 @@ const dbColumnAnnotations = {
|
|
|
907
862
|
}
|
|
908
863
|
}),
|
|
909
864
|
ignore: new _atscript_core.AnnotationSpec({
|
|
910
|
-
description: "Excludes a field from the database schema. The field exists in the Atscript type but has no column in the DB
|
|
865
|
+
description: (0, _atscript_core.annotationExample)("Excludes a field from the database schema. The field exists in the Atscript type but has no column in the DB.", "@db.ignore", "displayName: string"),
|
|
911
866
|
nodeType: ["prop"],
|
|
912
867
|
passedWhenReferred: false,
|
|
913
868
|
validate(token, _args, _doc) {
|
|
@@ -921,38 +876,18 @@ const dbColumnAnnotations = {
|
|
|
921
876
|
}
|
|
922
877
|
}),
|
|
923
878
|
encrypted: new _atscript_core.AnnotationSpec({
|
|
924
|
-
description: "Encrypts this field at rest. Values are AES-256-GCM encrypted by the core layer before reaching the database and decrypted on read — transparent to application code.\n\nConstraints: the field cannot be filtered, sorted, indexed, used as PK/FK, patched with arithmetic ops, or referenced by search/vector/geo features
|
|
879
|
+
description: (0, _atscript_core.annotationExample)("Encrypts this field at rest. Values are AES-256-GCM encrypted by the core layer before reaching the database and decrypted on read — transparent to application code.\n\nConstraints: the field cannot be filtered, sorted, indexed, used as PK/FK, patched with arithmetic ops, or referenced by search/vector/geo features.", "@db.encrypted", "apiToken?: string"),
|
|
925
880
|
nodeType: ["prop"],
|
|
926
881
|
passedWhenReferred: false,
|
|
927
882
|
multiple: false,
|
|
928
|
-
validate(token, _args,
|
|
929
|
-
const
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
["meta.id", "the primary key must be addressable"],
|
|
933
|
-
["db.rel.FK", "joins are impossible over ciphertext"],
|
|
934
|
-
["db.index.plain", "indexes over ciphertext are meaningless"],
|
|
935
|
-
["db.index.unique", "indexes over ciphertext are meaningless"],
|
|
936
|
-
["db.index.fulltext", "indexes over ciphertext are meaningless"],
|
|
937
|
-
["db.index.geo", "geo indexes over ciphertext are meaningless"],
|
|
938
|
-
["db.search.vector", "vector search over ciphertext is impossible"],
|
|
939
|
-
["db.search.filter", "search filters need cleartext equality"],
|
|
940
|
-
["db.column.version", "the OCC filter needs cleartext equality"],
|
|
941
|
-
["db.default.increment", "engine-side defaults bypass the encryption transform"],
|
|
942
|
-
["db.default.now", "engine-side defaults bypass the encryption transform"],
|
|
943
|
-
["db.onUpdate.now", "the update time is set after the encryption transform"],
|
|
944
|
-
["db.mongo.search.text", "Atlas Search over ciphertext is impossible"],
|
|
945
|
-
["db.mongo.search.autocomplete", "Atlas Search over ciphertext is impossible"]
|
|
946
|
-
]) if (field.countAnnotations(name) > 0) errors.push({
|
|
947
|
-
message: `@db.encrypted cannot coexist with @${name} — ${why}`,
|
|
948
|
-
severity: 1,
|
|
949
|
-
range: token.range
|
|
950
|
-
});
|
|
951
|
-
return errors;
|
|
883
|
+
validate(token, _args, doc) {
|
|
884
|
+
const typeProblem = require_view_validation.encryptedTypeSearchProblem(token, doc);
|
|
885
|
+
const errors = require_view_validation.validateIncompatible(token, "@db.encrypted", require_search_fields.ENCRYPTED_INCOMPATIBLE, doc);
|
|
886
|
+
return typeProblem ? [typeProblem, ...errors] : errors;
|
|
952
887
|
}
|
|
953
888
|
}),
|
|
954
889
|
writeOnly: new _atscript_core.AnnotationSpec({
|
|
955
|
-
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
|
|
890
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.writeOnly", "@db.encrypted", "credentials?: string"),
|
|
956
891
|
nodeType: ["prop"],
|
|
957
892
|
passedWhenReferred: false,
|
|
958
893
|
multiple: false,
|
|
@@ -974,21 +909,15 @@ const dbColumnAnnotations = {
|
|
|
974
909
|
})
|
|
975
910
|
};
|
|
976
911
|
function columnCapability(capability, verb) {
|
|
977
|
-
const example = capability === "filterable" ? " @db.column.filterable
|
|
912
|
+
const example = capability === "filterable" ? [" @db.column.filterable", " email: string"] : [" @db.column.sortable", " createdAt: number.timestamp"];
|
|
978
913
|
return new _atscript_core.AnnotationSpec({
|
|
979
|
-
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 every column the adapter can handle (default-open, back-compat) and the annotation is a no-op. Adapter capability always wins: \`@db.json\` / array columns stay non-sortable (and non-filterable on SQL adapters) even when annotated. \`/meta.fields\` advertises exactly what the gate accepts; index-backed columns additionally carry the advisory \`indexed\` flag.
|
|
980
|
-
|
|
981
|
-
**Example:**
|
|
982
|
-
\`\`\`atscript
|
|
983
|
-
@db.table "users"
|
|
984
|
-
@db.table.${capability} "manual"\nexport interface User {
|
|
985
|
-
` + example + "}\n```\n",
|
|
914
|
+
description: (0, _atscript_core.annotationExample)(`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 every column the adapter can handle (default-open, back-compat) and the annotation is a no-op. Adapter capability always wins: \`@db.json\` / array columns stay non-sortable (and non-filterable on SQL adapters) even when annotated. \`/meta.fields\` advertises exactly what the gate accepts; index-backed columns additionally carry the advisory \`indexed\` flag.`, "@db.table \"users\"", `@db.table.${capability} "manual"`, "export interface User {", ...example, "}"),
|
|
986
915
|
nodeType: ["prop"],
|
|
987
916
|
passedWhenReferred: false,
|
|
988
917
|
multiple: false,
|
|
989
918
|
validate(token, _args, _doc) {
|
|
990
919
|
const errors = [];
|
|
991
|
-
const owner =
|
|
920
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
992
921
|
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
993
922
|
message: `@db.column.${capability} is only valid on fields of a @db.table interface`,
|
|
994
923
|
severity: 1,
|
|
@@ -1029,7 +958,7 @@ function operandProblem(prop, doc) {
|
|
|
1029
958
|
if (computeArg(prop)) return void 0;
|
|
1030
959
|
const def = prop.getDefinition();
|
|
1031
960
|
const leaf = def && (0, _atscript_core.isRef)(def) ? doc.unwindType(def.id, def.chain)?.def : def;
|
|
1032
|
-
const base =
|
|
961
|
+
const base = require_view_validation.primitiveBaseType(leaf);
|
|
1033
962
|
if (base === void 0) return void 0;
|
|
1034
963
|
return require_search_fields.numericTypeProblem({
|
|
1035
964
|
base,
|
|
@@ -1071,7 +1000,7 @@ function validateCompute(token, args, doc) {
|
|
|
1071
1000
|
const errors = [];
|
|
1072
1001
|
const prop = token.parentNode;
|
|
1073
1002
|
const field = prop.id ?? "field";
|
|
1074
|
-
const view =
|
|
1003
|
+
const view = require_view_validation.getDbTableOwner(token);
|
|
1075
1004
|
const error = (message, range = token.range) => errors.push({
|
|
1076
1005
|
message,
|
|
1077
1006
|
severity: 1,
|
|
@@ -1081,15 +1010,15 @@ function validateCompute(token, args, doc) {
|
|
|
1081
1010
|
error("@db.compute is only valid on a field of a @db.view.for view");
|
|
1082
1011
|
return errors;
|
|
1083
1012
|
}
|
|
1084
|
-
errors.push(...
|
|
1013
|
+
errors.push(...require_view_validation.validateExclusiveWith(token, "@db.compute", EXCLUSIVE_WITH, doc));
|
|
1085
1014
|
if (!isPlainNumber(prop, doc)) error(`Field "${field}" has a @db.compute and must be typed \`number\``);
|
|
1086
1015
|
const expr = args[0]?.exprNode;
|
|
1087
1016
|
if (!expr) return errors;
|
|
1088
1017
|
const refs = expr.fieldRefs();
|
|
1089
|
-
const props = viewProps(view) ?? /* @__PURE__ */ new Map();
|
|
1018
|
+
const props = require_view_validation.viewProps(view) ?? /* @__PURE__ */ new Map();
|
|
1090
1019
|
if (refs.length === 0) error("@db.compute needs at least one view field — a constant column is not supported", args[0].range);
|
|
1091
1020
|
const scope = viewHavingScope(view);
|
|
1092
|
-
if (scope) errors.push(...
|
|
1021
|
+
if (scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc, "@db.compute references the view's own fields — declare it on the view first (`field: Type.field`)"));
|
|
1093
1022
|
for (const ref of refs) {
|
|
1094
1023
|
if (ref.typeRef) continue;
|
|
1095
1024
|
const operand = props.get(ref.fieldRef.text);
|
|
@@ -1104,7 +1033,7 @@ function validateCompute(token, args, doc) {
|
|
|
1104
1033
|
return errors;
|
|
1105
1034
|
}
|
|
1106
1035
|
const dbComputeAnnotations = { compute: new _atscript_core.AnnotationSpec({
|
|
1107
|
-
description: "Declares a **computed view column** (since 0.1.147): closed arithmetic over the view's own fields — dimensions, aggregates, first-row-join fields and other computed fields — evaluated by the database, so the column sorts, filters and pages like any other.\n\nGrammar: `+ - * /`, unary `-`, parentheses, numeric literals, `coalesce(a, b, …)`. Every value is a double (`7 / 2 = 3.5`); a NULL operand yields NULL; division by zero yields NULL — so an expression with `/` or an optional operand needs an optional field
|
|
1036
|
+
description: (0, _atscript_core.annotationExample)("Declares a **computed view column** (since 0.1.147): closed arithmetic over the view's own fields — dimensions, aggregates, first-row-join fields and other computed fields — evaluated by the database, so the column sorts, filters and pages like any other.\n\nGrammar: `+ - * /`, unary `-`, parentheses, numeric literals, `coalesce(a, b, …)`. Every value is a double (`7 / 2 = 3.5`); a NULL operand yields NULL; division by zero yields NULL — so an expression with `/` or an optional operand needs an optional field.", "@db.agg.count 'id', `Issue.status = 'open'`", "openCount: Issue.id", "", "@db.compute `openCount * 10 + coalesce(oldestSeverity, 0)`", "rank: number"),
|
|
1108
1037
|
nodeType: ["prop"],
|
|
1109
1038
|
passedWhenReferred: false,
|
|
1110
1039
|
argument: {
|
|
@@ -1119,7 +1048,7 @@ const dbComputeAnnotations = { compute: new _atscript_core.AnnotationSpec({
|
|
|
1119
1048
|
//#region src/plugin/annotations/index-ann.ts
|
|
1120
1049
|
const dbIndexAnnotations = { index: {
|
|
1121
1050
|
plain: new _atscript_core.AnnotationSpec({
|
|
1122
|
-
description: "Standard (non-unique) index for query performance. Fields sharing the same index name form a composite index
|
|
1051
|
+
description: (0, _atscript_core.annotationExample)("Standard (non-unique) index for query performance. Fields sharing the same index name form a composite index.", "@db.index.plain \"idx_timeline\", \"desc\"", "createdAt: number.timestamp"),
|
|
1123
1052
|
nodeType: ["prop"],
|
|
1124
1053
|
passedWhenReferred: false,
|
|
1125
1054
|
multiple: true,
|
|
@@ -1138,7 +1067,7 @@ const dbIndexAnnotations = { index: {
|
|
|
1138
1067
|
}]
|
|
1139
1068
|
}),
|
|
1140
1069
|
unique: new _atscript_core.AnnotationSpec({
|
|
1141
|
-
description: "Unique index — ensures no two rows/documents have the same value(s). Fields sharing the same index name form a composite unique constraint
|
|
1070
|
+
description: (0, _atscript_core.annotationExample)("Unique index — ensures no two rows/documents have the same value(s). Fields sharing the same index name form a composite unique constraint.", "@db.index.unique \"tenant_email\"", "email: string.email"),
|
|
1142
1071
|
nodeType: ["prop"],
|
|
1143
1072
|
passedWhenReferred: false,
|
|
1144
1073
|
multiple: true,
|
|
@@ -1151,10 +1080,12 @@ const dbIndexAnnotations = { index: {
|
|
|
1151
1080
|
}
|
|
1152
1081
|
}),
|
|
1153
1082
|
fulltext: new _atscript_core.AnnotationSpec({
|
|
1154
|
-
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
|
|
1083
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.index.fulltext \"ft_content\"", "title: string", "", "@db.index.fulltext \"ft_content\", 5", "bio: string", "", "@db.index.fulltext \"ft_content\"", "@db.index.unique", "refNo: number.int"),
|
|
1155
1084
|
validate(token, args, doc) {
|
|
1156
1085
|
const field = token.parentNode;
|
|
1157
|
-
const
|
|
1086
|
+
const sealed = require_view_validation.sealedSearchProblem(token, doc, "@db.index.fulltext");
|
|
1087
|
+
if (sealed) return [sealed];
|
|
1088
|
+
const verdict = require_view_validation.searchFieldVerdict(field, doc);
|
|
1158
1089
|
if ("problem" in verdict) return [{
|
|
1159
1090
|
message: `@db.index.fulltext needs a string or an integer field — "${field.id}" ${verdict.problem}`,
|
|
1160
1091
|
severity: 1,
|
|
@@ -1162,7 +1093,7 @@ const dbIndexAnnotations = { index: {
|
|
|
1162
1093
|
}];
|
|
1163
1094
|
if (verdict.kind !== "integer") return [];
|
|
1164
1095
|
const errors = [];
|
|
1165
|
-
const missing =
|
|
1096
|
+
const missing = require_view_validation.integerMemberIndexProblem(token);
|
|
1166
1097
|
if (missing) errors.push({
|
|
1167
1098
|
message: missing,
|
|
1168
1099
|
severity: 1,
|
|
@@ -1192,7 +1123,7 @@ const dbIndexAnnotations = { index: {
|
|
|
1192
1123
|
}]
|
|
1193
1124
|
}),
|
|
1194
1125
|
geo: new _atscript_core.AnnotationSpec({
|
|
1195
|
-
description: "Geospatial index on a `db.geoPoint` field. Enables `geoSearch()` (distance-ranked) and accelerates `$geoWithin` filters
|
|
1126
|
+
description: (0, _atscript_core.annotationExample)("Geospatial index on a `db.geoPoint` field. Enables `geoSearch()` (distance-ranked) and accelerates `$geoWithin` filters.", "@db.index.geo", "geo: db.geoPoint"),
|
|
1196
1127
|
nodeType: ["prop"],
|
|
1197
1128
|
passedWhenReferred: false,
|
|
1198
1129
|
multiple: false,
|
|
@@ -1201,7 +1132,8 @@ const dbIndexAnnotations = { index: {
|
|
|
1201
1132
|
name: "name",
|
|
1202
1133
|
type: "string",
|
|
1203
1134
|
description: "Index name. Defaults to the field name."
|
|
1204
|
-
}
|
|
1135
|
+
},
|
|
1136
|
+
validate: require_view_validation.refuseCiphertext("@db.index.geo")
|
|
1205
1137
|
})
|
|
1206
1138
|
} };
|
|
1207
1139
|
//#endregion
|
|
@@ -1217,13 +1149,13 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
|
|
|
1217
1149
|
if (targetProp.countAnnotations("db.index.unique") > 0) return true;
|
|
1218
1150
|
const pk = [...targetStruct.props.entries()].filter(([, p]) => p.countAnnotations("meta.id") > 0).map(([name]) => name);
|
|
1219
1151
|
if (pk.length <= 1) return true;
|
|
1220
|
-
const hostStruct =
|
|
1221
|
-
const owner =
|
|
1152
|
+
const hostStruct = require_view_validation.getParentStruct(token);
|
|
1153
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
1222
1154
|
if (!hostStruct || owner === void 0 || !isDbTable(owner)) return true;
|
|
1223
1155
|
const covered = /* @__PURE__ */ new Set();
|
|
1224
1156
|
for (const [, sibling] of hostStruct.props) {
|
|
1225
1157
|
if (sibling.countAnnotations("db.rel.FK") === 0) continue;
|
|
1226
|
-
if (
|
|
1158
|
+
if (require_view_validation.getAnnotationAlias(sibling, "db.rel.FK") !== alias) continue;
|
|
1227
1159
|
const siblingDef = sibling.getDefinition();
|
|
1228
1160
|
if (siblingDef && (0, _atscript_core.isRef)(siblingDef) && siblingDef.id === refTypeName) {
|
|
1229
1161
|
if (siblingDef.chain.length === 1) covered.add(siblingDef.chain[0].text);
|
|
@@ -1233,7 +1165,7 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
|
|
|
1233
1165
|
}
|
|
1234
1166
|
const dbRelAnnotations = { rel: {
|
|
1235
1167
|
FK: new _atscript_core.AnnotationSpec({
|
|
1236
|
-
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
|
|
1168
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.rel.FK", "authorId: User.id", "", "// With alias (required when multiple FKs point to the same type)", "@db.rel.FK \"author\"", "authorId: User.id"),
|
|
1237
1169
|
nodeType: ["prop"],
|
|
1238
1170
|
passedWhenReferred: false,
|
|
1239
1171
|
argument: {
|
|
@@ -1299,7 +1231,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1299
1231
|
}
|
|
1300
1232
|
}
|
|
1301
1233
|
if (!alias) {
|
|
1302
|
-
const struct =
|
|
1234
|
+
const struct = require_view_validation.getParentStruct(token);
|
|
1303
1235
|
if (struct) {
|
|
1304
1236
|
let sameTargetCount = 0;
|
|
1305
1237
|
for (const [, prop] of struct.props) {
|
|
@@ -1309,7 +1241,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1309
1241
|
const r = def;
|
|
1310
1242
|
if (!r.hasChain) continue;
|
|
1311
1243
|
if (r.id === refTypeName) {
|
|
1312
|
-
if (!
|
|
1244
|
+
if (!require_view_validation.getAnnotationAlias(prop, "db.rel.FK")) sameTargetCount++;
|
|
1313
1245
|
}
|
|
1314
1246
|
}
|
|
1315
1247
|
if (sameTargetCount > 1) errors.push({
|
|
@@ -1323,7 +1255,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1323
1255
|
}
|
|
1324
1256
|
}),
|
|
1325
1257
|
to: new _atscript_core.AnnotationSpec({
|
|
1326
|
-
description: "Forward navigational property — the FK is on **this** interface. The compiler resolves the matching @db.rel.FK by target type or alias
|
|
1258
|
+
description: (0, _atscript_core.annotationExample)("Forward navigational property — the FK is on **this** interface. The compiler resolves the matching @db.rel.FK by target type or alias.", "@db.rel.to", "author?: User", "", "// With alias", "@db.rel.to \"author\"", "author?: User"),
|
|
1327
1259
|
nodeType: ["prop"],
|
|
1328
1260
|
passedWhenReferred: false,
|
|
1329
1261
|
argument: {
|
|
@@ -1336,7 +1268,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1336
1268
|
const errors = [];
|
|
1337
1269
|
const field = token.parentNode;
|
|
1338
1270
|
const alias = args[0]?.text;
|
|
1339
|
-
const owner =
|
|
1271
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
1340
1272
|
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
1341
1273
|
message: "@db.rel.to is only valid on fields of a @db.table interface",
|
|
1342
1274
|
severity: 1,
|
|
@@ -1347,7 +1279,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1347
1279
|
severity: 1,
|
|
1348
1280
|
range: token.range
|
|
1349
1281
|
});
|
|
1350
|
-
const targetTypeName =
|
|
1282
|
+
const targetTypeName = require_view_validation.getNavTargetTypeName(field);
|
|
1351
1283
|
if (!targetTypeName) return errors;
|
|
1352
1284
|
const unwound = doc.unwindType(targetTypeName);
|
|
1353
1285
|
if (unwound) {
|
|
@@ -1365,15 +1297,15 @@ const dbRelAnnotations = { rel: {
|
|
|
1365
1297
|
severity: 1,
|
|
1366
1298
|
range: token.range
|
|
1367
1299
|
});
|
|
1368
|
-
const struct =
|
|
1300
|
+
const struct = require_view_validation.getParentStruct(token);
|
|
1369
1301
|
if (struct) {
|
|
1370
1302
|
const fieldName = field.id;
|
|
1371
1303
|
for (const [name, prop] of struct.props) {
|
|
1372
1304
|
if (name === fieldName) continue;
|
|
1373
1305
|
if (prop.countAnnotations("db.rel.to") === 0) continue;
|
|
1374
|
-
const propAlias =
|
|
1306
|
+
const propAlias = require_view_validation.getAnnotationAlias(prop, "db.rel.to");
|
|
1375
1307
|
if ((alias || void 0) === (propAlias || void 0)) {
|
|
1376
|
-
if (
|
|
1308
|
+
if (require_view_validation.getNavTargetTypeName(prop) === targetTypeName) {
|
|
1377
1309
|
errors.push({
|
|
1378
1310
|
message: `Duplicate @db.rel.to '${alias || targetTypeName}' — only one forward navigational property per alias`,
|
|
1379
1311
|
severity: 1,
|
|
@@ -1384,13 +1316,13 @@ const dbRelAnnotations = { rel: {
|
|
|
1384
1316
|
}
|
|
1385
1317
|
}
|
|
1386
1318
|
if (alias) {
|
|
1387
|
-
if (
|
|
1319
|
+
if (require_view_validation.findFKFieldsPointingTo(doc, struct, targetTypeName, alias).length === 0) errors.push({
|
|
1388
1320
|
message: `No @db.rel.FK '${alias}' found on this interface`,
|
|
1389
1321
|
severity: 1,
|
|
1390
1322
|
range: token.range
|
|
1391
1323
|
});
|
|
1392
1324
|
} else {
|
|
1393
|
-
const matches =
|
|
1325
|
+
const matches = require_view_validation.findFKFieldsPointingTo(doc, struct, targetTypeName);
|
|
1394
1326
|
if (matches.length === 0) errors.push({
|
|
1395
1327
|
message: `No @db.rel.FK on this interface points to '${targetTypeName}' — did you mean @db.rel.from?`,
|
|
1396
1328
|
severity: 1,
|
|
@@ -1407,7 +1339,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1407
1339
|
}
|
|
1408
1340
|
}),
|
|
1409
1341
|
from: new _atscript_core.AnnotationSpec({
|
|
1410
|
-
description: "Inverse navigational property — the FK is on the **target** interface, pointing back to this one
|
|
1342
|
+
description: (0, _atscript_core.annotationExample)("Inverse navigational property — the FK is on the **target** interface, pointing back to this one.", "@db.rel.from", "posts: Post[]", "", "// With alias", "@db.rel.from \"original\"", "comments: Comment[]"),
|
|
1411
1343
|
nodeType: ["prop"],
|
|
1412
1344
|
passedWhenReferred: false,
|
|
1413
1345
|
argument: {
|
|
@@ -1420,7 +1352,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1420
1352
|
const errors = [];
|
|
1421
1353
|
const field = token.parentNode;
|
|
1422
1354
|
const alias = args[0]?.text;
|
|
1423
|
-
const owner =
|
|
1355
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
1424
1356
|
if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
|
|
1425
1357
|
message: "@db.rel.from is only valid on fields of a @db.table interface",
|
|
1426
1358
|
severity: 1,
|
|
@@ -1431,7 +1363,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1431
1363
|
severity: 1,
|
|
1432
1364
|
range: token.range
|
|
1433
1365
|
});
|
|
1434
|
-
const targetTypeName =
|
|
1366
|
+
const targetTypeName = require_view_validation.getNavTargetTypeName(field);
|
|
1435
1367
|
if (!targetTypeName) return errors;
|
|
1436
1368
|
const unwound = doc.unwindType(targetTypeName);
|
|
1437
1369
|
if (!unwound) return errors;
|
|
@@ -1445,15 +1377,15 @@ const dbRelAnnotations = { rel: {
|
|
|
1445
1377
|
});
|
|
1446
1378
|
return errors;
|
|
1447
1379
|
}
|
|
1448
|
-
const struct =
|
|
1380
|
+
const struct = require_view_validation.getParentStruct(token);
|
|
1449
1381
|
if (struct) {
|
|
1450
1382
|
const fieldName = field.id;
|
|
1451
1383
|
for (const [name, prop] of struct.props) {
|
|
1452
1384
|
if (name === fieldName) continue;
|
|
1453
1385
|
if (prop.countAnnotations("db.rel.from") === 0) continue;
|
|
1454
|
-
const propAlias =
|
|
1386
|
+
const propAlias = require_view_validation.getAnnotationAlias(prop, "db.rel.from");
|
|
1455
1387
|
if ((alias || void 0) === (propAlias || void 0)) {
|
|
1456
|
-
if (
|
|
1388
|
+
if (require_view_validation.getNavTargetTypeName(prop) === targetTypeName) {
|
|
1457
1389
|
errors.push({
|
|
1458
1390
|
message: `Duplicate @db.rel.from '${alias || targetTypeName}' — only one inverse navigational property per alias`,
|
|
1459
1391
|
severity: 1,
|
|
@@ -1464,9 +1396,9 @@ const dbRelAnnotations = { rel: {
|
|
|
1464
1396
|
}
|
|
1465
1397
|
}
|
|
1466
1398
|
}
|
|
1467
|
-
const thisTypeName =
|
|
1399
|
+
const thisTypeName = require_view_validation.getParentTypeName(token);
|
|
1468
1400
|
if (!thisTypeName) return errors;
|
|
1469
|
-
const matches =
|
|
1401
|
+
const matches = require_view_validation.findFKFieldsPointingTo(targetDoc, targetDef, thisTypeName, alias);
|
|
1470
1402
|
if (alias) {
|
|
1471
1403
|
if (matches.length === 0) errors.push({
|
|
1472
1404
|
message: `No @db.rel.FK '${alias}' found on '${targetTypeName}'`,
|
|
@@ -1493,10 +1425,10 @@ const dbRelAnnotations = { rel: {
|
|
|
1493
1425
|
return errors;
|
|
1494
1426
|
}
|
|
1495
1427
|
}),
|
|
1496
|
-
onDelete:
|
|
1497
|
-
onUpdate:
|
|
1428
|
+
onDelete: require_view_validation.refActionAnnotation("onDelete"),
|
|
1429
|
+
onUpdate: require_view_validation.refActionAnnotation("onUpdate"),
|
|
1498
1430
|
via: new _atscript_core.AnnotationSpec({
|
|
1499
|
-
description: "Declares a many-to-many navigational property through an explicit junction table. `@db.rel.via` is self-sufficient — no `@db.rel.from` pairing is needed
|
|
1431
|
+
description: (0, _atscript_core.annotationExample)("Declares a many-to-many navigational property through an explicit junction table. `@db.rel.via` is self-sufficient — no `@db.rel.from` pairing is needed.", "@db.rel.via PostTag", "tags: Tag[]"),
|
|
1500
1432
|
nodeType: ["prop"],
|
|
1501
1433
|
passedWhenReferred: false,
|
|
1502
1434
|
argument: {
|
|
@@ -1520,7 +1452,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1520
1452
|
});
|
|
1521
1453
|
if (!args[0]) return errors;
|
|
1522
1454
|
const junctionName = args[0].text;
|
|
1523
|
-
errors.push(...
|
|
1455
|
+
errors.push(...require_view_validation.validateRefArgument(args[0], doc, {
|
|
1524
1456
|
accept: isDbTable,
|
|
1525
1457
|
expected: "must have @db.table annotation."
|
|
1526
1458
|
}));
|
|
@@ -1529,10 +1461,10 @@ const dbRelAnnotations = { rel: {
|
|
|
1529
1461
|
if (!junctionUnwound) return errors;
|
|
1530
1462
|
const junctionDef = junctionUnwound.def;
|
|
1531
1463
|
if (!(0, _atscript_core.isInterface)(junctionDef)) return errors;
|
|
1532
|
-
const thisTypeName =
|
|
1533
|
-
const targetTypeName =
|
|
1464
|
+
const thisTypeName = require_view_validation.getParentTypeName(token);
|
|
1465
|
+
const targetTypeName = require_view_validation.getNavTargetTypeName(field);
|
|
1534
1466
|
if (!thisTypeName || !targetTypeName) return errors;
|
|
1535
|
-
const fksToThis =
|
|
1467
|
+
const fksToThis = require_view_validation.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, thisTypeName);
|
|
1536
1468
|
if (fksToThis.length === 0) errors.push({
|
|
1537
1469
|
message: `Junction '${junctionName}' has no @db.rel.FK pointing to '${thisTypeName}'`,
|
|
1538
1470
|
severity: 1,
|
|
@@ -1544,7 +1476,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1544
1476
|
range: args[0].range
|
|
1545
1477
|
});
|
|
1546
1478
|
if (targetTypeName !== thisTypeName) {
|
|
1547
|
-
const fksToTarget =
|
|
1479
|
+
const fksToTarget = require_view_validation.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, targetTypeName);
|
|
1548
1480
|
if (fksToTarget.length === 0) errors.push({
|
|
1549
1481
|
message: `Junction '${junctionName}' has no @db.rel.FK pointing to '${targetTypeName}'`,
|
|
1550
1482
|
severity: 1,
|
|
@@ -1560,7 +1492,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1560
1492
|
}
|
|
1561
1493
|
}),
|
|
1562
1494
|
filterable: new _atscript_core.AnnotationSpec({
|
|
1563
|
-
description: "Lets HTTP clients filter the parent rows by this relation's related rows — `ticket=$some(status=open)` / `ticket=$none(...)` in a moost-db query (`{ ticket: { $some: { status: 'open' } } }`). Off by default: a client predicate filters the PARENT set and runs a correlated subquery. Server-side code (`transformFilter`, `actionRowScope`, direct table calls) never needs it. The related table's own field rules (visibility, encryption, write-only) apply inside the predicate
|
|
1495
|
+
description: (0, _atscript_core.annotationExample)("Lets HTTP clients filter the parent rows by this relation's related rows — `ticket=$some(status=open)` / `ticket=$none(...)` in a moost-db query (`{ ticket: { $some: { status: 'open' } } }`). Off by default: a client predicate filters the PARENT set and runs a correlated subquery. Server-side code (`transformFilter`, `actionRowScope`, direct table calls) never needs it. The related table's own field rules (visibility, encryption, write-only) apply inside the predicate.", "@db.rel.to", "@db.rel.filterable", "ticket?: Ticket"),
|
|
1564
1496
|
nodeType: ["prop"],
|
|
1565
1497
|
passedWhenReferred: false,
|
|
1566
1498
|
validate(token) {
|
|
@@ -1575,7 +1507,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1575
1507
|
}
|
|
1576
1508
|
}),
|
|
1577
1509
|
filter: new _atscript_core.AnnotationSpec({
|
|
1578
|
-
description: "Applies a filter to a navigational property, restricting which related records are loaded (`$with`) and which count in relational predicates (`$some` / `$none`)
|
|
1510
|
+
description: (0, _atscript_core.annotationExample)("Applies a filter to a navigational property, restricting which related records are loaded (`$with`) and which count in relational predicates (`$some` / `$none`).", "@db.rel.from", "@db.rel.filter `Post.published = true`", "publishedPosts: Post[]"),
|
|
1579
1511
|
nodeType: ["prop"],
|
|
1580
1512
|
passedWhenReferred: false,
|
|
1581
1513
|
argument: {
|
|
@@ -1600,7 +1532,7 @@ const dbRelAnnotations = { rel: {
|
|
|
1600
1532
|
}
|
|
1601
1533
|
if (!args[0]?.queryNode) return errors;
|
|
1602
1534
|
const scope = relFilterScope(field);
|
|
1603
|
-
if (scope) errors.push(...
|
|
1535
|
+
if (scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc));
|
|
1604
1536
|
const expression = args[0].queryNode.expression;
|
|
1605
1537
|
forEachComparison(expression, (cmp) => {
|
|
1606
1538
|
if (cmp.right && "fieldRef" in cmp.right) errors.push({
|
|
@@ -1609,13 +1541,13 @@ const dbRelAnnotations = { rel: {
|
|
|
1609
1541
|
range: cmp.right.fieldRef.range
|
|
1610
1542
|
});
|
|
1611
1543
|
});
|
|
1612
|
-
const junction = hasVia ?
|
|
1544
|
+
const junction = hasVia ? require_view_validation.getAnnotationAlias(field, "db.rel.via") : void 0;
|
|
1613
1545
|
if (junction) {
|
|
1614
1546
|
const conjuncts = (0, _atscript_core.isQueryLogical)(expression) && expression.operator === "and" ? expression.operands : [expression];
|
|
1615
1547
|
for (const conjunct of conjuncts) {
|
|
1616
1548
|
let junctionRef;
|
|
1617
1549
|
let targetRef = false;
|
|
1618
|
-
|
|
1550
|
+
require_view_validation.forEachFieldRef(conjunct, (ref) => {
|
|
1619
1551
|
if (ref.typeRef?.text === junction) junctionRef ??= ref.typeRef.range;
|
|
1620
1552
|
else targetRef = true;
|
|
1621
1553
|
});
|
|
@@ -1640,8 +1572,9 @@ function forEachComparison(expr, fn) {
|
|
|
1640
1572
|
const dbSearchAnnotations = { search: {
|
|
1641
1573
|
vector: {
|
|
1642
1574
|
$self: new _atscript_core.AnnotationSpec({
|
|
1643
|
-
description: "Marks a field as a **vector embedding** for **similarity search**.\n\n- Each adapter maps this to its native vector type and index:\n - **MongoDB** → Atlas `$vectorSearch` index\n - **MySQL 9+** → `VECTOR(N)` column + `VEC_DISTANCE_*` functions\n - **PostgreSQL** → pgvector `vector(N)` column + distance operators\n - **SQLite** → `BLOB` column + `vec0` virtual index (requires the `sqlite-vec` extension; falls back to JSON `TEXT` when unavailable)
|
|
1575
|
+
description: (0, _atscript_core.annotationExample)("Marks a field as a **vector embedding** for **similarity search**.\n\n- Each adapter maps this to its native vector type and index:\n - **MongoDB** → Atlas `$vectorSearch` index\n - **MySQL 9+** → `VECTOR(N)` column + `VEC_DISTANCE_*` functions\n - **PostgreSQL** → pgvector `vector(N)` column + distance operators\n - **SQLite** → `BLOB` column + `vec0` virtual index (requires the `sqlite-vec` extension; falls back to JSON `TEXT` when unavailable)", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector"),
|
|
1644
1576
|
nodeType: ["prop"],
|
|
1577
|
+
validate: require_view_validation.refuseCiphertext("@db.search.vector"),
|
|
1645
1578
|
passedWhenReferred: false,
|
|
1646
1579
|
multiple: false,
|
|
1647
1580
|
argument: [
|
|
@@ -1685,7 +1618,7 @@ const dbSearchAnnotations = { search: {
|
|
|
1685
1618
|
]
|
|
1686
1619
|
}),
|
|
1687
1620
|
threshold: new _atscript_core.AnnotationSpec({
|
|
1688
|
-
description: "Sets a **default minimum similarity threshold** for vector search queries on this field.\n\n- Results with a similarity score below this threshold are excluded.\n- Query-time `$threshold` control overrides this default.\n- Value range: `0` to `1` (where `1` means exact match)
|
|
1621
|
+
description: (0, _atscript_core.annotationExample)("Sets a **default minimum similarity threshold** for vector search queries on this field.\n\n- Results with a similarity score below this threshold are excluded.\n- Query-time `$threshold` control overrides this default.\n- Value range: `0` to `1` (where `1` means exact match).", "@db.search.vector 1536, \"cosine\"", "@db.search.vector.threshold 0.7", "embedding: db.vector"),
|
|
1689
1622
|
nodeType: ["prop"],
|
|
1690
1623
|
passedWhenReferred: false,
|
|
1691
1624
|
multiple: false,
|
|
@@ -1698,8 +1631,9 @@ const dbSearchAnnotations = { search: {
|
|
|
1698
1631
|
})
|
|
1699
1632
|
},
|
|
1700
1633
|
filter: new _atscript_core.AnnotationSpec({
|
|
1701
|
-
description: "Assigns a field as a **pre-filter** for a **vector search index**.\n\n- Filters allow vector search queries to return results only within a specific subset.\n- The referenced index must be defined using `@db.search.vector
|
|
1634
|
+
description: (0, _atscript_core.annotationExample)("Assigns a field as a **pre-filter** for a **vector search index**.\n\n- Filters allow vector search queries to return results only within a specific subset.\n- The referenced index must be defined using `@db.search.vector`.", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector", "", "@db.search.filter \"embedding\"", "category: string"),
|
|
1702
1635
|
nodeType: ["prop"],
|
|
1636
|
+
validate: require_view_validation.refuseCiphertext("@db.search.filter"),
|
|
1703
1637
|
passedWhenReferred: false,
|
|
1704
1638
|
multiple: true,
|
|
1705
1639
|
argument: {
|
|
@@ -1717,7 +1651,7 @@ const dbSearchAnnotations = { search: {
|
|
|
1717
1651
|
* read sorts by it without a `$nulls` entry for it.
|
|
1718
1652
|
*/
|
|
1719
1653
|
const dbSortAnnotations = { sort: { nulls: new _atscript_core.AnnotationSpec({
|
|
1720
|
-
description: "Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional and `| null` fields can be NULL — on another required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead)
|
|
1654
|
+
description: (0, _atscript_core.annotationExample)("Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional and `| null` fields can be NULL — on another required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead).", "@db.sort.nulls 'last'", "closedAt?: number.timestamp"),
|
|
1721
1655
|
nodeType: ["prop"],
|
|
1722
1656
|
passedWhenReferred: false,
|
|
1723
1657
|
multiple: false,
|
|
@@ -1743,16 +1677,20 @@ const dbSortAnnotations = { sort: { nulls: new _atscript_core.AnnotationSpec({
|
|
|
1743
1677
|
fail("@db.sort.nulls on a @db.encrypted field — encrypted fields are not sortable");
|
|
1744
1678
|
return errors;
|
|
1745
1679
|
}
|
|
1680
|
+
if (require_view_validation.viewFieldSeals(field, doc).encrypted) {
|
|
1681
|
+
fail("@db.sort.nulls on a view field over a @db.encrypted source field — encrypted fields are not sortable");
|
|
1682
|
+
return errors;
|
|
1683
|
+
}
|
|
1746
1684
|
const def = field.getDefinition();
|
|
1747
1685
|
let leaf = def;
|
|
1748
1686
|
if (def && (0, _atscript_core.isRef)(def)) leaf = doc.unwindType(def.id, def.chain)?.def;
|
|
1749
|
-
const base =
|
|
1687
|
+
const base = require_view_validation.primitiveBaseType(leaf);
|
|
1750
1688
|
if (def && ((0, _atscript_core.isArray)(def) || (0, _atscript_core.isStructure)(def)) || leaf && ((0, _atscript_core.isArray)(leaf) || (0, _atscript_core.isStructure)(leaf) || (0, _atscript_core.isInterface)(leaf)) || leaf && (0, _atscript_core.isPrimitive)(leaf) && (base === "array" || base === "object")) {
|
|
1751
1689
|
fail("@db.sort.nulls needs a sortable scalar field — not an object or an array");
|
|
1752
1690
|
return errors;
|
|
1753
1691
|
}
|
|
1754
|
-
const owner =
|
|
1755
|
-
if (!field.has("optional") && !
|
|
1692
|
+
const owner = require_view_validation.getDbTableOwner(token);
|
|
1693
|
+
if (!field.has("optional") && !require_view_validation.isNullableUnion(def, doc) && owner && owner.countAnnotations("db.table") > 0 && field.ownerNode?.ownerNode === owner) fail("@db.sort.nulls on a required field has no effect — the field is never NULL; make it optional (field?: …) or remove the annotation", 2);
|
|
1756
1694
|
return errors;
|
|
1757
1695
|
}
|
|
1758
1696
|
}) } };
|
|
@@ -1763,7 +1701,7 @@ const dbTableAnnotations = {
|
|
|
1763
1701
|
filterable: tableCapability("filterable"),
|
|
1764
1702
|
sortable: tableCapability("sortable"),
|
|
1765
1703
|
$self: new _atscript_core.AnnotationSpec({
|
|
1766
|
-
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
|
|
1704
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.table \"users\"", "export interface User { ... }"),
|
|
1767
1705
|
nodeType: ["interface"],
|
|
1768
1706
|
passedWhenReferred: false,
|
|
1769
1707
|
argument: {
|
|
@@ -1775,7 +1713,7 @@ const dbTableAnnotations = {
|
|
|
1775
1713
|
validate(token, _args, _doc) {
|
|
1776
1714
|
const errors = [];
|
|
1777
1715
|
const owner = token.parentNode;
|
|
1778
|
-
if (
|
|
1716
|
+
if (require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
|
|
1779
1717
|
message: "An interface cannot be both a @db.table and a @db.view",
|
|
1780
1718
|
severity: 1,
|
|
1781
1719
|
range: token.range
|
|
@@ -1784,7 +1722,7 @@ const dbTableAnnotations = {
|
|
|
1784
1722
|
}
|
|
1785
1723
|
}),
|
|
1786
1724
|
renamed: new _atscript_core.AnnotationSpec({
|
|
1787
|
-
description: "Specifies the previous table name for table rename migration. The sync engine generates ALTER TABLE RENAME instead of drop+create
|
|
1725
|
+
description: (0, _atscript_core.annotationExample)("Specifies the previous table name for table rename migration. The sync engine generates ALTER TABLE RENAME instead of drop+create.", "@db.table \"app_users\"", "@db.table.renamed \"users\"", "export interface User { ... }"),
|
|
1788
1726
|
nodeType: ["interface"],
|
|
1789
1727
|
passedWhenReferred: false,
|
|
1790
1728
|
argument: {
|
|
@@ -1803,7 +1741,7 @@ const dbTableAnnotations = {
|
|
|
1803
1741
|
}
|
|
1804
1742
|
}),
|
|
1805
1743
|
preferredId: { uniqueIndex: new _atscript_core.AnnotationSpec({
|
|
1806
|
-
description: "Selects a unique index as the table's preferred row identifier. When the unique-index name is omitted, the first declared unique-index group wins
|
|
1744
|
+
description: (0, _atscript_core.annotationExample)("Selects a unique index as the table's preferred row identifier. When the unique-index name is omitted, the first declared unique-index group wins.", "@db.table", "@db.table.preferredId.uniqueIndex \"by_slug\"", "export interface Post {", " @db.index.unique \"by_slug\"", " slug: string", "}"),
|
|
1807
1745
|
nodeType: ["interface"],
|
|
1808
1746
|
passedWhenReferred: false,
|
|
1809
1747
|
multiple: false,
|
|
@@ -1816,7 +1754,7 @@ const dbTableAnnotations = {
|
|
|
1816
1754
|
validate(token, args, _doc) {
|
|
1817
1755
|
const errors = [];
|
|
1818
1756
|
const owner = token.parentNode;
|
|
1819
|
-
if (
|
|
1757
|
+
if (require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
|
|
1820
1758
|
message: "@db.table.preferredId.uniqueIndex is not supported on @db.view interfaces (views have no unique-index declarations).",
|
|
1821
1759
|
severity: 1,
|
|
1822
1760
|
range: token.range
|
|
@@ -1851,7 +1789,7 @@ const dbTableAnnotations = {
|
|
|
1851
1789
|
}) }
|
|
1852
1790
|
},
|
|
1853
1791
|
schema: new _atscript_core.AnnotationSpec({
|
|
1854
|
-
description: "Assigns the entity to a database schema/namespace
|
|
1792
|
+
description: (0, _atscript_core.annotationExample)("Assigns the entity to a database schema/namespace.", "@db.table \"users\"", "@db.schema \"auth\"", "export interface User { ... }"),
|
|
1855
1793
|
nodeType: ["interface"],
|
|
1856
1794
|
passedWhenReferred: false,
|
|
1857
1795
|
argument: {
|
|
@@ -1861,7 +1799,7 @@ const dbTableAnnotations = {
|
|
|
1861
1799
|
}
|
|
1862
1800
|
}),
|
|
1863
1801
|
space: new _atscript_core.AnnotationSpec({
|
|
1864
|
-
description: "Binds the entity to a named database space (a `DbSpace` — one per physical database) for apps running more than one database at once (e.g. MongoDB + PostgreSQL). Absent means the default space.\n\nConsumed by the generated model manifest (grouping in `modelsBySpace`) and by `@TableController(Model)` token binding, which resolves the space registered under this name via `provideDbSpace(space, name)
|
|
1802
|
+
description: (0, _atscript_core.annotationExample)("Binds the entity to a named database space (a `DbSpace` — one per physical database) for apps running more than one database at once (e.g. MongoDB + PostgreSQL). Absent means the default space.\n\nConsumed by the generated model manifest (grouping in `modelsBySpace`) and by `@TableController(Model)` token binding, which resolves the space registered under this name via `provideDbSpace(space, name)`.", "@db.table \"feed_runs\"", "@db.space \"analytics\"", "export interface FeedRun { ... }"),
|
|
1865
1803
|
nodeType: ["interface"],
|
|
1866
1804
|
passedWhenReferred: false,
|
|
1867
1805
|
argument: {
|
|
@@ -1871,7 +1809,7 @@ const dbTableAnnotations = {
|
|
|
1871
1809
|
}
|
|
1872
1810
|
}),
|
|
1873
1811
|
http: { path: new _atscript_core.AnnotationSpec({
|
|
1874
|
-
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)
|
|
1812
|
+
description: (0, _atscript_core.annotationExample)("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).", "@db.table \"authors\"", "@db.http.path \"/authors\"", "export interface Author { ... }"),
|
|
1875
1813
|
nodeType: ["interface"],
|
|
1876
1814
|
passedWhenReferred: false,
|
|
1877
1815
|
argument: {
|
|
@@ -1881,7 +1819,7 @@ const dbTableAnnotations = {
|
|
|
1881
1819
|
}
|
|
1882
1820
|
}) },
|
|
1883
1821
|
sync: { method: new _atscript_core.AnnotationSpec({
|
|
1884
|
-
description: "Controls how the sync engine handles structural changes that cannot be applied via ALTER TABLE.\n\n- `\"recreate\"` — lossless: create temp table, copy data, drop old, rename.\n- `\"drop\"` — lossy: drop table entirely and create from scratch.\n\nWithout this annotation, structural changes fail with an error requiring manual intervention
|
|
1822
|
+
description: (0, _atscript_core.annotationExample)("Controls how the sync engine handles structural changes that cannot be applied via ALTER TABLE.\n\n- `\"recreate\"` — lossless: create temp table, copy data, drop old, rename.\n- `\"drop\"` — lossy: drop table entirely and create from scratch.\n\nWithout this annotation, structural changes fail with an error requiring manual intervention.", "@db.sync.method \"drop\"", "interface Logs { ... }"),
|
|
1885
1823
|
nodeType: ["interface"],
|
|
1886
1824
|
passedWhenReferred: false,
|
|
1887
1825
|
argument: {
|
|
@@ -1892,7 +1830,7 @@ const dbTableAnnotations = {
|
|
|
1892
1830
|
}
|
|
1893
1831
|
}) },
|
|
1894
1832
|
depth: { limit: new _atscript_core.AnnotationSpec({
|
|
1895
|
-
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
|
|
1833
|
+
description: (0, _atscript_core.annotationExample)("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.", "@db.table \"authors\"", "@db.depth.limit 2", "export interface Author { ... }"),
|
|
1896
1834
|
nodeType: ["interface"],
|
|
1897
1835
|
passedWhenReferred: false,
|
|
1898
1836
|
multiple: false,
|
|
@@ -1946,19 +1884,21 @@ function collectUniqueIndexGroups(owner) {
|
|
|
1946
1884
|
return groups;
|
|
1947
1885
|
}
|
|
1948
1886
|
function tableCapability(capability) {
|
|
1949
|
-
const example = capability === "filterable" ?
|
|
1887
|
+
const example = capability === "filterable" ? [
|
|
1888
|
+
" @db.column.filterable",
|
|
1889
|
+
" email: string",
|
|
1890
|
+
" // other fields not filterable via the controller"
|
|
1891
|
+
] : [
|
|
1892
|
+
" @db.column.sortable",
|
|
1893
|
+
" createdAt: number.timestamp",
|
|
1894
|
+
" // other fields not sortable via the controller"
|
|
1895
|
+
];
|
|
1950
1896
|
const verb = capability === "filterable" ? "filter" : "sort";
|
|
1951
1897
|
return new _atscript_core.AnnotationSpec({
|
|
1952
|
-
description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column the adapter can ${verb} is ${capability} (JSON / array columns are never sortable, and not filterable on SQL adapters).
|
|
1898
|
+
description: (0, _atscript_core.annotationExample)(`Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column the adapter can ${verb} is ${capability} (JSON / array columns are never sortable, and not filterable on SQL adapters).
|
|
1953
1899
|
- **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
|
|
1954
1900
|
|
|
1955
|
-
\`/meta.fields[*].` + capability + "` advertises exactly what the gate accepts in either mode. The policy applies to " + (capability === "filterable" ? "filters only — `$groupBy`, `$having` keys and aggregate fields use the adapter's physical capability.\n\n" : "`$sort` only.\n\n") + `Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
|
|
1956
|
-
|
|
1957
|
-
**Example:**
|
|
1958
|
-
\`\`\`atscript
|
|
1959
|
-
@db.table "users"
|
|
1960
|
-
@db.table.${capability} "manual"\nexport interface User {
|
|
1961
|
-
` + example + "}\n```\n",
|
|
1901
|
+
\`/meta.fields[*].` + capability + "` advertises exactly what the gate accepts in either mode. The policy applies to " + (capability === "filterable" ? "filters only — `$groupBy`, `$having` keys and aggregate fields use the adapter's physical capability.\n\n" : "`$sort` only.\n\n") + `Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.`, "@db.table \"users\"", `@db.table.${capability} "manual"`, "export interface User {", ...example, "}"),
|
|
1962
1902
|
nodeType: ["interface"],
|
|
1963
1903
|
passedWhenReferred: false,
|
|
1964
1904
|
multiple: false,
|
|
@@ -1985,7 +1925,7 @@ function tableCapability(capability) {
|
|
|
1985
1925
|
const UNIT_HOST_TYPES = ["decimal", "number"];
|
|
1986
1926
|
const dbUnitAnnotations = { unit: {
|
|
1987
1927
|
$self: new _atscript_core.AnnotationSpec({
|
|
1988
|
-
description: "Hard-coded **unit of measure** for this quantity field. Use when a table stores all values for this field in a single, fixed unit.\n\nFor per-row units, use `@db.unit.ref 'fieldName'` instead.\n\nValid on `decimal` and `number` fields. Unlike `@db.amount.currency`, the unit code is free-form (`'kg'`, `'rpm'`, `'qps'`, …) — no ISO-style validation.\n\nAggregations on a unit-tagged field do not need grouping when the unit is a fixed literal — the constraint is satisfied schema-wide
|
|
1928
|
+
description: (0, _atscript_core.annotationExample)("Hard-coded **unit of measure** for this quantity field. Use when a table stores all values for this field in a single, fixed unit.\n\nFor per-row units, use `@db.unit.ref 'fieldName'` instead.\n\nValid on `decimal` and `number` fields. Unlike `@db.amount.currency`, the unit code is free-form (`'kg'`, `'rpm'`, `'qps'`, …) — no ISO-style validation.\n\nAggregations on a unit-tagged field do not need grouping when the unit is a fixed literal — the constraint is satisfied schema-wide.", "@db.unit 'kg'", "weight: decimal"),
|
|
1989
1929
|
nodeType: ["prop"],
|
|
1990
1930
|
multiple: false,
|
|
1991
1931
|
argument: {
|
|
@@ -1995,16 +1935,16 @@ const dbUnitAnnotations = { unit: {
|
|
|
1995
1935
|
},
|
|
1996
1936
|
validate(token, _args, doc) {
|
|
1997
1937
|
const errors = [];
|
|
1998
|
-
errors.push(...
|
|
1999
|
-
errors.push(...
|
|
1938
|
+
errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.unit", UNIT_HOST_TYPES));
|
|
1939
|
+
errors.push(...require_view_validation.validateExclusiveWith(token, "@db.unit", [{
|
|
2000
1940
|
key: "db.unit.ref",
|
|
2001
1941
|
displayName: "@db.unit.ref"
|
|
2002
|
-
}]));
|
|
1942
|
+
}], doc));
|
|
2003
1943
|
return errors;
|
|
2004
1944
|
}
|
|
2005
1945
|
}),
|
|
2006
1946
|
ref: new _atscript_core.AnnotationSpec({
|
|
2007
|
-
description: "Binds this quantity to a **sibling field** that holds its unit. Use when different rows may carry different units (e.g. mixed kg/lb measurements).\n\nThe referenced field must be a `string`.\n\nValid on `decimal` and `number` fields. Aggregating this field forces the referenced unit field into `$groupBy` at runtime — summing rows with different units is rejected
|
|
1947
|
+
description: (0, _atscript_core.annotationExample)("Binds this quantity to a **sibling field** that holds its unit. Use when different rows may carry different units (e.g. mixed kg/lb measurements).\n\nThe referenced field must be a `string`.\n\nValid on `decimal` and `number` fields. Aggregating this field forces the referenced unit field into `$groupBy` at runtime — summing rows with different units is rejected.", "unit: string", "@db.unit.ref 'unit'", "weight: decimal"),
|
|
2008
1948
|
nodeType: ["prop"],
|
|
2009
1949
|
passedWhenReferred: false,
|
|
2010
1950
|
multiple: false,
|
|
@@ -2015,15 +1955,15 @@ const dbUnitAnnotations = { unit: {
|
|
|
2015
1955
|
},
|
|
2016
1956
|
validate(token, args, doc) {
|
|
2017
1957
|
const errors = [];
|
|
2018
|
-
errors.push(...
|
|
2019
|
-
errors.push(...
|
|
1958
|
+
errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.unit.ref", UNIT_HOST_TYPES));
|
|
1959
|
+
errors.push(...require_view_validation.validateExclusiveWith(token, "@db.unit.ref", [{
|
|
2020
1960
|
key: "db.unit",
|
|
2021
1961
|
displayName: "@db.unit"
|
|
2022
1962
|
}, {
|
|
2023
1963
|
key: "db.amount.currency.ref",
|
|
2024
1964
|
displayName: "@db.amount.currency.ref"
|
|
2025
|
-
}]));
|
|
2026
|
-
errors.push(...
|
|
1965
|
+
}], doc));
|
|
1966
|
+
errors.push(...require_view_validation.validateSiblingStringField(token, args, doc, "@db.unit.ref"));
|
|
2027
1967
|
return errors;
|
|
2028
1968
|
}
|
|
2029
1969
|
})
|
|
@@ -2039,7 +1979,7 @@ const dbUnitAnnotations = { unit: {
|
|
|
2039
1979
|
*/
|
|
2040
1980
|
function validateJoinOrder(orderToken, scope, doc) {
|
|
2041
1981
|
const target = scope.unqualifiedTarget;
|
|
2042
|
-
const errors =
|
|
1982
|
+
const errors = require_view_validation.validateQueryScope(orderToken, scope, doc, `a first-row join orders by fields of its target '${target}'`);
|
|
2043
1983
|
const seen = /* @__PURE__ */ new Set();
|
|
2044
1984
|
for (const item of orderToken.orderNode.items) {
|
|
2045
1985
|
const { ref } = item;
|
|
@@ -2087,7 +2027,7 @@ function orderKeyProblem(doc, target, path) {
|
|
|
2087
2027
|
}
|
|
2088
2028
|
const dbViewAnnotations = { view: {
|
|
2089
2029
|
$self: new _atscript_core.AnnotationSpec({
|
|
2090
|
-
description: "Marks an interface as a **database view**. Optionally takes a view name argument
|
|
2030
|
+
description: (0, _atscript_core.annotationExample)("Marks an interface as a **database view**. Optionally takes a view name argument.", "@db.view \"active_premium_users\"", "@db.view.for User", "export interface ActivePremiumUser { ... }"),
|
|
2091
2031
|
nodeType: ["interface"],
|
|
2092
2032
|
passedWhenReferred: false,
|
|
2093
2033
|
argument: {
|
|
@@ -2107,14 +2047,14 @@ const dbViewAnnotations = { view: {
|
|
|
2107
2047
|
}
|
|
2108
2048
|
}),
|
|
2109
2049
|
for: new _atscript_core.AnnotationSpec({
|
|
2110
|
-
description: "Specifies the entry/primary source for a computed view — a `@db.table`, or another `@db.view` (views over views, since 0.1.141). Required for views that map fields via chain refs. A `@db.alias` type cannot be the entry
|
|
2050
|
+
description: (0, _atscript_core.annotationExample)("Specifies the entry/primary source for a computed view — a `@db.table`, or another `@db.view` (views over views, since 0.1.141). Required for views that map fields via chain refs. A `@db.alias` type cannot be the entry.", "@db.view.for Order", "@db.view.filter `Order.status = 'active'`", "export interface ActiveOrderDetails { ... }"),
|
|
2111
2051
|
nodeType: ["interface"],
|
|
2112
2052
|
passedWhenReferred: false,
|
|
2113
2053
|
argument: {
|
|
2114
2054
|
name: "entry",
|
|
2115
2055
|
type: "ref",
|
|
2116
2056
|
description: "The primary/entry table or view type (must have @db.table or @db.view).",
|
|
2117
|
-
refFilter:
|
|
2057
|
+
refFilter: require_view_validation.isDbSourceDecl
|
|
2118
2058
|
},
|
|
2119
2059
|
validate(token, args, doc) {
|
|
2120
2060
|
const errors = [];
|
|
@@ -2124,19 +2064,19 @@ const dbViewAnnotations = { view: {
|
|
|
2124
2064
|
severity: 1,
|
|
2125
2065
|
range: token.range
|
|
2126
2066
|
});
|
|
2127
|
-
if (args[0]) errors.push(...
|
|
2128
|
-
const cycle =
|
|
2067
|
+
if (args[0]) errors.push(...require_view_validation.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
|
|
2068
|
+
const cycle = require_view_validation.findViewCycle(owner, doc);
|
|
2129
2069
|
if (cycle) errors.push({
|
|
2130
2070
|
message: `View '${cycle[0]}' depends on itself: ${cycle.join(" → ")}`,
|
|
2131
2071
|
severity: 1,
|
|
2132
2072
|
range: args[0]?.range ?? token.range
|
|
2133
2073
|
});
|
|
2134
|
-
errors.push(...validateViewInterface(owner, doc));
|
|
2074
|
+
errors.push(...require_view_validation.validateViewInterface(owner, doc));
|
|
2135
2075
|
return errors;
|
|
2136
2076
|
}
|
|
2137
2077
|
}),
|
|
2138
2078
|
joins: new _atscript_core.AnnotationSpec({
|
|
2139
|
-
description: "Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins). The target is a `@db.table`, a `@db.view` (since 0.1.141), or a `@db.alias` type — the way to join one table twice or to self-join the entry table: every scope name (entry + joins) must be unique
|
|
2079
|
+
description: (0, _atscript_core.annotationExample)("Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins). The target is a `@db.table`, a `@db.view` (since 0.1.141), or a `@db.alias` type — the way to join one table twice or to self-join the entry table: every scope name (entry + joins) must be unique.", "@db.view.for Order", "@db.view.joins Customer, `Customer.id = Order.customerId`", "@db.view.joins Region, `Region.id = Customer.regionId`, 'left'", "export interface OrderRegion { ... }", "```", "", "**First-row join** (since 0.1.147): a 4th argument — an ordering of the target's fields (`` `raisedAt, id` ``, `` `severity desc` ``) — joins only the FIRST matching target row by that ordering (the target's primary key is appended as the final tie-break), so every field read through the join comes from that one row. NULL sorts first in `asc`.", "```atscript", "@db.view.joins OldestOpenIssue, `OldestOpenIssue.ticketId = Ticket.id and OldestOpenIssue.status = 'open'`, 'left', `raisedAt`"),
|
|
2140
2080
|
nodeType: ["interface"],
|
|
2141
2081
|
passedWhenReferred: false,
|
|
2142
2082
|
multiple: true,
|
|
@@ -2172,7 +2112,7 @@ const dbViewAnnotations = { view: {
|
|
|
2172
2112
|
validate(token, args, doc) {
|
|
2173
2113
|
const errors = [];
|
|
2174
2114
|
const owner = token.parentNode;
|
|
2175
|
-
if (!
|
|
2115
|
+
if (!require_view_validation.hasAnyViewAnnotation(owner) && !args[0]) {
|
|
2176
2116
|
errors.push({
|
|
2177
2117
|
message: "@db.view.joins is only valid on @db.view interfaces",
|
|
2178
2118
|
severity: 1,
|
|
@@ -2180,11 +2120,11 @@ const dbViewAnnotations = { view: {
|
|
|
2180
2120
|
});
|
|
2181
2121
|
return errors;
|
|
2182
2122
|
}
|
|
2183
|
-
if (args[0]) errors.push(...
|
|
2123
|
+
if (args[0]) errors.push(...require_view_validation.validateRefArgument(args[0], doc, {
|
|
2184
2124
|
accept: isJoinTarget,
|
|
2185
2125
|
expected: "must be a @db.table or a @db.view."
|
|
2186
2126
|
}));
|
|
2187
|
-
const entryTypeName =
|
|
2127
|
+
const entryTypeName = require_view_validation.getAnnotationAlias(owner, "db.view.for");
|
|
2188
2128
|
if (!entryTypeName) {
|
|
2189
2129
|
errors.push({
|
|
2190
2130
|
message: "@db.view.joins requires @db.view.for to identify the entry table",
|
|
@@ -2193,14 +2133,14 @@ const dbViewAnnotations = { view: {
|
|
|
2193
2133
|
});
|
|
2194
2134
|
return errors;
|
|
2195
2135
|
}
|
|
2196
|
-
const joins =
|
|
2136
|
+
const joins = require_view_validation.viewJoins(owner);
|
|
2197
2137
|
const join = joins.find((a) => a.token === token);
|
|
2198
|
-
const earlier = join ?
|
|
2138
|
+
const earlier = join ? require_view_validation.earlierJoinTargets(joins, join) : [];
|
|
2199
2139
|
if (args[0]) {
|
|
2200
2140
|
const target = args[0].text;
|
|
2201
2141
|
const aliasHint = (name) => {
|
|
2202
2142
|
const decl = doc.getDeclarationOwnerNode(target)?.node;
|
|
2203
|
-
return decl &&
|
|
2143
|
+
return decl && require_view_validation.isAliasDecl(decl) ? "" : ` — declare a @db.alias type (\`@db.alias ${name}\` + \`export type Other = ${name}\`) to join it under another name`;
|
|
2204
2144
|
};
|
|
2205
2145
|
if (target === entryTypeName) errors.push({
|
|
2206
2146
|
message: `@db.view.joins cannot join the entry table '${target}' directly${aliasHint(target)}`,
|
|
@@ -2214,14 +2154,14 @@ const dbViewAnnotations = { view: {
|
|
|
2214
2154
|
});
|
|
2215
2155
|
}
|
|
2216
2156
|
const scope = join && viewJoinScope(owner, join, earlier);
|
|
2217
|
-
if (args[1]?.queryNode && scope) errors.push(...
|
|
2157
|
+
if (args[1]?.queryNode && scope) errors.push(...require_view_validation.validateQueryScope(args[1], scope, doc, "a join may reference the entry table and joins declared before it"));
|
|
2218
2158
|
const order = join && args[3]?.orderNode ? joinOrderScope(join) : void 0;
|
|
2219
2159
|
if (order) errors.push(...validateJoinOrder(args[3], order, doc));
|
|
2220
2160
|
return errors;
|
|
2221
2161
|
}
|
|
2222
2162
|
}),
|
|
2223
2163
|
filter: new _atscript_core.AnnotationSpec({
|
|
2224
|
-
description: "WHERE clause for a view, filtering which rows are included
|
|
2164
|
+
description: (0, _atscript_core.annotationExample)("WHERE clause for a view, filtering which rows are included.", "@db.view.for User", "@db.view.filter `User.status = 'active' and User.age >= 18`", "export interface ActiveUser { ... }"),
|
|
2225
2165
|
nodeType: ["interface"],
|
|
2226
2166
|
passedWhenReferred: false,
|
|
2227
2167
|
argument: {
|
|
@@ -2233,7 +2173,7 @@ const dbViewAnnotations = { view: {
|
|
|
2233
2173
|
validate(token, args, doc) {
|
|
2234
2174
|
const errors = [];
|
|
2235
2175
|
const owner = token.parentNode;
|
|
2236
|
-
if (!
|
|
2176
|
+
if (!require_view_validation.hasAnyViewAnnotation(owner) && !args[0]) {
|
|
2237
2177
|
errors.push({
|
|
2238
2178
|
message: "@db.view.filter is only valid on @db.view interfaces",
|
|
2239
2179
|
severity: 1,
|
|
@@ -2251,18 +2191,18 @@ const dbViewAnnotations = { view: {
|
|
|
2251
2191
|
});
|
|
2252
2192
|
return errors;
|
|
2253
2193
|
}
|
|
2254
|
-
errors.push(...
|
|
2194
|
+
errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc));
|
|
2255
2195
|
return errors;
|
|
2256
2196
|
}
|
|
2257
2197
|
}),
|
|
2258
2198
|
materialized: new _atscript_core.AnnotationSpec({
|
|
2259
|
-
description: "Marks a view as materialized (precomputed, stored on disk). Supported by PostgreSQL, CockroachDB, Oracle, SQL Server (indexed views), Snowflake. MongoDB supports on-demand materialized views via $merge/$out. Not applicable to MySQL or SQLite
|
|
2199
|
+
description: (0, _atscript_core.annotationExample)("Marks a view as materialized (precomputed, stored on disk). Supported by PostgreSQL, CockroachDB, Oracle, SQL Server (indexed views), Snowflake. MongoDB supports on-demand materialized views via $merge/$out. Not applicable to MySQL or SQLite.", "@db.view.materialized", "@db.view.for User", "@db.view.filter `User.status = 'active'`", "export interface ActiveUsers { ... }"),
|
|
2260
2200
|
nodeType: ["interface"],
|
|
2261
2201
|
passedWhenReferred: false,
|
|
2262
2202
|
validate(token, _args, _doc) {
|
|
2263
2203
|
const errors = [];
|
|
2264
2204
|
const owner = token.parentNode;
|
|
2265
|
-
if (!
|
|
2205
|
+
if (!require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
|
|
2266
2206
|
message: "@db.view.materialized is only valid on @db.view interfaces",
|
|
2267
2207
|
severity: 1,
|
|
2268
2208
|
range: token.range
|
|
@@ -2271,7 +2211,7 @@ const dbViewAnnotations = { view: {
|
|
|
2271
2211
|
}
|
|
2272
2212
|
}),
|
|
2273
2213
|
renamed: new _atscript_core.AnnotationSpec({
|
|
2274
|
-
description: "Specifies the previous view name for view rename migration. The sync engine drops the old view and creates the new one
|
|
2214
|
+
description: (0, _atscript_core.annotationExample)("Specifies the previous view name for view rename migration. The sync engine drops the old view and creates the new one.", "@db.view \"active_premium_users\"", "@db.view.renamed \"active_users\"", "@db.view.for User", "export interface ActivePremiumUser { ... }"),
|
|
2275
2215
|
nodeType: ["interface"],
|
|
2276
2216
|
passedWhenReferred: false,
|
|
2277
2217
|
argument: {
|
|
@@ -2282,7 +2222,7 @@ const dbViewAnnotations = { view: {
|
|
|
2282
2222
|
validate(token, _args, _doc) {
|
|
2283
2223
|
const errors = [];
|
|
2284
2224
|
const owner = token.parentNode;
|
|
2285
|
-
if (!
|
|
2225
|
+
if (!require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
|
|
2286
2226
|
message: "@db.view.renamed requires @db.view on the same interface",
|
|
2287
2227
|
severity: 1,
|
|
2288
2228
|
range: token.range
|
|
@@ -2291,7 +2231,7 @@ const dbViewAnnotations = { view: {
|
|
|
2291
2231
|
}
|
|
2292
2232
|
}),
|
|
2293
2233
|
having: new _atscript_core.AnnotationSpec({
|
|
2294
|
-
description: "Post-aggregation filter (HAVING clause) for analytical views. References the view's own fields, unqualified (`totalRevenue > 100`) — aggregate fields by their alias, plain fields as GROUP BY dimensions; source-table refs are not in scope
|
|
2234
|
+
description: (0, _atscript_core.annotationExample)("Post-aggregation filter (HAVING clause) for analytical views. References the view's own fields, unqualified (`totalRevenue > 100`) — aggregate fields by their alias, plain fields as GROUP BY dimensions; source-table refs are not in scope.", "@db.view", "@db.view.for Order", "@db.view.having `totalRevenue > 100`", "export interface TopCategories { ... }"),
|
|
2295
2235
|
nodeType: ["interface"],
|
|
2296
2236
|
passedWhenReferred: false,
|
|
2297
2237
|
argument: {
|
|
@@ -2303,7 +2243,7 @@ const dbViewAnnotations = { view: {
|
|
|
2303
2243
|
validate(token, args, doc) {
|
|
2304
2244
|
const errors = [];
|
|
2305
2245
|
const owner = token.parentNode;
|
|
2306
|
-
if (!
|
|
2246
|
+
if (!require_view_validation.hasAnyViewAnnotation(owner)) {
|
|
2307
2247
|
errors.push({
|
|
2308
2248
|
message: "@db.view.having is only valid on @db.view interfaces",
|
|
2309
2249
|
severity: 1,
|
|
@@ -2312,7 +2252,7 @@ const dbViewAnnotations = { view: {
|
|
|
2312
2252
|
return errors;
|
|
2313
2253
|
}
|
|
2314
2254
|
const scope = viewHavingScope(owner);
|
|
2315
|
-
if (args[0]?.queryNode && scope) errors.push(...
|
|
2255
|
+
if (args[0]?.queryNode && scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc, "@db.view.having references the view's own fields unqualified"));
|
|
2316
2256
|
return errors;
|
|
2317
2257
|
}
|
|
2318
2258
|
})
|
|
@@ -2359,18 +2299,18 @@ const dbPlugin = (options) => ({
|
|
|
2359
2299
|
kind: "array",
|
|
2360
2300
|
of: "number"
|
|
2361
2301
|
},
|
|
2362
|
-
documentation: "Represents a **vector embedding** (array of numbers) for **similarity search**.\n\n- Equivalent to `number[]` but explicitly marks the field as a vector embedding.\n- Each adapter maps this to its native vector type:\n - **MongoDB** → BSON array\n - **MySQL 9+** → `VECTOR(N)`\n - **PostgreSQL** → pgvector `vector(N)`\n - **SQLite** → JSON
|
|
2302
|
+
documentation: (0, _atscript_core.annotationExample)("Represents a **vector embedding** (array of numbers) for **similarity search**.\n\n- Equivalent to `number[]` but explicitly marks the field as a vector embedding.\n- Each adapter maps this to its native vector type:\n - **MongoDB** → BSON array\n - **MySQL 9+** → `VECTOR(N)`\n - **PostgreSQL** → pgvector `vector(N)`\n - **SQLite** → JSON", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector")
|
|
2363
2303
|
},
|
|
2364
2304
|
geoPoint: {
|
|
2365
2305
|
type: {
|
|
2366
2306
|
kind: "array",
|
|
2367
2307
|
of: "number"
|
|
2368
2308
|
},
|
|
2369
|
-
documentation: "Represents a **geographic point** as a `[longitude, latitude]` tuple (GeoJSON coordinate order).\n\n- Equivalent to `number[]` of length 2, but explicitly marks the field as a geo point.\n- **Coordinate order is GeoJSON order: longitude first.**\n- Each adapter maps this to its native storage:\n - **MongoDB** → GeoJSON `{ type: 'Point', coordinates: [lng, lat] }`\n - **PostgreSQL** → PostGIS `geography(Point,4326)` (JSONB without PostGIS)\n - **MySQL** → `POINT SRID 4326`\n - **SQLite** → JSON `TEXT` (haversine-based search)
|
|
2309
|
+
documentation: (0, _atscript_core.annotationExample)("Represents a **geographic point** as a `[longitude, latitude]` tuple (GeoJSON coordinate order).\n\n- Equivalent to `number[]` of length 2, but explicitly marks the field as a geo point.\n- **Coordinate order is GeoJSON order: longitude first.**\n- Each adapter maps this to its native storage:\n - **MongoDB** → GeoJSON `{ type: 'Point', coordinates: [lng, lat] }`\n - **PostgreSQL** → PostGIS `geography(Point,4326)` (JSONB without PostGIS)\n - **MySQL** → `POINT SRID 4326`\n - **SQLite** → JSON `TEXT` (haversine-based search)", "@db.index.geo", "geo: db.geoPoint")
|
|
2370
2310
|
},
|
|
2371
2311
|
currencyCode: {
|
|
2372
2312
|
type: "string",
|
|
2373
|
-
documentation: "Represents a **currency code** — typically ISO 4217 (`'USD'`, `'EUR'`, `'JPY'`) but accepts any uppercase alphanumeric code 2–10 chars long, so crypto and custom codes (`'BTC'`, `'USDC'`, `'POINTS'`) fit too.\n\nPair with `@db.amount.currency.ref 'fieldName'` on a `decimal` field to bind the amount to its row-level currency. The validator checks that the ref target resolves to this type (or a plain `string`)
|
|
2313
|
+
documentation: (0, _atscript_core.annotationExample)("Represents a **currency code** — typically ISO 4217 (`'USD'`, `'EUR'`, `'JPY'`) but accepts any uppercase alphanumeric code 2–10 chars long, so crypto and custom codes (`'BTC'`, `'USDC'`, `'POINTS'`) fit too.\n\nPair with `@db.amount.currency.ref 'fieldName'` on a `decimal` field to bind the amount to its row-level currency. The validator checks that the ref target resolves to this type (or a plain `string`).", "currency: db.currencyCode", "@db.amount.currency.ref 'currency'", "amount: decimal"),
|
|
2374
2314
|
annotations: { "expect.pattern": [{
|
|
2375
2315
|
pattern: "^[A-Z0-9]{2,10}$",
|
|
2376
2316
|
message: "Invalid currency code (expected 2–10 uppercase letters or digits)"
|