@atscript/db 0.1.38
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/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/agg-BJFJ3dFQ.mjs +8 -0
- package/dist/agg-DnUWAOK8.cjs +14 -0
- package/dist/agg.cjs +3 -0
- package/dist/agg.d.ts +13 -0
- package/dist/agg.mjs +3 -0
- package/dist/chunk-CrpGerW8.cjs +31 -0
- package/dist/control_as-BFPERAF_.cjs +28 -0
- package/dist/control_as-bjmwe24C.mjs +26 -0
- package/dist/index.cjs +2887 -0
- package/dist/index.d.ts +1706 -0
- package/dist/index.mjs +2846 -0
- package/dist/logger-B7oxCfLQ.mjs +12 -0
- package/dist/logger-Dt2v_-wb.cjs +18 -0
- package/dist/nested-writer-BkqL7cp3.cjs +667 -0
- package/dist/nested-writer-NEN51mnR.mjs +576 -0
- package/dist/plugin.cjs +993 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.mjs +989 -0
- package/dist/rel.cjs +20 -0
- package/dist/rel.d.ts +1305 -0
- package/dist/rel.mjs +5 -0
- package/dist/relation-helpers-DyBIlQnB.mjs +29 -0
- package/dist/relation-helpers-guFL_oRf.cjs +47 -0
- package/dist/relation-loader-CpnDRf9k.cjs +415 -0
- package/dist/relation-loader-D4mTw6yH.cjs +4 -0
- package/dist/relation-loader-Dv7qXYq7.mjs +409 -0
- package/dist/relation-loader-Ggy1ujwR.mjs +4 -0
- package/dist/shared.cjs +13 -0
- package/dist/shared.d.ts +70 -0
- package/dist/shared.mjs +3 -0
- package/dist/sync.cjs +1205 -0
- package/dist/sync.d.ts +1878 -0
- package/dist/sync.mjs +1186 -0
- package/dist/validation-utils-DEoCMmEb.cjs +304 -0
- package/dist/validation-utils-DhR_mtKa.mjs +237 -0
- package/package.json +81 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const require_chunk = require('./chunk-CrpGerW8.cjs');
|
|
3
|
+
const __atscript_core = require_chunk.__toESM(require("@atscript/core"));
|
|
4
|
+
|
|
5
|
+
//#region packages/db/src/shared/annotation-utils.ts
|
|
6
|
+
function getDbTableOwner(token) {
|
|
7
|
+
const field = token.parentNode;
|
|
8
|
+
const struct = field.ownerNode;
|
|
9
|
+
if (!struct || !(0, __atscript_core.isStructure)(struct)) return undefined;
|
|
10
|
+
const iface = struct.ownerNode;
|
|
11
|
+
return iface && (0, __atscript_core.isInterface)(iface) ? iface : struct;
|
|
12
|
+
}
|
|
13
|
+
function getParentStruct(token) {
|
|
14
|
+
const field = token.parentNode;
|
|
15
|
+
const struct = field.ownerNode;
|
|
16
|
+
return struct && (0, __atscript_core.isStructure)(struct) ? struct : undefined;
|
|
17
|
+
}
|
|
18
|
+
function getParentTypeName(token) {
|
|
19
|
+
const struct = getParentStruct(token);
|
|
20
|
+
if (!struct) return undefined;
|
|
21
|
+
const iface = struct.ownerNode;
|
|
22
|
+
return iface && (0, __atscript_core.isInterface)(iface) ? iface.id : struct.id;
|
|
23
|
+
}
|
|
24
|
+
function validateFieldBaseType(token, doc, annotationName, expectedType) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const field = token.parentNode;
|
|
27
|
+
const definition = field.getDefinition();
|
|
28
|
+
if (!definition || !(0, __atscript_core.isRef)(definition)) return errors;
|
|
29
|
+
const unwound = doc.unwindType(definition.id, definition.chain);
|
|
30
|
+
if (!unwound || !(0, __atscript_core.isPrimitive)(unwound.def)) return errors;
|
|
31
|
+
const ct = unwound.def.config.type;
|
|
32
|
+
const baseType = typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
|
|
33
|
+
const allowed = Array.isArray(expectedType) ? expectedType : [expectedType];
|
|
34
|
+
if (!allowed.includes(baseType)) errors.push({
|
|
35
|
+
message: `${annotationName} is not compatible with type "${baseType}" — requires ${allowed.join(" or ")}`,
|
|
36
|
+
severity: 1,
|
|
37
|
+
range: token.range
|
|
38
|
+
});
|
|
39
|
+
return errors;
|
|
40
|
+
}
|
|
41
|
+
function getNavTargetTypeName(field) {
|
|
42
|
+
let def = field.getDefinition();
|
|
43
|
+
if ((0, __atscript_core.isArray)(def)) def = def?.getDefinition();
|
|
44
|
+
if ((0, __atscript_core.isRef)(def)) return def.id;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function getAnnotationAlias(prop, annotationName) {
|
|
48
|
+
const annotations = prop.annotations?.filter((a) => a.name === annotationName);
|
|
49
|
+
if (!annotations || annotations.length === 0) return undefined;
|
|
50
|
+
return annotations[0].args.length > 0 ? annotations[0].args[0].text : undefined;
|
|
51
|
+
}
|
|
52
|
+
function refActionAnnotation(name) {
|
|
53
|
+
return new __atscript_core.AnnotationSpec({
|
|
54
|
+
description: `Referential action when the target ${name === "onDelete" ? "row is deleted" : "key is updated"}. Only valid on @db.rel.FK fields.\n\n` + "**Example:**\n" + "```atscript\n" + "@db.rel.FK\n" + `@db.rel.${name} "cascade"\n` + "authorId: User.id\n" + "```\n",
|
|
55
|
+
nodeType: ["prop"],
|
|
56
|
+
argument: {
|
|
57
|
+
name: "action",
|
|
58
|
+
type: "string",
|
|
59
|
+
values: [
|
|
60
|
+
"cascade",
|
|
61
|
+
"restrict",
|
|
62
|
+
"noAction",
|
|
63
|
+
"setNull",
|
|
64
|
+
"setDefault"
|
|
65
|
+
],
|
|
66
|
+
description: "Referential action: \"cascade\", \"restrict\", \"noAction\", \"setNull\", or \"setDefault\"."
|
|
67
|
+
},
|
|
68
|
+
validate(token, args, doc) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const field = token.parentNode;
|
|
71
|
+
if (field.countAnnotations("db.rel.FK") === 0) errors.push({
|
|
72
|
+
message: `@db.rel.${name} is only valid on @db.rel.FK fields`,
|
|
73
|
+
severity: 1,
|
|
74
|
+
range: token.range
|
|
75
|
+
});
|
|
76
|
+
if (args[0]) {
|
|
77
|
+
const action = args[0].text;
|
|
78
|
+
if (action === "setNull" && !field.has("optional")) errors.push({
|
|
79
|
+
message: `@db.rel.${name} "setNull" requires the FK field to be optional (?)`,
|
|
80
|
+
severity: 1,
|
|
81
|
+
range: token.range
|
|
82
|
+
});
|
|
83
|
+
if (action === "setDefault" && field.countAnnotations("db.default") === 0 && field.countAnnotations("db.default.increment") === 0 && field.countAnnotations("db.default.uuid") === 0 && field.countAnnotations("db.default.now") === 0) errors.push({
|
|
84
|
+
message: `@db.rel.${name} "setDefault" but no @db.default.* annotation — field will have no fallback value`,
|
|
85
|
+
severity: 2,
|
|
86
|
+
range: token.range
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const fkAlias = getAnnotationAlias(field, "db.rel.FK");
|
|
90
|
+
if (fkAlias) {
|
|
91
|
+
const struct = getParentStruct(token);
|
|
92
|
+
if (struct) {
|
|
93
|
+
const annotationName = `db.rel.${name}`;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const [, prop] of struct.props) {
|
|
96
|
+
if (prop.countAnnotations("db.rel.FK") === 0) continue;
|
|
97
|
+
if (prop.countAnnotations(annotationName) === 0) continue;
|
|
98
|
+
const propFkAlias = getAnnotationAlias(prop, "db.rel.FK");
|
|
99
|
+
if (propFkAlias === fkAlias) count++;
|
|
100
|
+
}
|
|
101
|
+
if (count > 1) errors.push({
|
|
102
|
+
message: `Composite FK '${fkAlias}' has @db.rel.${name} on multiple fields — declare it on exactly one`,
|
|
103
|
+
severity: 1,
|
|
104
|
+
range: token.range
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return errors;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region packages/db/src/shared/validation-utils.ts
|
|
115
|
+
function validateRefArgument(token, doc, options) {
|
|
116
|
+
const messages = [];
|
|
117
|
+
const text = token.text;
|
|
118
|
+
const [typeName, ...chain] = text.split(".");
|
|
119
|
+
const decl = doc.getDeclarationOwnerNode(typeName);
|
|
120
|
+
if (!decl) {
|
|
121
|
+
const regDef = doc.registry.definitions.get(typeName);
|
|
122
|
+
if (regDef?.imported) return messages;
|
|
123
|
+
messages.push({
|
|
124
|
+
severity: 1,
|
|
125
|
+
message: `Unknown type '${typeName}'.`,
|
|
126
|
+
range: token.range
|
|
127
|
+
});
|
|
128
|
+
return messages;
|
|
129
|
+
}
|
|
130
|
+
if (chain.length > 0) {
|
|
131
|
+
const unwound = doc.unwindType(typeName, chain);
|
|
132
|
+
if (!unwound) {
|
|
133
|
+
messages.push({
|
|
134
|
+
severity: 1,
|
|
135
|
+
message: `Field '${chain.join(".")}' does not exist on type '${typeName}'.`,
|
|
136
|
+
range: token.range
|
|
137
|
+
});
|
|
138
|
+
return messages;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options?.requireDbTable && decl.node) {
|
|
142
|
+
const hasDbTable = decl.node.countAnnotations("db.table") > 0;
|
|
143
|
+
if (!hasDbTable) messages.push({
|
|
144
|
+
severity: 1,
|
|
145
|
+
message: `Type '${typeName}' must have @db.table annotation.`,
|
|
146
|
+
range: token.range
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return messages;
|
|
150
|
+
}
|
|
151
|
+
function findFKFieldsPointingTo(doc, iface, targetTypeName, alias) {
|
|
152
|
+
const results = [];
|
|
153
|
+
let struct;
|
|
154
|
+
if ((0, __atscript_core.isInterface)(iface) && iface.hasExtends) {
|
|
155
|
+
const resolved = doc.resolveInterfaceExtends(iface);
|
|
156
|
+
if (resolved && (0, __atscript_core.isStructure)(resolved)) struct = resolved;
|
|
157
|
+
}
|
|
158
|
+
if (!struct) struct = (0, __atscript_core.isStructure)(iface) ? iface : (0, __atscript_core.isInterface)(iface) && (0, __atscript_core.isStructure)(iface.getDefinition()) ? iface.getDefinition() : undefined;
|
|
159
|
+
if (!struct) return results;
|
|
160
|
+
for (const [name, prop] of struct.props) {
|
|
161
|
+
if (prop.countAnnotations("db.rel.FK") === 0) continue;
|
|
162
|
+
const def = prop.getDefinition();
|
|
163
|
+
if (!def || !(0, __atscript_core.isRef)(def)) continue;
|
|
164
|
+
const ref = def;
|
|
165
|
+
if (!ref.hasChain) continue;
|
|
166
|
+
const refTypeName = ref.id;
|
|
167
|
+
const refField = ref.chain.map((c) => c.text).join(".");
|
|
168
|
+
if (refTypeName !== targetTypeName) continue;
|
|
169
|
+
if (alias !== undefined) {
|
|
170
|
+
const fkAnnotations = prop.annotations?.filter((a) => a.name === "db.rel.FK");
|
|
171
|
+
const hasMatchingAlias = fkAnnotations?.some((a) => a.args.length > 0 && a.args[0].text === alias);
|
|
172
|
+
if (!hasMatchingAlias) continue;
|
|
173
|
+
}
|
|
174
|
+
results.push({
|
|
175
|
+
name,
|
|
176
|
+
prop,
|
|
177
|
+
chainRef: {
|
|
178
|
+
type: refTypeName,
|
|
179
|
+
field: refField
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
const viewAnnotationNames = [
|
|
186
|
+
"db.view",
|
|
187
|
+
"db.view.for",
|
|
188
|
+
"db.view.joins",
|
|
189
|
+
"db.view.filter",
|
|
190
|
+
"db.view.materialized"
|
|
191
|
+
];
|
|
192
|
+
function hasAnyViewAnnotation(node) {
|
|
193
|
+
return viewAnnotationNames.some((name) => node.countAnnotations(name) > 0);
|
|
194
|
+
}
|
|
195
|
+
function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
|
|
196
|
+
const errors = [];
|
|
197
|
+
const queryNode = queryToken.queryNode;
|
|
198
|
+
if (!queryNode) return errors;
|
|
199
|
+
function walkFieldRef(ref) {
|
|
200
|
+
if (ref.typeRef) {
|
|
201
|
+
const typeName = ref.typeRef.text;
|
|
202
|
+
if (!allowedTypes.includes(typeName)) errors.push({
|
|
203
|
+
message: `Query references '${typeName}' which is not in scope — expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`,
|
|
204
|
+
severity: 1,
|
|
205
|
+
range: ref.typeRef.range
|
|
206
|
+
});
|
|
207
|
+
} else if (unqualifiedTarget === null) errors.push({
|
|
208
|
+
message: `Unqualified field reference '${ref.fieldRef.text}' — use qualified form (e.g., Type.${ref.fieldRef.text})`,
|
|
209
|
+
severity: 1,
|
|
210
|
+
range: ref.fieldRef.range
|
|
211
|
+
});
|
|
212
|
+
else {
|
|
213
|
+
const unwound = doc.unwindType(unqualifiedTarget);
|
|
214
|
+
if (unwound) {
|
|
215
|
+
const targetDef = unwound.def;
|
|
216
|
+
if ((0, __atscript_core.isInterface)(targetDef) || (0, __atscript_core.isStructure)(targetDef)) {
|
|
217
|
+
const struct = (0, __atscript_core.isInterface)(targetDef) ? targetDef.getDefinition() : targetDef;
|
|
218
|
+
if (struct && (0, __atscript_core.isStructure)(struct) && !struct.props.has(ref.fieldRef.text)) errors.push({
|
|
219
|
+
message: `Field '${ref.fieldRef.text}' does not exist on '${unqualifiedTarget}'`,
|
|
220
|
+
severity: 1,
|
|
221
|
+
range: ref.fieldRef.range
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function walkExpr(expr) {
|
|
228
|
+
if ((0, __atscript_core.isQueryLogical)(expr)) for (const operand of expr.operands) walkExpr(operand);
|
|
229
|
+
else if ((0, __atscript_core.isQueryComparison)(expr)) {
|
|
230
|
+
walkFieldRef(expr.left);
|
|
231
|
+
if (expr.right && "fieldRef" in expr.right) walkFieldRef(expr.right);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
walkExpr(queryNode.expression);
|
|
235
|
+
return errors;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
Object.defineProperty(exports, 'findFKFieldsPointingTo', {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
get: function () {
|
|
242
|
+
return findFKFieldsPointingTo;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(exports, 'getAnnotationAlias', {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
get: function () {
|
|
248
|
+
return getAnnotationAlias;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
Object.defineProperty(exports, 'getDbTableOwner', {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
get: function () {
|
|
254
|
+
return getDbTableOwner;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(exports, 'getNavTargetTypeName', {
|
|
258
|
+
enumerable: true,
|
|
259
|
+
get: function () {
|
|
260
|
+
return getNavTargetTypeName;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
Object.defineProperty(exports, 'getParentStruct', {
|
|
264
|
+
enumerable: true,
|
|
265
|
+
get: function () {
|
|
266
|
+
return getParentStruct;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
Object.defineProperty(exports, 'getParentTypeName', {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
get: function () {
|
|
272
|
+
return getParentTypeName;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(exports, 'hasAnyViewAnnotation', {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
get: function () {
|
|
278
|
+
return hasAnyViewAnnotation;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(exports, 'refActionAnnotation', {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get: function () {
|
|
284
|
+
return refActionAnnotation;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(exports, 'validateFieldBaseType', {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
get: function () {
|
|
290
|
+
return validateFieldBaseType;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
Object.defineProperty(exports, 'validateQueryScope', {
|
|
294
|
+
enumerable: true,
|
|
295
|
+
get: function () {
|
|
296
|
+
return validateQueryScope;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(exports, 'validateRefArgument', {
|
|
300
|
+
enumerable: true,
|
|
301
|
+
get: function () {
|
|
302
|
+
return validateRefArgument;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { AnnotationSpec, isArray, isInterface, isPrimitive, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
|
|
2
|
+
|
|
3
|
+
//#region packages/db/src/shared/annotation-utils.ts
|
|
4
|
+
function getDbTableOwner(token) {
|
|
5
|
+
const field = token.parentNode;
|
|
6
|
+
const struct = field.ownerNode;
|
|
7
|
+
if (!struct || !isStructure(struct)) return undefined;
|
|
8
|
+
const iface = struct.ownerNode;
|
|
9
|
+
return iface && isInterface(iface) ? iface : struct;
|
|
10
|
+
}
|
|
11
|
+
function getParentStruct(token) {
|
|
12
|
+
const field = token.parentNode;
|
|
13
|
+
const struct = field.ownerNode;
|
|
14
|
+
return struct && isStructure(struct) ? struct : undefined;
|
|
15
|
+
}
|
|
16
|
+
function getParentTypeName(token) {
|
|
17
|
+
const struct = getParentStruct(token);
|
|
18
|
+
if (!struct) return undefined;
|
|
19
|
+
const iface = struct.ownerNode;
|
|
20
|
+
return iface && isInterface(iface) ? iface.id : struct.id;
|
|
21
|
+
}
|
|
22
|
+
function validateFieldBaseType(token, doc, annotationName, expectedType) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const field = token.parentNode;
|
|
25
|
+
const definition = field.getDefinition();
|
|
26
|
+
if (!definition || !isRef(definition)) return errors;
|
|
27
|
+
const unwound = doc.unwindType(definition.id, definition.chain);
|
|
28
|
+
if (!unwound || !isPrimitive(unwound.def)) return errors;
|
|
29
|
+
const ct = unwound.def.config.type;
|
|
30
|
+
const baseType = typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
|
|
31
|
+
const allowed = Array.isArray(expectedType) ? expectedType : [expectedType];
|
|
32
|
+
if (!allowed.includes(baseType)) errors.push({
|
|
33
|
+
message: `${annotationName} is not compatible with type "${baseType}" — requires ${allowed.join(" or ")}`,
|
|
34
|
+
severity: 1,
|
|
35
|
+
range: token.range
|
|
36
|
+
});
|
|
37
|
+
return errors;
|
|
38
|
+
}
|
|
39
|
+
function getNavTargetTypeName(field) {
|
|
40
|
+
let def = field.getDefinition();
|
|
41
|
+
if (isArray(def)) def = def?.getDefinition();
|
|
42
|
+
if (isRef(def)) return def.id;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function getAnnotationAlias(prop, annotationName) {
|
|
46
|
+
const annotations = prop.annotations?.filter((a) => a.name === annotationName);
|
|
47
|
+
if (!annotations || annotations.length === 0) return undefined;
|
|
48
|
+
return annotations[0].args.length > 0 ? annotations[0].args[0].text : undefined;
|
|
49
|
+
}
|
|
50
|
+
function refActionAnnotation(name) {
|
|
51
|
+
return new AnnotationSpec({
|
|
52
|
+
description: `Referential action when the target ${name === "onDelete" ? "row is deleted" : "key is updated"}. Only valid on @db.rel.FK fields.\n\n` + "**Example:**\n" + "```atscript\n" + "@db.rel.FK\n" + `@db.rel.${name} "cascade"\n` + "authorId: User.id\n" + "```\n",
|
|
53
|
+
nodeType: ["prop"],
|
|
54
|
+
argument: {
|
|
55
|
+
name: "action",
|
|
56
|
+
type: "string",
|
|
57
|
+
values: [
|
|
58
|
+
"cascade",
|
|
59
|
+
"restrict",
|
|
60
|
+
"noAction",
|
|
61
|
+
"setNull",
|
|
62
|
+
"setDefault"
|
|
63
|
+
],
|
|
64
|
+
description: "Referential action: \"cascade\", \"restrict\", \"noAction\", \"setNull\", or \"setDefault\"."
|
|
65
|
+
},
|
|
66
|
+
validate(token, args, doc) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
const field = token.parentNode;
|
|
69
|
+
if (field.countAnnotations("db.rel.FK") === 0) errors.push({
|
|
70
|
+
message: `@db.rel.${name} is only valid on @db.rel.FK fields`,
|
|
71
|
+
severity: 1,
|
|
72
|
+
range: token.range
|
|
73
|
+
});
|
|
74
|
+
if (args[0]) {
|
|
75
|
+
const action = args[0].text;
|
|
76
|
+
if (action === "setNull" && !field.has("optional")) errors.push({
|
|
77
|
+
message: `@db.rel.${name} "setNull" requires the FK field to be optional (?)`,
|
|
78
|
+
severity: 1,
|
|
79
|
+
range: token.range
|
|
80
|
+
});
|
|
81
|
+
if (action === "setDefault" && field.countAnnotations("db.default") === 0 && field.countAnnotations("db.default.increment") === 0 && field.countAnnotations("db.default.uuid") === 0 && field.countAnnotations("db.default.now") === 0) errors.push({
|
|
82
|
+
message: `@db.rel.${name} "setDefault" but no @db.default.* annotation — field will have no fallback value`,
|
|
83
|
+
severity: 2,
|
|
84
|
+
range: token.range
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const fkAlias = getAnnotationAlias(field, "db.rel.FK");
|
|
88
|
+
if (fkAlias) {
|
|
89
|
+
const struct = getParentStruct(token);
|
|
90
|
+
if (struct) {
|
|
91
|
+
const annotationName = `db.rel.${name}`;
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (const [, prop] of struct.props) {
|
|
94
|
+
if (prop.countAnnotations("db.rel.FK") === 0) continue;
|
|
95
|
+
if (prop.countAnnotations(annotationName) === 0) continue;
|
|
96
|
+
const propFkAlias = getAnnotationAlias(prop, "db.rel.FK");
|
|
97
|
+
if (propFkAlias === fkAlias) count++;
|
|
98
|
+
}
|
|
99
|
+
if (count > 1) errors.push({
|
|
100
|
+
message: `Composite FK '${fkAlias}' has @db.rel.${name} on multiple fields — declare it on exactly one`,
|
|
101
|
+
severity: 1,
|
|
102
|
+
range: token.range
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return errors;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region packages/db/src/shared/validation-utils.ts
|
|
113
|
+
function validateRefArgument(token, doc, options) {
|
|
114
|
+
const messages = [];
|
|
115
|
+
const text = token.text;
|
|
116
|
+
const [typeName, ...chain] = text.split(".");
|
|
117
|
+
const decl = doc.getDeclarationOwnerNode(typeName);
|
|
118
|
+
if (!decl) {
|
|
119
|
+
const regDef = doc.registry.definitions.get(typeName);
|
|
120
|
+
if (regDef?.imported) return messages;
|
|
121
|
+
messages.push({
|
|
122
|
+
severity: 1,
|
|
123
|
+
message: `Unknown type '${typeName}'.`,
|
|
124
|
+
range: token.range
|
|
125
|
+
});
|
|
126
|
+
return messages;
|
|
127
|
+
}
|
|
128
|
+
if (chain.length > 0) {
|
|
129
|
+
const unwound = doc.unwindType(typeName, chain);
|
|
130
|
+
if (!unwound) {
|
|
131
|
+
messages.push({
|
|
132
|
+
severity: 1,
|
|
133
|
+
message: `Field '${chain.join(".")}' does not exist on type '${typeName}'.`,
|
|
134
|
+
range: token.range
|
|
135
|
+
});
|
|
136
|
+
return messages;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (options?.requireDbTable && decl.node) {
|
|
140
|
+
const hasDbTable = decl.node.countAnnotations("db.table") > 0;
|
|
141
|
+
if (!hasDbTable) messages.push({
|
|
142
|
+
severity: 1,
|
|
143
|
+
message: `Type '${typeName}' must have @db.table annotation.`,
|
|
144
|
+
range: token.range
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return messages;
|
|
148
|
+
}
|
|
149
|
+
function findFKFieldsPointingTo(doc, iface, targetTypeName, alias) {
|
|
150
|
+
const results = [];
|
|
151
|
+
let struct;
|
|
152
|
+
if (isInterface(iface) && iface.hasExtends) {
|
|
153
|
+
const resolved = doc.resolveInterfaceExtends(iface);
|
|
154
|
+
if (resolved && isStructure(resolved)) struct = resolved;
|
|
155
|
+
}
|
|
156
|
+
if (!struct) struct = isStructure(iface) ? iface : isInterface(iface) && isStructure(iface.getDefinition()) ? iface.getDefinition() : undefined;
|
|
157
|
+
if (!struct) return results;
|
|
158
|
+
for (const [name, prop] of struct.props) {
|
|
159
|
+
if (prop.countAnnotations("db.rel.FK") === 0) continue;
|
|
160
|
+
const def = prop.getDefinition();
|
|
161
|
+
if (!def || !isRef(def)) continue;
|
|
162
|
+
const ref = def;
|
|
163
|
+
if (!ref.hasChain) continue;
|
|
164
|
+
const refTypeName = ref.id;
|
|
165
|
+
const refField = ref.chain.map((c) => c.text).join(".");
|
|
166
|
+
if (refTypeName !== targetTypeName) continue;
|
|
167
|
+
if (alias !== undefined) {
|
|
168
|
+
const fkAnnotations = prop.annotations?.filter((a) => a.name === "db.rel.FK");
|
|
169
|
+
const hasMatchingAlias = fkAnnotations?.some((a) => a.args.length > 0 && a.args[0].text === alias);
|
|
170
|
+
if (!hasMatchingAlias) continue;
|
|
171
|
+
}
|
|
172
|
+
results.push({
|
|
173
|
+
name,
|
|
174
|
+
prop,
|
|
175
|
+
chainRef: {
|
|
176
|
+
type: refTypeName,
|
|
177
|
+
field: refField
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
const viewAnnotationNames = [
|
|
184
|
+
"db.view",
|
|
185
|
+
"db.view.for",
|
|
186
|
+
"db.view.joins",
|
|
187
|
+
"db.view.filter",
|
|
188
|
+
"db.view.materialized"
|
|
189
|
+
];
|
|
190
|
+
function hasAnyViewAnnotation(node) {
|
|
191
|
+
return viewAnnotationNames.some((name) => node.countAnnotations(name) > 0);
|
|
192
|
+
}
|
|
193
|
+
function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
|
|
194
|
+
const errors = [];
|
|
195
|
+
const queryNode = queryToken.queryNode;
|
|
196
|
+
if (!queryNode) return errors;
|
|
197
|
+
function walkFieldRef(ref) {
|
|
198
|
+
if (ref.typeRef) {
|
|
199
|
+
const typeName = ref.typeRef.text;
|
|
200
|
+
if (!allowedTypes.includes(typeName)) errors.push({
|
|
201
|
+
message: `Query references '${typeName}' which is not in scope — expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`,
|
|
202
|
+
severity: 1,
|
|
203
|
+
range: ref.typeRef.range
|
|
204
|
+
});
|
|
205
|
+
} else if (unqualifiedTarget === null) errors.push({
|
|
206
|
+
message: `Unqualified field reference '${ref.fieldRef.text}' — use qualified form (e.g., Type.${ref.fieldRef.text})`,
|
|
207
|
+
severity: 1,
|
|
208
|
+
range: ref.fieldRef.range
|
|
209
|
+
});
|
|
210
|
+
else {
|
|
211
|
+
const unwound = doc.unwindType(unqualifiedTarget);
|
|
212
|
+
if (unwound) {
|
|
213
|
+
const targetDef = unwound.def;
|
|
214
|
+
if (isInterface(targetDef) || isStructure(targetDef)) {
|
|
215
|
+
const struct = isInterface(targetDef) ? targetDef.getDefinition() : targetDef;
|
|
216
|
+
if (struct && isStructure(struct) && !struct.props.has(ref.fieldRef.text)) errors.push({
|
|
217
|
+
message: `Field '${ref.fieldRef.text}' does not exist on '${unqualifiedTarget}'`,
|
|
218
|
+
severity: 1,
|
|
219
|
+
range: ref.fieldRef.range
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function walkExpr(expr) {
|
|
226
|
+
if (isQueryLogical(expr)) for (const operand of expr.operands) walkExpr(operand);
|
|
227
|
+
else if (isQueryComparison(expr)) {
|
|
228
|
+
walkFieldRef(expr.left);
|
|
229
|
+
if (expr.right && "fieldRef" in expr.right) walkFieldRef(expr.right);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
walkExpr(queryNode.expression);
|
|
233
|
+
return errors;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
export { findFKFieldsPointingTo, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, refActionAnnotation, validateFieldBaseType, validateQueryScope, validateRefArgument };
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atscript/db",
|
|
3
|
+
"version": "0.1.38",
|
|
4
|
+
"description": "Database adapter utilities for atscript.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"atscript",
|
|
7
|
+
"database",
|
|
8
|
+
"utils"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/moostjs/atscript/tree/main/packages/db#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/moostjs/atscript/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Artem Maltsev",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moostjs/atscript.git",
|
|
19
|
+
"directory": "packages/db"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./sync": {
|
|
34
|
+
"types": "./dist/sync.d.ts",
|
|
35
|
+
"import": "./dist/sync.mjs",
|
|
36
|
+
"require": "./dist/sync.cjs"
|
|
37
|
+
},
|
|
38
|
+
"./plugin": {
|
|
39
|
+
"types": "./dist/plugin.d.ts",
|
|
40
|
+
"import": "./dist/plugin.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./rel": {
|
|
43
|
+
"types": "./dist/rel.d.ts",
|
|
44
|
+
"import": "./dist/rel.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./agg": {
|
|
47
|
+
"types": "./dist/agg.d.ts",
|
|
48
|
+
"import": "./dist/agg.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./shared": {
|
|
51
|
+
"types": "./dist/shared.d.ts",
|
|
52
|
+
"import": "./dist/shared.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
},
|
|
56
|
+
"build": [
|
|
57
|
+
{
|
|
58
|
+
"entries": [
|
|
59
|
+
"src/index.ts",
|
|
60
|
+
"src/sync.ts",
|
|
61
|
+
"src/plugin.ts",
|
|
62
|
+
"src/rel.ts",
|
|
63
|
+
"src/agg.ts",
|
|
64
|
+
"src/shared.ts"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"vitest": "3.2.4"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"@uniqu/core": "^0.1.2",
|
|
73
|
+
"@atscript/core": "^0.1.38",
|
|
74
|
+
"@atscript/typescript": "^0.1.38"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"before-build": "node ../typescript/cli.cjs -f js",
|
|
78
|
+
"pub": "pnpm publish --access public",
|
|
79
|
+
"test": "vitest"
|
|
80
|
+
}
|
|
81
|
+
}
|