@facilis.dev/analyze 0.0.0-stage → 0.1.0
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 +18 -2
- package/dist/analyze.d.ts +17 -0
- package/dist/analyze.js +99 -0
- package/dist/ast/node.d.ts +29 -0
- package/dist/ast/node.js +85 -0
- package/dist/ast/walk.d.ts +8 -0
- package/dist/ast/walk.js +40 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +12 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.js +51 -0
- package/dist/graph-edges.d.ts +3 -0
- package/dist/graph-edges.js +148 -0
- package/dist/graph-ids.d.ts +6 -0
- package/dist/graph-ids.js +22 -0
- package/dist/graph.d.ts +15 -0
- package/dist/graph.js +158 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/json.d.ts +8 -0
- package/dist/json.js +31 -0
- package/dist/module-graph.d.ts +33 -0
- package/dist/module-graph.js +86 -0
- package/dist/module-info.d.ts +27 -0
- package/dist/module-info.js +143 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +41 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +1 -0
- package/dist/passes/clients.d.ts +22 -0
- package/dist/passes/clients.js +167 -0
- package/dist/passes/config.d.ts +16 -0
- package/dist/passes/config.js +229 -0
- package/dist/passes/file-facts.d.ts +13 -0
- package/dist/passes/file-facts.js +130 -0
- package/dist/passes/i18n.d.ts +14 -0
- package/dist/passes/i18n.js +55 -0
- package/dist/passes/job-relations-diagnostics.d.ts +9 -0
- package/dist/passes/job-relations-diagnostics.js +38 -0
- package/dist/passes/job-relations.d.ts +18 -0
- package/dist/passes/job-relations.js +52 -0
- package/dist/passes/jobs.d.ts +20 -0
- package/dist/passes/jobs.js +133 -0
- package/dist/passes/jsx.d.ts +22 -0
- package/dist/passes/jsx.js +89 -0
- package/dist/passes/path-expression.d.ts +26 -0
- package/dist/passes/path-expression.js +265 -0
- package/dist/passes/route-config.d.ts +24 -0
- package/dist/passes/route-config.js +152 -0
- package/dist/passes/route-description.d.ts +13 -0
- package/dist/passes/route-description.js +23 -0
- package/dist/passes/route-elevation.d.ts +18 -0
- package/dist/passes/route-elevation.js +136 -0
- package/dist/passes/route-handler.d.ts +27 -0
- package/dist/passes/route-handler.js +87 -0
- package/dist/passes/route-location.d.ts +16 -0
- package/dist/passes/route-location.js +58 -0
- package/dist/passes/route-relation-resolve.d.ts +51 -0
- package/dist/passes/route-relation-resolve.js +172 -0
- package/dist/passes/route-relations-diagnostics.d.ts +15 -0
- package/dist/passes/route-relations-diagnostics.js +79 -0
- package/dist/passes/route-relations-follow.d.ts +16 -0
- package/dist/passes/route-relations-follow.js +116 -0
- package/dist/passes/route-relations-usage.d.ts +50 -0
- package/dist/passes/route-relations-usage.js +44 -0
- package/dist/passes/route-relations-walk.d.ts +25 -0
- package/dist/passes/route-relations-walk.js +199 -0
- package/dist/passes/route-relations.d.ts +17 -0
- package/dist/passes/route-relations.js +43 -0
- package/dist/passes/route-table.d.ts +21 -0
- package/dist/passes/route-table.js +104 -0
- package/dist/passes/route-tables.d.ts +10 -0
- package/dist/passes/route-tables.js +42 -0
- package/dist/passes/routes.d.ts +18 -0
- package/dist/passes/routes.js +152 -0
- package/dist/passes/schema-column-types.d.ts +9 -0
- package/dist/passes/schema-column-types.js +55 -0
- package/dist/passes/schema-columns.d.ts +35 -0
- package/dist/passes/schema-columns.js +197 -0
- package/dist/passes/schema-dsl.d.ts +24 -0
- package/dist/passes/schema-dsl.js +117 -0
- package/dist/passes/schema-file-links-ddl.d.ts +18 -0
- package/dist/passes/schema-file-links-ddl.js +106 -0
- package/dist/passes/schema-file-links.d.ts +19 -0
- package/dist/passes/schema-file-links.js +144 -0
- package/dist/passes/schema-foreign-keys.d.ts +21 -0
- package/dist/passes/schema-foreign-keys.js +80 -0
- package/dist/passes/schema-policies.d.ts +15 -0
- package/dist/passes/schema-policies.js +201 -0
- package/dist/passes/schema-re-exports.d.ts +5 -0
- package/dist/passes/schema-re-exports.js +77 -0
- package/dist/passes/schema-relations.d.ts +8 -0
- package/dist/passes/schema-relations.js +182 -0
- package/dist/passes/schema-scope.d.ts +7 -0
- package/dist/passes/schema-scope.js +20 -0
- package/dist/passes/schema-tables.d.ts +13 -0
- package/dist/passes/schema-tables.js +149 -0
- package/dist/passes/schema.d.ts +9 -0
- package/dist/passes/schema.js +55 -0
- package/dist/passes/screen-components.d.ts +22 -0
- package/dist/passes/screen-components.js +143 -0
- package/dist/passes/screen-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +159 -0
- package/dist/passes/screens.d.ts +12 -0
- package/dist/passes/screens.js +278 -0
- package/dist/passes/static-read.d.ts +47 -0
- package/dist/passes/static-read.js +151 -0
- package/dist/passes/views.d.ts +20 -0
- package/dist/passes/views.js +101 -0
- package/dist/passes/workflow-tables.d.ts +18 -0
- package/dist/passes/workflow-tables.js +71 -0
- package/dist/passes/workflows.d.ts +8 -0
- package/dist/passes/workflows.js +160 -0
- package/dist/path-resolver.d.ts +20 -0
- package/dist/path-resolver.js +134 -0
- package/dist/revision.d.ts +2 -0
- package/dist/revision.js +23 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.js +13 -0
- package/dist/static-eval.d.ts +36 -0
- package/dist/static-eval.js +215 -0
- package/dist/types-graph.d.ts +130 -0
- package/dist/types-graph.js +4 -0
- package/dist/types-jobs.d.ts +28 -0
- package/dist/types-jobs.js +14 -0
- package/dist/types-relations.d.ts +49 -0
- package/dist/types-relations.js +11 -0
- package/dist/types-workflows.d.ts +36 -0
- package/dist/types-workflows.js +3 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.js +1 -0
- package/package.json +49 -4
- package/src/analyze.ts +119 -0
- package/src/ast/node.ts +102 -0
- package/src/ast/walk.ts +37 -0
- package/src/diagnostics.ts +22 -0
- package/src/fs.ts +59 -0
- package/src/graph-edges.ts +167 -0
- package/src/graph-ids.ts +31 -0
- package/src/graph.ts +190 -0
- package/src/index.ts +24 -0
- package/src/json.ts +34 -0
- package/src/module-graph.ts +117 -0
- package/src/module-info.ts +162 -0
- package/src/node.ts +54 -0
- package/src/parser.ts +19 -0
- package/src/passes/clients.ts +233 -0
- package/src/passes/config.ts +278 -0
- package/src/passes/file-facts.ts +142 -0
- package/src/passes/i18n.ts +66 -0
- package/src/passes/job-relations-diagnostics.ts +60 -0
- package/src/passes/job-relations.ts +76 -0
- package/src/passes/jobs.ts +181 -0
- package/src/passes/jsx.ts +92 -0
- package/src/passes/path-expression.ts +297 -0
- package/src/passes/route-config.ts +188 -0
- package/src/passes/route-description.ts +33 -0
- package/src/passes/route-elevation.ts +161 -0
- package/src/passes/route-handler.ts +105 -0
- package/src/passes/route-location.ts +62 -0
- package/src/passes/route-relation-resolve.ts +228 -0
- package/src/passes/route-relations-diagnostics.ts +115 -0
- package/src/passes/route-relations-follow.ts +146 -0
- package/src/passes/route-relations-usage.ts +98 -0
- package/src/passes/route-relations-walk.ts +255 -0
- package/src/passes/route-relations.ts +62 -0
- package/src/passes/route-table.ts +123 -0
- package/src/passes/route-tables.ts +50 -0
- package/src/passes/routes.ts +188 -0
- package/src/passes/schema-column-types.ts +62 -0
- package/src/passes/schema-columns.ts +287 -0
- package/src/passes/schema-dsl.ts +136 -0
- package/src/passes/schema-file-links-ddl.ts +125 -0
- package/src/passes/schema-file-links.ts +213 -0
- package/src/passes/schema-foreign-keys.ts +121 -0
- package/src/passes/schema-policies.ts +270 -0
- package/src/passes/schema-re-exports.ts +95 -0
- package/src/passes/schema-relations.ts +239 -0
- package/src/passes/schema-scope.ts +29 -0
- package/src/passes/schema-tables.ts +216 -0
- package/src/passes/schema.ts +67 -0
- package/src/passes/screen-components.ts +162 -0
- package/src/passes/screen-preview.ts +268 -0
- package/src/passes/screens.ts +291 -0
- package/src/passes/static-read.ts +196 -0
- package/src/passes/views.ts +125 -0
- package/src/passes/workflow-tables.ts +87 -0
- package/src/passes/workflows.ts +213 -0
- package/src/path-resolver.ts +146 -0
- package/src/revision.ts +25 -0
- package/src/source.ts +15 -0
- package/src/static-eval.ts +242 -0
- package/src/types-graph.ts +161 -0
- package/src/types-jobs.ts +40 -0
- package/src/types-relations.ts +64 -0
- package/src/types-workflows.ts +42 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callArguments,
|
|
3
|
+
identifierName,
|
|
4
|
+
memberName,
|
|
5
|
+
nodeArray,
|
|
6
|
+
objectEntries,
|
|
7
|
+
propertyName,
|
|
8
|
+
propertyValue,
|
|
9
|
+
unwrapExpression,
|
|
10
|
+
type AstNode,
|
|
11
|
+
} from "../ast/node.ts";
|
|
12
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
13
|
+
import type { Diagnostic, RelationFact, UnknownFact } from "../types.ts";
|
|
14
|
+
import {
|
|
15
|
+
callbackExpression,
|
|
16
|
+
createDslResolver,
|
|
17
|
+
nodeRef,
|
|
18
|
+
schemaDiagnostics,
|
|
19
|
+
schemaUnknown,
|
|
20
|
+
topLevelDeclarators,
|
|
21
|
+
type DslResolver,
|
|
22
|
+
} from "./schema-dsl.ts";
|
|
23
|
+
import { resolveTableBinding, type TableLookup } from "./schema-foreign-keys.ts";
|
|
24
|
+
|
|
25
|
+
export interface RelationScan {
|
|
26
|
+
readonly relations: readonly RelationFact[];
|
|
27
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface HelperBindings {
|
|
31
|
+
readonly one: ReadonlySet<string>;
|
|
32
|
+
readonly many: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Map the callback's destructured `one`/`many` helpers to their local names. */
|
|
36
|
+
function helperBindings(callback: AstNode | undefined): HelperBindings | null {
|
|
37
|
+
const fn = unwrapExpression(callback);
|
|
38
|
+
if (fn === undefined) return null;
|
|
39
|
+
if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return null;
|
|
40
|
+
const param = unwrapExpression(nodeArray(fn, "params")[0]);
|
|
41
|
+
if (param?.type !== "ObjectPattern") return null;
|
|
42
|
+
const one = new Set<string>();
|
|
43
|
+
const many = new Set<string>();
|
|
44
|
+
for (const property of objectEntries(param)) {
|
|
45
|
+
if (property.type === "SpreadElement") continue;
|
|
46
|
+
const key = propertyName(property);
|
|
47
|
+
const local = identifierName(propertyValue(property));
|
|
48
|
+
if (local === undefined) continue;
|
|
49
|
+
if (key === "one") one.add(local);
|
|
50
|
+
if (key === "many") many.add(local);
|
|
51
|
+
}
|
|
52
|
+
return { one, many };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readColumnList(
|
|
56
|
+
options: AstNode | undefined,
|
|
57
|
+
file: ProjectFile,
|
|
58
|
+
unknowns: UnknownFact[],
|
|
59
|
+
): { fields: string[]; references: string[] } {
|
|
60
|
+
const empty = { fields: [] as string[], references: [] as string[] };
|
|
61
|
+
if (options === undefined) return empty;
|
|
62
|
+
const node = unwrapExpression(options);
|
|
63
|
+
if (node?.type !== "ObjectExpression") {
|
|
64
|
+
unknowns.push(
|
|
65
|
+
schemaUnknown("relation options are not an object literal", nodeRef(file, options)),
|
|
66
|
+
);
|
|
67
|
+
return empty;
|
|
68
|
+
}
|
|
69
|
+
const readList = (key: string): string[] => {
|
|
70
|
+
const property = objectEntries(node).find((entry) => propertyName(entry) === key);
|
|
71
|
+
if (property === undefined) return [];
|
|
72
|
+
const value = unwrapExpression(propertyValue(property));
|
|
73
|
+
if (value?.type !== "ArrayExpression") {
|
|
74
|
+
unknowns.push(
|
|
75
|
+
schemaUnknown("relation " + key + " is not an array literal", nodeRef(file, property)),
|
|
76
|
+
);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const columns: string[] = [];
|
|
80
|
+
for (const element of nodeArray(value, "elements")) {
|
|
81
|
+
const item = unwrapExpression(element);
|
|
82
|
+
const name = item?.type === "MemberExpression" ? memberName(item) : identifierName(item);
|
|
83
|
+
if (name === undefined) {
|
|
84
|
+
unknowns.push(
|
|
85
|
+
schemaUnknown(
|
|
86
|
+
"relation " + key + " entry is not a column reference",
|
|
87
|
+
nodeRef(file, item),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
columns.push(name);
|
|
93
|
+
}
|
|
94
|
+
return columns;
|
|
95
|
+
};
|
|
96
|
+
return { fields: readList("fields"), references: readList("references") };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readRelationEntry(input: {
|
|
100
|
+
readonly entry: AstNode;
|
|
101
|
+
readonly from: string;
|
|
102
|
+
readonly graph: ModuleGraph;
|
|
103
|
+
readonly file: ProjectFile;
|
|
104
|
+
readonly lookup: TableLookup;
|
|
105
|
+
readonly helpers: HelperBindings;
|
|
106
|
+
}): { relation: RelationFact | null; diagnostics: readonly Diagnostic[] } {
|
|
107
|
+
const { entry, from, graph, file, lookup, helpers } = input;
|
|
108
|
+
const unknowns: UnknownFact[] = [];
|
|
109
|
+
const value = unwrapExpression(propertyValue(entry));
|
|
110
|
+
const source = nodeRef(file, entry);
|
|
111
|
+
if (value?.type !== "CallExpression") {
|
|
112
|
+
return {
|
|
113
|
+
relation: null,
|
|
114
|
+
diagnostics: schemaDiagnostics([
|
|
115
|
+
schemaUnknown("relations() entry is not a one()/many() call", source),
|
|
116
|
+
]),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const calleeLocal = identifierName(unwrapExpression(value.callee));
|
|
120
|
+
const cardinality =
|
|
121
|
+
calleeLocal !== undefined && helpers.one.has(calleeLocal)
|
|
122
|
+
? "one"
|
|
123
|
+
: calleeLocal !== undefined && helpers.many.has(calleeLocal)
|
|
124
|
+
? "many"
|
|
125
|
+
: null;
|
|
126
|
+
if (cardinality === null) {
|
|
127
|
+
return {
|
|
128
|
+
relation: null,
|
|
129
|
+
diagnostics: schemaDiagnostics([
|
|
130
|
+
schemaUnknown("relations() entry is not a one()/many() call", source),
|
|
131
|
+
]),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const args = callArguments(value);
|
|
135
|
+
const targetLocal = identifierName(unwrapExpression(args[0]));
|
|
136
|
+
const targetTable =
|
|
137
|
+
targetLocal === undefined ? null : resolveTableBinding(graph, file.path, targetLocal, lookup);
|
|
138
|
+
if (targetTable === null) {
|
|
139
|
+
unknowns.push(schemaUnknown("relation target table is not resolvable statically", source));
|
|
140
|
+
}
|
|
141
|
+
const columns = readColumnList(args[1], file, unknowns);
|
|
142
|
+
const relation: RelationFact = {
|
|
143
|
+
from,
|
|
144
|
+
to: targetTable ?? targetLocal ?? "unknown",
|
|
145
|
+
cardinality,
|
|
146
|
+
fields: columns.fields,
|
|
147
|
+
references: columns.references,
|
|
148
|
+
source,
|
|
149
|
+
unknown: unknowns,
|
|
150
|
+
};
|
|
151
|
+
return { relation, diagnostics: schemaDiagnostics(unknowns) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readRelations(input: {
|
|
155
|
+
readonly graph: ModuleGraph;
|
|
156
|
+
readonly file: ProjectFile;
|
|
157
|
+
readonly call: AstNode;
|
|
158
|
+
readonly lookup: TableLookup;
|
|
159
|
+
}): RelationScan {
|
|
160
|
+
const { graph, file, call, lookup } = input;
|
|
161
|
+
const source = nodeRef(file, call);
|
|
162
|
+
const args = callArguments(call);
|
|
163
|
+
const fromLocal = identifierName(unwrapExpression(args[0]));
|
|
164
|
+
const fromTable =
|
|
165
|
+
fromLocal === undefined ? null : resolveTableBinding(graph, file.path, fromLocal, lookup);
|
|
166
|
+
const from = fromTable ?? fromLocal ?? "unknown";
|
|
167
|
+
const callback = args[1];
|
|
168
|
+
const body = callbackExpression(callback);
|
|
169
|
+
const helpers = helperBindings(callback);
|
|
170
|
+
if (body?.type !== "ObjectExpression" || helpers === null) {
|
|
171
|
+
return {
|
|
172
|
+
relations: [],
|
|
173
|
+
diagnostics: schemaDiagnostics([
|
|
174
|
+
schemaUnknown("relations() callback is not a destructured one/many callback", source),
|
|
175
|
+
]),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const relations: RelationFact[] = [];
|
|
179
|
+
const diagnostics: Diagnostic[] = [];
|
|
180
|
+
if (fromTable === null) {
|
|
181
|
+
const unknown = schemaUnknown("relation source table is not resolvable statically", source);
|
|
182
|
+
diagnostics.push(...schemaDiagnostics([unknown]));
|
|
183
|
+
}
|
|
184
|
+
for (const entry of objectEntries(body)) {
|
|
185
|
+
if (propertyName(entry) === undefined) {
|
|
186
|
+
diagnostics.push(
|
|
187
|
+
...schemaDiagnostics([
|
|
188
|
+
schemaUnknown("computed relation key is outside the DSL", nodeRef(file, entry)),
|
|
189
|
+
]),
|
|
190
|
+
);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const result = readRelationEntry({ entry, from, graph, file, lookup, helpers });
|
|
194
|
+
if (result.relation !== null) relations.push(result.relation);
|
|
195
|
+
diagnostics.push(...result.diagnostics);
|
|
196
|
+
}
|
|
197
|
+
return { relations, diagnostics };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function buildRelationFacts(
|
|
201
|
+
graph: ModuleGraph,
|
|
202
|
+
files: readonly ProjectFile[],
|
|
203
|
+
lookup: TableLookup,
|
|
204
|
+
): RelationScan {
|
|
205
|
+
const relations: RelationFact[] = [];
|
|
206
|
+
const diagnostics: Diagnostic[] = [];
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const resolver: DslResolver = createDslResolver(graph, file);
|
|
209
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
210
|
+
const call = unwrapExpression(declarator.init);
|
|
211
|
+
if (call?.type !== "CallExpression") continue;
|
|
212
|
+
const symbol = resolver(call.callee);
|
|
213
|
+
if (symbol?.pkg !== "drizzle-orm" || symbol.name !== "relations") continue;
|
|
214
|
+
const scan = readRelations({ graph, file, call, lookup });
|
|
215
|
+
relations.push(...scan.relations);
|
|
216
|
+
diagnostics.push(...scan.diagnostics);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { relations: dedupeRelations(relations), diagnostics };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Duplicate declarations (e.g. split files) must not duplicate edges. */
|
|
223
|
+
function dedupeRelations(relations: readonly RelationFact[]): RelationFact[] {
|
|
224
|
+
const seen = new Set<string>();
|
|
225
|
+
const out: RelationFact[] = [];
|
|
226
|
+
for (const relation of relations) {
|
|
227
|
+
const key = [
|
|
228
|
+
relation.from,
|
|
229
|
+
relation.to,
|
|
230
|
+
relation.cardinality,
|
|
231
|
+
relation.fields.join(","),
|
|
232
|
+
relation.references.join(","),
|
|
233
|
+
].join("|");
|
|
234
|
+
if (seen.has(key)) continue;
|
|
235
|
+
seen.add(key);
|
|
236
|
+
out.push(relation);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ColumnFact, ConfigFacts, ScopeFact } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export interface ScopedColumn {
|
|
4
|
+
readonly column: ColumnFact;
|
|
5
|
+
readonly tenant: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalized(value: string): string {
|
|
9
|
+
return value.replaceAll("_", "").replaceAll("-", "").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function matchesConfig(column: ColumnFact, names: readonly string[]): boolean {
|
|
13
|
+
const property = normalized(column.property);
|
|
14
|
+
const sqlName = normalized(column.name);
|
|
15
|
+
return names.some((candidate) => {
|
|
16
|
+
const value = normalized(candidate);
|
|
17
|
+
return value === property || value === sqlName;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Tenant scope wins over owner scope: both can be declared on the same table. */
|
|
22
|
+
export function scopeOf(columns: readonly ScopedColumn[], config: ConfigFacts): ScopeFact | null {
|
|
23
|
+
const tenant =
|
|
24
|
+
columns.find((entry) => entry.tenant) ??
|
|
25
|
+
columns.find((entry) => matchesConfig(entry.column, config.tenantColumns));
|
|
26
|
+
if (tenant !== undefined) return { column: tenant.column.property, kind: "tenant" };
|
|
27
|
+
const owner = columns.find((entry) => matchesConfig(entry.column, config.ownerColumns));
|
|
28
|
+
return owner === undefined ? null : { column: owner.column.property, kind: "owner" };
|
|
29
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callArguments,
|
|
3
|
+
identifierName,
|
|
4
|
+
memberName,
|
|
5
|
+
objectEntries,
|
|
6
|
+
propertyName,
|
|
7
|
+
propertyValue,
|
|
8
|
+
unwrapExpression,
|
|
9
|
+
type AstNode,
|
|
10
|
+
} from "../ast/node.ts";
|
|
11
|
+
import { diagnostic } from "../diagnostics.ts";
|
|
12
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
13
|
+
import { staticString } from "../static-eval.ts";
|
|
14
|
+
import type { ConfigFacts, Diagnostic, TableFact, UnknownFact } from "../types.ts";
|
|
15
|
+
import { buildColumnFact, type ColumnReference } from "./schema-columns.ts";
|
|
16
|
+
import { scopeOf, type ScopedColumn } from "./schema-scope.ts";
|
|
17
|
+
import {
|
|
18
|
+
createDslResolver,
|
|
19
|
+
nodeRef,
|
|
20
|
+
schemaDiagnostics,
|
|
21
|
+
schemaUnknown,
|
|
22
|
+
topLevelDeclarators,
|
|
23
|
+
type DslResolver,
|
|
24
|
+
} from "./schema-dsl.ts";
|
|
25
|
+
import { buildPolicyFacts } from "./schema-policies.ts";
|
|
26
|
+
|
|
27
|
+
export interface TableDraft {
|
|
28
|
+
readonly fact: TableFact;
|
|
29
|
+
readonly binding: string | null;
|
|
30
|
+
readonly references: readonly ColumnReference[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TableScan {
|
|
34
|
+
readonly drafts: readonly TableDraft[];
|
|
35
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TableCall {
|
|
39
|
+
readonly call: AstNode;
|
|
40
|
+
readonly methods: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Local `pgEnum("name", values)` consts; a column built from one is a known enum builder. */
|
|
44
|
+
function collectEnumBindings(
|
|
45
|
+
file: ProjectFile,
|
|
46
|
+
resolver: DslResolver,
|
|
47
|
+
graph: ModuleGraph,
|
|
48
|
+
): Map<string, string> {
|
|
49
|
+
const enums = new Map<string, string>();
|
|
50
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
51
|
+
const binding = identifierName(declarator.id);
|
|
52
|
+
const init = unwrapExpression(declarator.init);
|
|
53
|
+
if (binding === undefined || init?.type !== "CallExpression") continue;
|
|
54
|
+
const symbol = resolver(init.callee);
|
|
55
|
+
if (symbol?.pkg !== "pg-core" || symbol.name !== "pgEnum") continue;
|
|
56
|
+
const name = staticString(callArguments(init)[0], { graph, file: file.path });
|
|
57
|
+
enums.set(binding, name.ok ? name.value : binding);
|
|
58
|
+
}
|
|
59
|
+
return enums;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The `pgTable` call inside `pgTable(...).enableRLS()`, plus the chain methods after it. */
|
|
63
|
+
function findTableCall(init: unknown, resolver: DslResolver): TableCall | null {
|
|
64
|
+
let current = unwrapExpression(init);
|
|
65
|
+
const methods: string[] = [];
|
|
66
|
+
let guard = 0;
|
|
67
|
+
while (current?.type === "CallExpression" && guard < 32) {
|
|
68
|
+
guard += 1;
|
|
69
|
+
const callee = unwrapExpression(current.callee);
|
|
70
|
+
if (callee === undefined) return null;
|
|
71
|
+
if (callee.type === "MemberExpression") {
|
|
72
|
+
const name = memberName(callee);
|
|
73
|
+
if (name === undefined) return null;
|
|
74
|
+
methods.unshift(name);
|
|
75
|
+
current = unwrapExpression(callee.object);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const symbol = resolver(callee);
|
|
79
|
+
if (symbol?.pkg !== "pg-core" || symbol.name !== "pgTable") return null;
|
|
80
|
+
return { call: current, methods };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildTableDraft(input: {
|
|
86
|
+
readonly graph: ModuleGraph;
|
|
87
|
+
readonly file: ProjectFile;
|
|
88
|
+
readonly resolver: DslResolver;
|
|
89
|
+
readonly enums: ReadonlyMap<string, string>;
|
|
90
|
+
readonly config: ConfigFacts;
|
|
91
|
+
readonly binding: string | null;
|
|
92
|
+
readonly tableCall: TableCall;
|
|
93
|
+
}): { draft: TableDraft; diagnostics: readonly Diagnostic[] } {
|
|
94
|
+
const { graph, file, resolver, enums, config, binding, tableCall } = input;
|
|
95
|
+
const source = nodeRef(file, tableCall.call);
|
|
96
|
+
const args = callArguments(tableCall.call);
|
|
97
|
+
const unknowns: UnknownFact[] = [];
|
|
98
|
+
const diagnostics: Diagnostic[] = [];
|
|
99
|
+
const nameResult = staticString(args[0], { graph, file: file.path });
|
|
100
|
+
let name: string;
|
|
101
|
+
if (nameResult.ok && nameResult.value.length > 0) {
|
|
102
|
+
name = nameResult.value;
|
|
103
|
+
} else {
|
|
104
|
+
name = binding ?? "unknown";
|
|
105
|
+
unknowns.push(schemaUnknown('table name of "' + name + '" is not a static string', source));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const columns: ScopedColumn[] = [];
|
|
109
|
+
const references: ColumnReference[] = [];
|
|
110
|
+
const columnsArgument = unwrapExpression(args[1]);
|
|
111
|
+
if (columnsArgument?.type !== "ObjectExpression") {
|
|
112
|
+
unknowns.push(
|
|
113
|
+
schemaUnknown('columns of table "' + name + '" are not an object literal', source),
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
for (const entry of objectEntries(columnsArgument)) {
|
|
117
|
+
if (entry.type === "SpreadElement") {
|
|
118
|
+
unknowns.push(
|
|
119
|
+
schemaUnknown(
|
|
120
|
+
'spread columns in table "' + name + '" are outside the DSL',
|
|
121
|
+
nodeRef(file, entry),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const property = propertyName(entry);
|
|
127
|
+
if (property === undefined) {
|
|
128
|
+
unknowns.push(
|
|
129
|
+
schemaUnknown(
|
|
130
|
+
'computed column key in table "' + name + '" is outside the DSL',
|
|
131
|
+
nodeRef(file, entry),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const result = buildColumnFact({
|
|
137
|
+
property,
|
|
138
|
+
expression: propertyValue(entry),
|
|
139
|
+
propertyNode: entry,
|
|
140
|
+
file,
|
|
141
|
+
graph,
|
|
142
|
+
resolver,
|
|
143
|
+
enums,
|
|
144
|
+
});
|
|
145
|
+
diagnostics.push(...result.diagnostics);
|
|
146
|
+
if (result.reference !== null) {
|
|
147
|
+
references.push({ column: property, target: result.reference });
|
|
148
|
+
}
|
|
149
|
+
columns.push({ column: result.column, tenant: result.tenant });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const policyScan = buildPolicyFacts({
|
|
154
|
+
extras: args[2],
|
|
155
|
+
file,
|
|
156
|
+
graph,
|
|
157
|
+
resolver,
|
|
158
|
+
});
|
|
159
|
+
diagnostics.push(...policyScan.diagnostics);
|
|
160
|
+
const rlsEnabled = tableCall.methods.includes("enableRLS");
|
|
161
|
+
const scope = scopeOf(columns, config);
|
|
162
|
+
const fact: TableFact = {
|
|
163
|
+
name,
|
|
164
|
+
binding,
|
|
165
|
+
columns: columns.map((entry) => entry.column),
|
|
166
|
+
primaryKey: columns
|
|
167
|
+
.filter((entry) => entry.column.primaryKey)
|
|
168
|
+
.map((entry) => entry.column.property),
|
|
169
|
+
foreignKeys: [],
|
|
170
|
+
policies: policyScan.policies,
|
|
171
|
+
rlsEnabled,
|
|
172
|
+
scope,
|
|
173
|
+
source,
|
|
174
|
+
unknown: unknowns,
|
|
175
|
+
};
|
|
176
|
+
diagnostics.push(...schemaDiagnostics(unknowns));
|
|
177
|
+
if (scope !== null && (!rlsEnabled || policyScan.policies.length === 0)) {
|
|
178
|
+
diagnostics.push(
|
|
179
|
+
diagnostic(
|
|
180
|
+
"rls-on-scoped-table",
|
|
181
|
+
"error",
|
|
182
|
+
'scoped table "' + name + '" must call .enableRLS() and declare a policy',
|
|
183
|
+
source,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return { draft: { fact, binding, references }, diagnostics };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function collectTableDrafts(
|
|
191
|
+
graph: ModuleGraph,
|
|
192
|
+
file: ProjectFile,
|
|
193
|
+
config: ConfigFacts,
|
|
194
|
+
): TableScan {
|
|
195
|
+
const resolver = createDslResolver(graph, file);
|
|
196
|
+
const enums = collectEnumBindings(file, resolver, graph);
|
|
197
|
+
const drafts: TableDraft[] = [];
|
|
198
|
+
const diagnostics: Diagnostic[] = [];
|
|
199
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
200
|
+
const binding = identifierName(declarator.id) ?? null;
|
|
201
|
+
const tableCall = findTableCall(declarator.init, resolver);
|
|
202
|
+
if (tableCall === null) continue;
|
|
203
|
+
const result = buildTableDraft({
|
|
204
|
+
graph,
|
|
205
|
+
file,
|
|
206
|
+
resolver,
|
|
207
|
+
enums,
|
|
208
|
+
config,
|
|
209
|
+
binding,
|
|
210
|
+
tableCall,
|
|
211
|
+
});
|
|
212
|
+
drafts.push(result.draft);
|
|
213
|
+
diagnostics.push(...result.diagnostics);
|
|
214
|
+
}
|
|
215
|
+
return { drafts, diagnostics };
|
|
216
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
2
|
+
import type { ConfigFacts, Diagnostic, RelationFact, TableFact } from "../types.ts";
|
|
3
|
+
import { isSchemaFile } from "./schema-dsl.ts";
|
|
4
|
+
import { collectFileLinkDrafts } from "./schema-file-links.ts";
|
|
5
|
+
import { buildTableLookup, finalizeTables, withFrameworkTables } from "./schema-foreign-keys.ts";
|
|
6
|
+
import { collectTableDrafts, type TableDraft } from "./schema-tables.ts";
|
|
7
|
+
import { collectReExportedDrafts } from "./schema-re-exports.ts";
|
|
8
|
+
import { buildRelationFacts } from "./schema-relations.ts";
|
|
9
|
+
|
|
10
|
+
export interface SchemaResult {
|
|
11
|
+
readonly tables: readonly TableFact[];
|
|
12
|
+
readonly relations: readonly RelationFact[];
|
|
13
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function schemaFiles(graph: ModuleGraph): ProjectFile[] {
|
|
17
|
+
return [...graph.files.values()]
|
|
18
|
+
.filter((file) => isSchemaFile(graph, file.path))
|
|
19
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Tables declared twice (invalid TS, but analyzable) keep the first declaration. */
|
|
23
|
+
function dedupeDrafts(drafts: readonly TableDraft[]): TableDraft[] {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
const out: TableDraft[] = [];
|
|
26
|
+
for (const draft of drafts) {
|
|
27
|
+
if (seen.has(draft.fact.name)) continue;
|
|
28
|
+
seen.add(draft.fact.name);
|
|
29
|
+
out.push(draft);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Interpret the Drizzle DSL under `db/**`, including framework tables reached by re-export. */
|
|
35
|
+
export function buildSchemaFacts(graph: ModuleGraph, config: ConfigFacts): SchemaResult {
|
|
36
|
+
const files = schemaFiles(graph);
|
|
37
|
+
const collected: TableDraft[] = [];
|
|
38
|
+
const externalTables: TableDraft[] = [];
|
|
39
|
+
const diagnostics: Diagnostic[] = [];
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const scan = collectTableDrafts(graph, file, config);
|
|
42
|
+
collected.push(...scan.drafts);
|
|
43
|
+
diagnostics.push(...scan.diagnostics);
|
|
44
|
+
// Generated link tables follow the file's own declarations, which is their source order:
|
|
45
|
+
// `const todoFiles = fileLinks(todos, ...)` comes after the `todos` it references.
|
|
46
|
+
const links = collectFileLinkDrafts(graph, file, scan.drafts);
|
|
47
|
+
collected.push(...links.drafts);
|
|
48
|
+
externalTables.push(...links.externalTables);
|
|
49
|
+
diagnostics.push(...links.diagnostics);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const seenNames = new Set(collected.map((draft) => draft.fact.name));
|
|
53
|
+
const visitedFiles = new Set(files.map((file) => file.path));
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const scan = collectReExportedDrafts(graph, file, config, seenNames, visitedFiles);
|
|
56
|
+
collected.push(...scan.drafts);
|
|
57
|
+
diagnostics.push(...scan.diagnostics);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const drafts = dedupeDrafts(collected);
|
|
61
|
+
const lookup = withFrameworkTables(buildTableLookup(drafts), externalTables);
|
|
62
|
+
const finalized = finalizeTables(graph, drafts, lookup);
|
|
63
|
+
diagnostics.push(...finalized.diagnostics);
|
|
64
|
+
const relations = buildRelationFacts(graph, files, lookup);
|
|
65
|
+
diagnostics.push(...relations.diagnostics);
|
|
66
|
+
return { tables: finalized.tables, relations: relations.relations, diagnostics };
|
|
67
|
+
}
|