@atscript/db 0.1.135 → 0.1.137
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-DKuf_v2L.d.cts → agg-CV7y8nC6.d.cts} +13 -3
- package/dist/{agg-BtlGeRfj.d.mts → agg-D5DHsAby.d.mts} +13 -3
- package/dist/agg.cjs +35 -3
- package/dist/agg.d.cts +2 -2
- package/dist/agg.d.mts +2 -2
- package/dist/agg.mjs +34 -1
- package/dist/aggregate-fns-CGBv3E8S.cjs +87 -0
- package/dist/aggregate-fns-CfsveE1w.mjs +58 -0
- package/dist/{buckets-GruoVxH5.d.mts → buckets-BFG2RYRW.d.mts} +662 -31
- package/dist/{buckets-D6PBXKRJ.d.cts → buckets-C-27xmtq.d.cts} +662 -31
- package/dist/column-diff-BmqvgBWw.d.cts +24 -0
- package/dist/{db-view-Doh8vPg3.mjs → column-diff-BwOA5101.mjs} +795 -125
- package/dist/{db-view-Dm7MpUGB.cjs → column-diff-CgxgFKzx.cjs} +873 -125
- package/dist/column-diff-DPkbZIVE.d.mts +24 -0
- package/dist/index.cjs +79 -55
- package/dist/index.d.cts +12 -10
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +30 -11
- package/dist/{nested-writer-wk1EFUNY.cjs → nested-writer-BZNCuqI6.cjs} +16 -2
- package/dist/{nested-writer-CqL24ojl.mjs → nested-writer-FWD5oOYh.mjs} +11 -3
- package/dist/plugin.cjs +217 -77
- package/dist/plugin.mjs +217 -78
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -20
- package/dist/rel.d.mts +2 -20
- package/dist/rel.mjs +2 -2
- package/dist/relation-helpers-D3Zu0Mta.d.mts +30 -0
- package/dist/relation-helpers-DxrvS6ar.d.cts +30 -0
- package/dist/{relation-loader-BD4xANQJ.cjs → relation-loader-6ZB_5KFq.cjs} +1 -1
- package/dist/{relation-loader-B68R1LET.mjs → relation-loader-CTFaZpVa.mjs} +1 -1
- package/dist/shared.cjs +5 -1
- package/dist/shared.d.cts +16 -3
- package/dist/shared.d.mts +16 -3
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +38 -321
- package/dist/sync.d.cts +29 -17
- package/dist/sync.d.mts +29 -17
- package/dist/sync.mjs +6 -289
- package/dist/{validation-utils-MWOP1Ts4.mjs → validation-utils-B4h-GW4d.mjs} +29 -7
- package/dist/{validation-utils-B7SXPkm7.cjs → validation-utils-Dg0hW6dn.cjs} +52 -6
- package/dist/{validator-CewfnGZj.d.mts → validator-Drb2N-YL.d.cts} +1 -1
- package/dist/{validator-CewfnGZj.d.cts → validator-Drb2N-YL.d.mts} +1 -1
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +1 -1
- package/dist/agg-CvXDGnKi.mjs +0 -61
- package/dist/agg-EcIbAFnJ.cjs +0 -78
- package/dist/db-space-9CH5neN7.d.mts +0 -439
- package/dist/db-space-BdtBNTeH.d.cts +0 -439
package/dist/plugin.cjs
CHANGED
|
@@ -32,8 +32,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
32
32
|
value: mod,
|
|
33
33
|
enumerable: true
|
|
34
34
|
}) : target, mod));
|
|
35
|
+
//#endregion
|
|
36
|
+
const require_aggregate_fns = require("./aggregate-fns-CGBv3E8S.cjs");
|
|
35
37
|
require("./consts-BzRfCcH2.cjs");
|
|
36
|
-
const require_validation_utils = require("./validation-utils-
|
|
38
|
+
const require_validation_utils = require("./validation-utils-Dg0hW6dn.cjs");
|
|
37
39
|
let node_path = require("node:path");
|
|
38
40
|
node_path = __toESM(node_path, 1);
|
|
39
41
|
let node_url = require("node:url");
|
|
@@ -130,67 +132,98 @@ async function generateModelManifest(options, output, format, repo) {
|
|
|
130
132
|
}
|
|
131
133
|
//#endregion
|
|
132
134
|
//#region src/plugin/annotations/agg.ts
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
135
|
+
/**
|
|
136
|
+
* A conditional aggregate that is NULL when no row matches (so its field
|
|
137
|
+
* must be optional): every NULL-when-empty function but `sum`, whose
|
|
138
|
+
* conditional form is `COALESCE(…, 0)`.
|
|
139
|
+
*/
|
|
140
|
+
function conditionalIsNullable(name) {
|
|
141
|
+
return name !== "sum" && require_aggregate_fns.NULL_WHEN_EMPTY_AGGREGATE_FNS.has(name);
|
|
142
|
+
}
|
|
143
|
+
/** The optional 2nd argument every `@db.agg.*` takes. */
|
|
144
|
+
const CONDITION_ARG = {
|
|
145
|
+
name: "condition",
|
|
146
|
+
type: "query",
|
|
147
|
+
optional: true,
|
|
148
|
+
description: "Row predicate of a conditional aggregate: only rows where it holds are aggregated (SQL `FN(CASE WHEN … THEN field END)`). May reference the entry table and every join; unqualified fields resolve to the entry table."
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The rules every `@db.agg.*` shares: `'*'` is `count`'s only, and a
|
|
152
|
+
* condition (2nd argument) must stay within the view's tables (entry + every
|
|
153
|
+
* join, like `@db.view.filter`) and — for the aggregates that are NULL when
|
|
154
|
+
* no row matches (avg / min / max) — sit on an optional field.
|
|
155
|
+
*/
|
|
156
|
+
function validateAggArgs(name, token, args, doc) {
|
|
157
|
+
const errors = [];
|
|
158
|
+
const annotation = `@db.agg.${name}`;
|
|
159
|
+
if (args[0]?.text === "*" && name !== "count") errors.push({
|
|
160
|
+
message: `${annotation} needs a field — only @db.agg.count accepts '*'`,
|
|
161
|
+
severity: 1,
|
|
162
|
+
range: args[0].range
|
|
163
|
+
});
|
|
164
|
+
const condition = args[1];
|
|
165
|
+
if (!condition?.queryNode) return errors;
|
|
166
|
+
const owner = require_validation_utils.getDbTableOwner(token);
|
|
167
|
+
const entryTypeName = owner ? require_validation_utils.getAnnotationAlias(owner, "db.view.for") : void 0;
|
|
168
|
+
if (!owner || !entryTypeName) {
|
|
169
|
+
errors.push({
|
|
170
|
+
message: `A conditional ${annotation} requires @db.view.for on the view`,
|
|
171
|
+
severity: 1,
|
|
172
|
+
range: condition.range
|
|
173
|
+
});
|
|
174
|
+
return errors;
|
|
175
|
+
}
|
|
176
|
+
errors.push(...require_validation_utils.validateQueryScope(condition, require_validation_utils.viewScopeTypes(owner), entryTypeName, doc));
|
|
177
|
+
const prop = token.parentNode;
|
|
178
|
+
if (conditionalIsNullable(name) && prop && !prop.has("optional")) {
|
|
179
|
+
const field = prop.id ?? "field";
|
|
180
|
+
errors.push({
|
|
181
|
+
message: `Field "${field}" has a conditional ${annotation} and must be optional (${field}?: …) — it is NULL when no row matches`,
|
|
182
|
+
severity: 1,
|
|
183
|
+
range: token.range
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return errors;
|
|
187
|
+
}
|
|
188
|
+
const CONDITIONAL = " An optional 2nd argument (a query) makes it conditional: only rows where it holds are aggregated.";
|
|
189
|
+
/** What a conditional `name` yields when no row matches (from `NULL_WHEN_EMPTY_AGGREGATE_FNS`). */
|
|
190
|
+
function conditionalNote(name) {
|
|
191
|
+
if (!require_aggregate_fns.NULL_WHEN_EMPTY_AGGREGATE_FNS.has(name)) return "";
|
|
192
|
+
const fn = name.toUpperCase();
|
|
193
|
+
return conditionalIsNullable(name) ? ` A conditional ${fn} is NULL when no row matches, so its field must be optional.` : ` A conditional ${fn} is 0 (not NULL) when no row matches.`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* One `@db.agg.<name>` spec: `[field, condition?]` (field optional for count
|
|
197
|
+
* only). The description gets the conditional-form sentences appended.
|
|
198
|
+
*/
|
|
199
|
+
function aggSpec(name, description, field, types, extra = "") {
|
|
200
|
+
return new _atscript_core.AnnotationSpec({
|
|
201
|
+
description: description + CONDITIONAL + conditionalNote(name) + extra,
|
|
186
202
|
nodeType: ["prop"],
|
|
187
203
|
passedWhenReferred: false,
|
|
188
|
-
argument: {
|
|
204
|
+
argument: [{
|
|
189
205
|
name: "field",
|
|
190
206
|
type: "string",
|
|
191
|
-
|
|
207
|
+
optional: field.optional,
|
|
208
|
+
description: field.description
|
|
209
|
+
}, CONDITION_ARG],
|
|
210
|
+
validate(token, args, doc) {
|
|
211
|
+
const errors = validateAggArgs(name, token, args, doc);
|
|
212
|
+
if (types) errors.push(...require_validation_utils.validateFieldBaseType(token, doc, `@db.agg.${name}`, types));
|
|
213
|
+
return errors;
|
|
192
214
|
}
|
|
193
|
-
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const dbAggAnnotations = { agg: {
|
|
218
|
+
sum: aggSpec("sum", "Declares a view field as SUM of a source column.", { description: "Source column name to sum." }, ["number", "decimal"]),
|
|
219
|
+
avg: aggSpec("avg", "Declares a view field as AVG of a source column.", { description: "Source column name to average." }, ["number", "decimal"]),
|
|
220
|
+
count: aggSpec("count", "Declares a view field as COUNT. Without argument (or with `'*'`): COUNT(*). With field name argument: COUNT(field) (non-null count).", {
|
|
221
|
+
optional: true,
|
|
222
|
+
description: "Source column name to count non-null values. Omit (or `'*'`) for COUNT(*)."
|
|
223
|
+
}, ["number"], " A conditional COUNT(*) is spelled `@db.agg.count '*', <query>`."),
|
|
224
|
+
countDistinct: aggSpec("countDistinct", "Declares a view field as COUNT(DISTINCT field): the number of distinct non-null values of a source column.", { description: "Source column name whose distinct non-null values are counted." }, ["number"]),
|
|
225
|
+
min: aggSpec("min", "Declares a view field as MIN of a source column.", { description: "Source column name." }),
|
|
226
|
+
max: aggSpec("max", "Declares a view field as MAX of a source column.", { description: "Source column name." })
|
|
194
227
|
} };
|
|
195
228
|
//#endregion
|
|
196
229
|
//#region src/plugin/annotations/amount.ts
|
|
@@ -280,7 +313,7 @@ const dbColumnAnnotations = {
|
|
|
280
313
|
}) },
|
|
281
314
|
column: {
|
|
282
315
|
$self: new _atscript_core.AnnotationSpec({
|
|
283
|
-
description: "Overrides the physical column name in the database. For nested (flattened) fields, the parent prefix is still prepended automatically.\n\n**Example:**\n```atscript\n@db.column \"first_name\"\nfirstName: string\n// → physical column: first_name\n\n// Nested:\naddress: {\n @db.column \"zip_code\"\n zip: string\n}\n// → physical column: address__zip_code\n```\n",
|
|
316
|
+
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.\n\n**Example:**\n```atscript\n@db.column \"first_name\"\nfirstName: string\n// → physical column: first_name\n\n// Nested:\naddress: {\n @db.column \"zip_code\"\n zip: string\n}\n// → physical column: address__zip_code\n```\n",
|
|
284
317
|
nodeType: ["prop"],
|
|
285
318
|
passedWhenReferred: false,
|
|
286
319
|
argument: {
|
|
@@ -290,7 +323,7 @@ const dbColumnAnnotations = {
|
|
|
290
323
|
}
|
|
291
324
|
}),
|
|
292
325
|
renamed: new _atscript_core.AnnotationSpec({
|
|
293
|
-
description: "Specifies the previous local field name for column rename migration. The sync engine generates ALTER TABLE RENAME COLUMN instead of drop+add.\n\n**Example:**\n```atscript\n@db.column.renamed \"zip\"\npostalCode: string\n// Renames address__zip → address__postalCode\n```\n",
|
|
326
|
+
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).\n\n**Example:**\n```atscript\n@db.column.renamed \"zip\"\npostalCode: string\n// Renames address__zip → address__postalCode\n```\n",
|
|
294
327
|
nodeType: ["prop"],
|
|
295
328
|
passedWhenReferred: false,
|
|
296
329
|
argument: {
|
|
@@ -1296,6 +1329,86 @@ const dbUnitAnnotations = { unit: {
|
|
|
1296
1329
|
})
|
|
1297
1330
|
} };
|
|
1298
1331
|
//#endregion
|
|
1332
|
+
//#region src/shared/view-validation.ts
|
|
1333
|
+
/** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
|
|
1334
|
+
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}`);
|
|
1335
|
+
/** Primitive leaf types a JSON-stored path may end at. */
|
|
1336
|
+
const JSON_LEAF_TYPES = new Set([
|
|
1337
|
+
"string",
|
|
1338
|
+
"number",
|
|
1339
|
+
"boolean"
|
|
1340
|
+
]);
|
|
1341
|
+
/** Props of a view interface (its own structure; views don't use `extends`). */
|
|
1342
|
+
function viewProps(owner) {
|
|
1343
|
+
if ((0, _atscript_core.isInterface)(owner)) return owner.props;
|
|
1344
|
+
const def = owner.getDefinition();
|
|
1345
|
+
return def && (0, _atscript_core.isStructure)(def) ? def.props : void 0;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* VW8: a chain ref that passes through a `@db.json` or array node reads a
|
|
1349
|
+
* JSON leaf, which views can only extract as a string / number / boolean.
|
|
1350
|
+
* Intermediate nodes that `unwindType` can't reach are skipped (sync-time
|
|
1351
|
+
* resolution reports them).
|
|
1352
|
+
*/
|
|
1353
|
+
function validateJsonChain(fieldName, ref, doc, range) {
|
|
1354
|
+
const typeName = ref.id;
|
|
1355
|
+
const chain = ref.chain.map((t) => t.text);
|
|
1356
|
+
if (!typeName || chain.length < 2) return [];
|
|
1357
|
+
let throughJson = false;
|
|
1358
|
+
for (let i = 1; i < chain.length; i++) {
|
|
1359
|
+
const step = doc.unwindType(typeName, chain.slice(0, i));
|
|
1360
|
+
if (!step) return [];
|
|
1361
|
+
const stepNode = step.node;
|
|
1362
|
+
if (stepNode && (0, _atscript_core.isProp)(stepNode) && stepNode.countAnnotations("db.json") > 0 || (0, _atscript_core.isArray)(step.def)) {
|
|
1363
|
+
throughJson = true;
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!throughJson) return [];
|
|
1368
|
+
const leaf = doc.unwindType(typeName, chain)?.def;
|
|
1369
|
+
const leafType = require_validation_utils.primitiveBaseType(leaf);
|
|
1370
|
+
if (leafType !== void 0 && JSON_LEAF_TYPES.has(leafType)) return [];
|
|
1371
|
+
return [{
|
|
1372
|
+
message: `Field "${fieldName}" reads "${typeName}.${chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
|
|
1373
|
+
severity: 1,
|
|
1374
|
+
range
|
|
1375
|
+
}];
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Whole-view checks, run once per `@db.view.for` interface:
|
|
1379
|
+
*
|
|
1380
|
+
* - VW7 — a field reading from a left-joined table must be optional (the
|
|
1381
|
+
* join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
|
|
1382
|
+
* fields are exempt (they count 0, never NULL).
|
|
1383
|
+
* - VW8 — a chain ref through a `@db.json` or array node must end at a
|
|
1384
|
+
* primitive string / number / boolean leaf.
|
|
1385
|
+
* @since 0.1.136
|
|
1386
|
+
*/
|
|
1387
|
+
function validateViewInterface(owner, doc) {
|
|
1388
|
+
const errors = [];
|
|
1389
|
+
const props = viewProps(owner);
|
|
1390
|
+
if (!props) return errors;
|
|
1391
|
+
const leftJoined = /* @__PURE__ */ new Set();
|
|
1392
|
+
for (const join of require_validation_utils.viewJoins(owner)) {
|
|
1393
|
+
const target = join.args[0]?.text;
|
|
1394
|
+
if (target && join.args[2]?.text === "left") leftJoined.add(target);
|
|
1395
|
+
}
|
|
1396
|
+
for (const [fieldName, prop] of props) {
|
|
1397
|
+
const def = prop.getDefinition();
|
|
1398
|
+
if (!def || !(0, _atscript_core.isRef)(def)) continue;
|
|
1399
|
+
const ref = def;
|
|
1400
|
+
const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
|
|
1401
|
+
if (!range) continue;
|
|
1402
|
+
if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
|
|
1403
|
+
message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
|
|
1404
|
+
severity: 1,
|
|
1405
|
+
range
|
|
1406
|
+
});
|
|
1407
|
+
errors.push(...validateJsonChain(fieldName, ref, doc, range));
|
|
1408
|
+
}
|
|
1409
|
+
return errors;
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1299
1412
|
//#region src/plugin/annotations/view.ts
|
|
1300
1413
|
const dbViewAnnotations = { view: {
|
|
1301
1414
|
$self: new _atscript_core.AnnotationSpec({
|
|
@@ -1327,29 +1440,41 @@ const dbViewAnnotations = { view: {
|
|
|
1327
1440
|
},
|
|
1328
1441
|
validate(token, args, doc) {
|
|
1329
1442
|
const errors = [];
|
|
1330
|
-
|
|
1443
|
+
const owner = token.parentNode;
|
|
1444
|
+
if (owner.countAnnotations("db.table") > 0) errors.push({
|
|
1331
1445
|
message: "An interface cannot be both a @db.table and a @db.view",
|
|
1332
1446
|
severity: 1,
|
|
1333
1447
|
range: token.range
|
|
1334
1448
|
});
|
|
1335
1449
|
if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, { requireDbTable: true }));
|
|
1450
|
+
errors.push(...validateViewInterface(owner, doc));
|
|
1336
1451
|
return errors;
|
|
1337
1452
|
}
|
|
1338
1453
|
}),
|
|
1339
1454
|
joins: new _atscript_core.AnnotationSpec({
|
|
1340
|
-
description: "Declares an explicit join for a view.
|
|
1455
|
+
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); a table can be joined once (no aliases / self-joins).\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.joins Customer, `Customer.id = Order.customerId`\n@db.view.joins Region, `Region.id = Customer.regionId`, 'left'\nexport interface OrderRegion { ... }\n```\n",
|
|
1341
1456
|
nodeType: ["interface"],
|
|
1342
1457
|
multiple: true,
|
|
1343
1458
|
mergeStrategy: "append",
|
|
1344
|
-
argument: [
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1459
|
+
argument: [
|
|
1460
|
+
{
|
|
1461
|
+
name: "target",
|
|
1462
|
+
type: "ref",
|
|
1463
|
+
description: "The table type to join (must have @db.table)."
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
name: "condition",
|
|
1467
|
+
type: "query",
|
|
1468
|
+
description: "Join condition expression."
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
name: "kind",
|
|
1472
|
+
type: "string",
|
|
1473
|
+
optional: true,
|
|
1474
|
+
description: "`\"inner\"` (default) drops entry rows without a match; `\"left\"` keeps them with NULLs.",
|
|
1475
|
+
values: ["inner", "left"]
|
|
1476
|
+
}
|
|
1477
|
+
],
|
|
1353
1478
|
validate(token, args, doc) {
|
|
1354
1479
|
const errors = [];
|
|
1355
1480
|
const owner = token.parentNode;
|
|
@@ -1371,9 +1496,29 @@ const dbViewAnnotations = { view: {
|
|
|
1371
1496
|
});
|
|
1372
1497
|
return errors;
|
|
1373
1498
|
}
|
|
1499
|
+
const allJoins = require_validation_utils.viewJoins(owner);
|
|
1500
|
+
const position = allJoins.findIndex((a) => a.token === token);
|
|
1501
|
+
const earlier = require_validation_utils.joinTargets(position === -1 ? [] : allJoins.slice(0, position));
|
|
1502
|
+
if (args[0]) {
|
|
1503
|
+
const target = args[0].text;
|
|
1504
|
+
if (target === entryTypeName) errors.push({
|
|
1505
|
+
message: `@db.view.joins cannot join the entry table '${target}' — no join aliases / self-joins yet`,
|
|
1506
|
+
severity: 1,
|
|
1507
|
+
range: args[0].range
|
|
1508
|
+
});
|
|
1509
|
+
else if (earlier.includes(target)) errors.push({
|
|
1510
|
+
message: `'${target}' is joined more than once — no join aliases / self-joins yet`,
|
|
1511
|
+
severity: 1,
|
|
1512
|
+
range: args[0].range
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1374
1515
|
if (args[1]?.queryNode && args[0]) {
|
|
1375
1516
|
const joinTargetName = args[0].text;
|
|
1376
|
-
errors.push(...require_validation_utils.validateQueryScope(args[1], [
|
|
1517
|
+
errors.push(...require_validation_utils.validateQueryScope(args[1], [
|
|
1518
|
+
joinTargetName,
|
|
1519
|
+
entryTypeName,
|
|
1520
|
+
...earlier
|
|
1521
|
+
], entryTypeName, doc, "a join may reference the entry table and joins declared before it"));
|
|
1377
1522
|
}
|
|
1378
1523
|
return errors;
|
|
1379
1524
|
}
|
|
@@ -1407,12 +1552,7 @@ const dbViewAnnotations = { view: {
|
|
|
1407
1552
|
});
|
|
1408
1553
|
return errors;
|
|
1409
1554
|
}
|
|
1410
|
-
|
|
1411
|
-
const joinsAnnotations = owner.annotations?.filter((a) => a.name === "db.view.joins");
|
|
1412
|
-
if (joinsAnnotations) {
|
|
1413
|
-
for (const join of joinsAnnotations) if (join.args[0]) allowedTypes.push(join.args[0].text);
|
|
1414
|
-
}
|
|
1415
|
-
errors.push(...require_validation_utils.validateQueryScope(args[0], allowedTypes, entryTypeName, doc));
|
|
1555
|
+
errors.push(...require_validation_utils.validateQueryScope(args[0], require_validation_utils.viewScopeTypes(owner), entryTypeName, doc));
|
|
1416
1556
|
return errors;
|
|
1417
1557
|
}
|
|
1418
1558
|
}),
|