@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,62 @@
|
|
|
1
|
+
import type { AstNode } from "../ast/node.ts";
|
|
2
|
+
import { isRecord } from "../json.ts";
|
|
3
|
+
import { evaluateStatic, type StaticContext } from "../static-eval.ts";
|
|
4
|
+
import type { ColumnDefaultFact } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
/** Static object argument of a builder/method, e.g. `{ withTimezone: true }`. */
|
|
7
|
+
export function optionsOf(
|
|
8
|
+
node: AstNode | undefined,
|
|
9
|
+
context: StaticContext,
|
|
10
|
+
): Record<string, unknown> | null {
|
|
11
|
+
if (node === undefined) return null;
|
|
12
|
+
const evaluated = evaluateStatic(node, context);
|
|
13
|
+
return evaluated.ok && isRecord(evaluated.value) ? evaluated.value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Display type derived from the builder and its static arguments (never a rendered SQL type). */
|
|
17
|
+
export function displayType(
|
|
18
|
+
name: string,
|
|
19
|
+
args: readonly AstNode[],
|
|
20
|
+
context: StaticContext,
|
|
21
|
+
): string {
|
|
22
|
+
switch (name) {
|
|
23
|
+
case "varchar": {
|
|
24
|
+
const length = optionsOf(args[1], context)?.["length"];
|
|
25
|
+
return typeof length === "number" ? "varchar(" + length + ")" : "varchar";
|
|
26
|
+
}
|
|
27
|
+
case "timestamp": {
|
|
28
|
+
const options = optionsOf(args[1], context);
|
|
29
|
+
const precision = options?.["precision"];
|
|
30
|
+
const base = typeof precision === "number" ? "timestamp(" + precision + ")" : "timestamp";
|
|
31
|
+
return options?.["withTimezone"] === true ? base + " with time zone" : base;
|
|
32
|
+
}
|
|
33
|
+
case "numeric": {
|
|
34
|
+
const options = optionsOf(args[1], context);
|
|
35
|
+
const precision = options?.["precision"];
|
|
36
|
+
if (typeof precision !== "number") return "numeric";
|
|
37
|
+
const scale = options?.["scale"];
|
|
38
|
+
return typeof scale === "number"
|
|
39
|
+
? "numeric(" + precision + ", " + scale + ")"
|
|
40
|
+
: "numeric(" + precision + ")";
|
|
41
|
+
}
|
|
42
|
+
case "doublePrecision":
|
|
43
|
+
return "double precision";
|
|
44
|
+
case "tenantColumn":
|
|
45
|
+
return "uuid";
|
|
46
|
+
default:
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `.default(value)`: only primitives are facts; a SQL expression is an explicit unknown. */
|
|
52
|
+
export function readDefault(node: AstNode | undefined, context: StaticContext): ColumnDefaultFact {
|
|
53
|
+
if (node === undefined) return { kind: "unknown", value: null };
|
|
54
|
+
const evaluated = evaluateStatic(node, context);
|
|
55
|
+
if (!evaluated.ok) return { kind: "unknown", value: null };
|
|
56
|
+
const value = evaluated.value;
|
|
57
|
+
if (value === null) return { kind: "literal", value: "null" };
|
|
58
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
59
|
+
return { kind: "literal", value: String(value) };
|
|
60
|
+
}
|
|
61
|
+
return { kind: "unknown", value: null };
|
|
62
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callArguments,
|
|
3
|
+
identifierName,
|
|
4
|
+
literalString,
|
|
5
|
+
memberName,
|
|
6
|
+
unwrapExpression,
|
|
7
|
+
type AstNode,
|
|
8
|
+
} from "../ast/node.ts";
|
|
9
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
10
|
+
import { staticString, type StaticContext } from "../static-eval.ts";
|
|
11
|
+
import type {
|
|
12
|
+
ColumnDefaultFact,
|
|
13
|
+
ColumnFact,
|
|
14
|
+
Diagnostic,
|
|
15
|
+
SourceRef,
|
|
16
|
+
UnknownFact,
|
|
17
|
+
} from "../types.ts";
|
|
18
|
+
import {
|
|
19
|
+
callbackExpression,
|
|
20
|
+
nodeRef,
|
|
21
|
+
schemaDiagnostics,
|
|
22
|
+
schemaUnknown,
|
|
23
|
+
type DslResolver,
|
|
24
|
+
type DslSymbol,
|
|
25
|
+
} from "./schema-dsl.ts";
|
|
26
|
+
import { displayType, optionsOf, readDefault } from "./schema-column-types.ts";
|
|
27
|
+
|
|
28
|
+
/** A `.references(() => table.column, opts)` target; the table name is resolved after every draft exists. */
|
|
29
|
+
export interface ForeignKeyTarget {
|
|
30
|
+
readonly binding: string;
|
|
31
|
+
readonly property: string;
|
|
32
|
+
readonly onDelete: string | null;
|
|
33
|
+
readonly onUpdate: string | null;
|
|
34
|
+
readonly source: SourceRef;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ColumnReference {
|
|
38
|
+
readonly column: string;
|
|
39
|
+
readonly target: ForeignKeyTarget;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ColumnResult {
|
|
43
|
+
readonly column: ColumnFact;
|
|
44
|
+
readonly reference: ForeignKeyTarget | null;
|
|
45
|
+
/** True when the builder is `tenantColumn()`; the table pass turns it into a scope fact. */
|
|
46
|
+
readonly tenant: boolean;
|
|
47
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ColumnInput {
|
|
51
|
+
readonly property: string;
|
|
52
|
+
readonly expression: unknown;
|
|
53
|
+
/** Property node: the source line when the value is unreadable. */
|
|
54
|
+
readonly propertyNode: AstNode;
|
|
55
|
+
readonly file: ProjectFile;
|
|
56
|
+
readonly graph: ModuleGraph;
|
|
57
|
+
readonly resolver: DslResolver;
|
|
58
|
+
/** Local `pgEnum` const bindings, local name -> enum name. */
|
|
59
|
+
readonly enums: ReadonlyMap<string, string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface MethodCall {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly args: readonly AstNode[];
|
|
65
|
+
readonly node: AstNode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface BuilderChain {
|
|
69
|
+
readonly builder: DslSymbol | null;
|
|
70
|
+
readonly calleeName: string;
|
|
71
|
+
readonly args: readonly AstNode[];
|
|
72
|
+
readonly methods: readonly MethodCall[];
|
|
73
|
+
readonly enumType: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PG_BUILDERS = new Set([
|
|
77
|
+
"uuid",
|
|
78
|
+
"text",
|
|
79
|
+
"boolean",
|
|
80
|
+
"timestamp",
|
|
81
|
+
"varchar",
|
|
82
|
+
"integer",
|
|
83
|
+
"bigint",
|
|
84
|
+
"numeric",
|
|
85
|
+
"date",
|
|
86
|
+
"jsonb",
|
|
87
|
+
"json",
|
|
88
|
+
"serial",
|
|
89
|
+
"bigserial",
|
|
90
|
+
"real",
|
|
91
|
+
"doublePrecision",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
function isKnownBuilder(symbol: DslSymbol): boolean {
|
|
95
|
+
if (symbol.pkg === "pg-core") return PG_BUILDERS.has(symbol.name);
|
|
96
|
+
return symbol.pkg === "facilis-db" && symbol.name === "tenantColumn";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Split `uuid("id").primaryKey().defaultRandom()` into its builder call and chained methods. */
|
|
100
|
+
function readBuilderChain(
|
|
101
|
+
expression: unknown,
|
|
102
|
+
resolver: DslResolver,
|
|
103
|
+
enums: ReadonlyMap<string, string>,
|
|
104
|
+
): BuilderChain | null {
|
|
105
|
+
let current = unwrapExpression(expression);
|
|
106
|
+
const methods: MethodCall[] = [];
|
|
107
|
+
let guard = 0;
|
|
108
|
+
while (current !== undefined && guard < 32) {
|
|
109
|
+
guard += 1;
|
|
110
|
+
if (current.type !== "CallExpression") return null;
|
|
111
|
+
const callee = unwrapExpression(current.callee);
|
|
112
|
+
if (callee === undefined) return null;
|
|
113
|
+
const args = callArguments(current);
|
|
114
|
+
if (callee.type === "MemberExpression") {
|
|
115
|
+
const name = memberName(callee);
|
|
116
|
+
if (name === undefined) return null;
|
|
117
|
+
methods.unshift({ name, args, node: current });
|
|
118
|
+
current = unwrapExpression(callee.object);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const calleeName = identifierName(callee);
|
|
122
|
+
if (calleeName === undefined) return null;
|
|
123
|
+
const enumType = enums.get(calleeName);
|
|
124
|
+
if (enumType !== undefined) {
|
|
125
|
+
return { builder: null, calleeName, args, methods, enumType };
|
|
126
|
+
}
|
|
127
|
+
return { builder: resolver(callee), calleeName, args, methods, enumType: null };
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function optionString(
|
|
133
|
+
options: Record<string, unknown>,
|
|
134
|
+
key: string,
|
|
135
|
+
source: SourceRef,
|
|
136
|
+
unknowns: UnknownFact[],
|
|
137
|
+
): string | null {
|
|
138
|
+
if (!Object.hasOwn(options, key)) return null;
|
|
139
|
+
const value = options[key];
|
|
140
|
+
if (typeof value === "string") return value;
|
|
141
|
+
unknowns.push(schemaUnknown('references() option "' + key + '" is not a string', source));
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readReference(
|
|
146
|
+
method: MethodCall,
|
|
147
|
+
context: StaticContext,
|
|
148
|
+
file: ProjectFile,
|
|
149
|
+
): { target: ForeignKeyTarget | null; unknowns: UnknownFact[] } {
|
|
150
|
+
const source = nodeRef(file, method.node);
|
|
151
|
+
const target = callbackExpression(method.args[0]);
|
|
152
|
+
const binding =
|
|
153
|
+
target?.type === "MemberExpression"
|
|
154
|
+
? identifierName(unwrapExpression(target.object))
|
|
155
|
+
: undefined;
|
|
156
|
+
const property = target?.type === "MemberExpression" ? memberName(target) : undefined;
|
|
157
|
+
if (binding === undefined || property === undefined) {
|
|
158
|
+
return {
|
|
159
|
+
target: null,
|
|
160
|
+
unknowns: [
|
|
161
|
+
schemaUnknown("references() target is not a static table.column reference", source),
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const unknowns: UnknownFact[] = [];
|
|
166
|
+
let onDelete: string | null = null;
|
|
167
|
+
let onUpdate: string | null = null;
|
|
168
|
+
if (method.args[1] !== undefined) {
|
|
169
|
+
const options = optionsOf(method.args[1], context);
|
|
170
|
+
if (options === null) {
|
|
171
|
+
unknowns.push(schemaUnknown("references() options are not static", source));
|
|
172
|
+
} else {
|
|
173
|
+
onDelete = optionString(options, "onDelete", source, unknowns);
|
|
174
|
+
onUpdate = optionString(options, "onUpdate", source, unknowns);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { target: { binding, property, onDelete, onUpdate, source }, unknowns };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildColumnFact(input: ColumnInput): ColumnResult {
|
|
181
|
+
const context: StaticContext = { graph: input.graph, file: input.file.path };
|
|
182
|
+
const valueNode = unwrapExpression(input.expression);
|
|
183
|
+
const source = nodeRef(input.file, valueNode ?? input.propertyNode);
|
|
184
|
+
const chain = readBuilderChain(input.expression, input.resolver, input.enums);
|
|
185
|
+
const unknowns: UnknownFact[] = [];
|
|
186
|
+
let builder = "unknown";
|
|
187
|
+
let type = "unknown";
|
|
188
|
+
let name = input.property;
|
|
189
|
+
let tenant = false;
|
|
190
|
+
let reference: ForeignKeyTarget | null = null;
|
|
191
|
+
|
|
192
|
+
if (chain === null) {
|
|
193
|
+
unknowns.push(
|
|
194
|
+
schemaUnknown('column "' + input.property + '" is not a column builder call', source),
|
|
195
|
+
);
|
|
196
|
+
} else if (chain.enumType !== null) {
|
|
197
|
+
builder = chain.calleeName;
|
|
198
|
+
type = chain.enumType;
|
|
199
|
+
name = literalString(chain.args[0]) ?? input.property;
|
|
200
|
+
} else if (chain.builder !== null && isKnownBuilder(chain.builder)) {
|
|
201
|
+
builder = chain.builder.name;
|
|
202
|
+
type = displayType(chain.builder.name, chain.args, context);
|
|
203
|
+
tenant = chain.builder.pkg === "facilis-db" && chain.builder.name === "tenantColumn";
|
|
204
|
+
const first = chain.args[0];
|
|
205
|
+
if (first === undefined) {
|
|
206
|
+
// `tenantColumn()` defaults its SQL name to tenant_id in @facilis.dev/db; other builders use the property key.
|
|
207
|
+
name = tenant ? "tenant_id" : input.property;
|
|
208
|
+
} else {
|
|
209
|
+
const evaluated = staticString(first, context);
|
|
210
|
+
if (evaluated.ok) {
|
|
211
|
+
name = evaluated.value;
|
|
212
|
+
} else {
|
|
213
|
+
unknowns.push(
|
|
214
|
+
schemaUnknown('SQL name of column "' + input.property + '" is not static', source),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
builder = chain.builder?.name ?? chain.calleeName;
|
|
220
|
+
name = literalString(chain.args[0]) ?? input.property;
|
|
221
|
+
unknowns.push(
|
|
222
|
+
schemaUnknown('column builder "' + builder + '" is outside the documented DSL', source),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let notNull = false;
|
|
227
|
+
let primaryKey = false;
|
|
228
|
+
let unique = false;
|
|
229
|
+
let columnDefault: ColumnDefaultFact | null = null;
|
|
230
|
+
let array = false;
|
|
231
|
+
if (chain !== null) {
|
|
232
|
+
for (const method of chain.methods) {
|
|
233
|
+
switch (method.name) {
|
|
234
|
+
case "notNull":
|
|
235
|
+
notNull = true;
|
|
236
|
+
break;
|
|
237
|
+
case "primaryKey":
|
|
238
|
+
primaryKey = true;
|
|
239
|
+
break;
|
|
240
|
+
case "unique":
|
|
241
|
+
unique = true;
|
|
242
|
+
break;
|
|
243
|
+
case "default":
|
|
244
|
+
columnDefault = readDefault(method.args[0], context);
|
|
245
|
+
break;
|
|
246
|
+
case "defaultRandom":
|
|
247
|
+
columnDefault = { kind: "random", value: null };
|
|
248
|
+
break;
|
|
249
|
+
case "defaultNow":
|
|
250
|
+
columnDefault = { kind: "now", value: null };
|
|
251
|
+
break;
|
|
252
|
+
case "array":
|
|
253
|
+
array = true;
|
|
254
|
+
break;
|
|
255
|
+
case "references": {
|
|
256
|
+
const result = readReference(method, context, input.file);
|
|
257
|
+
reference = result.target;
|
|
258
|
+
unknowns.push(...result.unknowns);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
unknowns.push(
|
|
263
|
+
schemaUnknown(
|
|
264
|
+
'column method ".' + method.name + '" is outside the documented DSL',
|
|
265
|
+
nodeRef(input.file, method.node),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (array && type !== "unknown") type += "[]";
|
|
273
|
+
|
|
274
|
+
const column: ColumnFact = {
|
|
275
|
+
property: input.property,
|
|
276
|
+
name,
|
|
277
|
+
builder,
|
|
278
|
+
type,
|
|
279
|
+
notNull,
|
|
280
|
+
primaryKey,
|
|
281
|
+
unique,
|
|
282
|
+
default: columnDefault,
|
|
283
|
+
source,
|
|
284
|
+
unknown: unknowns,
|
|
285
|
+
};
|
|
286
|
+
return { column, reference, tenant, diagnostics: schemaDiagnostics(unknowns) };
|
|
287
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
identifierName,
|
|
3
|
+
isAstNode,
|
|
4
|
+
memberName,
|
|
5
|
+
nodeArray,
|
|
6
|
+
unwrapExpression,
|
|
7
|
+
type AstNode,
|
|
8
|
+
} from "../ast/node.ts";
|
|
9
|
+
import { diagnostic } from "../diagnostics.ts";
|
|
10
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
|
|
11
|
+
import { sourceRef } from "../source.ts";
|
|
12
|
+
import type { Diagnostic, Severity, SourceRef, UnknownFact } from "../types.ts";
|
|
13
|
+
|
|
14
|
+
/** Package a DSL symbol came from; calls are classified by import specifier, never by name. */
|
|
15
|
+
export type DslPackage = "pg-core" | "drizzle-orm" | "facilis-db";
|
|
16
|
+
|
|
17
|
+
export interface DslSymbol {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly pkg: DslPackage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolves a callee expression to a whitelisted DSL symbol imported from a known package. */
|
|
23
|
+
export type DslResolver = (callee: unknown) => DslSymbol | null;
|
|
24
|
+
|
|
25
|
+
function packageOf(specifier: string): DslPackage | null {
|
|
26
|
+
if (specifier === "drizzle-orm/pg-core" || specifier.startsWith("drizzle-orm/pg-core/")) {
|
|
27
|
+
return "pg-core";
|
|
28
|
+
}
|
|
29
|
+
if (specifier === "drizzle-orm" || specifier.startsWith("drizzle-orm/")) return "drizzle-orm";
|
|
30
|
+
if (specifier === "@facilis.dev/db" || specifier.startsWith("@facilis.dev/db/")) return "facilis-db";
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Package classification for an aliased specifier that resolved into an installed package. */
|
|
35
|
+
function packageOfResolved(path: string): DslPackage | null {
|
|
36
|
+
if (path.includes("node_modules/drizzle-orm/pg-core/")) return "pg-core";
|
|
37
|
+
if (path.includes("node_modules/drizzle-orm/")) return "drizzle-orm";
|
|
38
|
+
if (path.includes("node_modules/@facilis.dev/db/")) return "facilis-db";
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDslResolver(graph: ModuleGraph, file: ProjectFile): DslResolver {
|
|
43
|
+
const named = new Map<string, DslSymbol>();
|
|
44
|
+
const namespaces = new Map<string, DslPackage>();
|
|
45
|
+
for (const imported of file.module.imports) {
|
|
46
|
+
let pkg = packageOf(imported.specifier);
|
|
47
|
+
if (pkg === null) {
|
|
48
|
+
const resolved = graph.resolve(file.path, imported.specifier);
|
|
49
|
+
if (resolved !== null) pkg = packageOfResolved(resolved);
|
|
50
|
+
}
|
|
51
|
+
if (pkg === null) continue;
|
|
52
|
+
if (imported.kind === "namespace") {
|
|
53
|
+
namespaces.set(imported.local, pkg);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (imported.kind !== "named" || imported.imported === null) continue;
|
|
57
|
+
named.set(imported.local, { name: imported.imported, pkg });
|
|
58
|
+
}
|
|
59
|
+
return (callee) => {
|
|
60
|
+
const node = unwrapExpression(callee);
|
|
61
|
+
if (node === undefined) return null;
|
|
62
|
+
if (node.type === "Identifier") {
|
|
63
|
+
const local = identifierName(node);
|
|
64
|
+
return local === undefined ? null : (named.get(local) ?? null);
|
|
65
|
+
}
|
|
66
|
+
if (node.type !== "MemberExpression" || node.computed === true) return null;
|
|
67
|
+
const namespace = identifierName(unwrapExpression(node.object));
|
|
68
|
+
const pkg = namespace === undefined ? undefined : namespaces.get(namespace);
|
|
69
|
+
const name = memberName(node);
|
|
70
|
+
if (pkg === undefined || name === undefined) return null;
|
|
71
|
+
return { name, pkg };
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Variable declarators at module top level, including `export const ...`. */
|
|
76
|
+
export function topLevelDeclarators(program: AstNode): AstNode[] {
|
|
77
|
+
const out: AstNode[] = [];
|
|
78
|
+
for (const statement of nodeArray(program, "body")) {
|
|
79
|
+
const declaration =
|
|
80
|
+
statement.type === "VariableDeclaration"
|
|
81
|
+
? statement
|
|
82
|
+
: statement.type === "ExportNamedDeclaration" && isAstNode(statement.declaration)
|
|
83
|
+
? statement.declaration
|
|
84
|
+
: undefined;
|
|
85
|
+
if (declaration === undefined || declaration.type !== "VariableDeclaration") continue;
|
|
86
|
+
out.push(...nodeArray(declaration, "declarations"));
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The expression a callback returns; supports `() => x` and `() => { return x; }`. */
|
|
92
|
+
export function callbackExpression(callback: AstNode | undefined): AstNode | undefined {
|
|
93
|
+
const fn = unwrapExpression(callback);
|
|
94
|
+
if (fn === undefined) return undefined;
|
|
95
|
+
if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return undefined;
|
|
96
|
+
const body = unwrapExpression(fn.body);
|
|
97
|
+
if (body === undefined || body.type !== "BlockStatement") return body;
|
|
98
|
+
const returns = nodeArray(body, "body").filter(
|
|
99
|
+
(statement) => statement.type === "ReturnStatement",
|
|
100
|
+
);
|
|
101
|
+
const statement = returns.length === 1 ? returns[0] : undefined;
|
|
102
|
+
return statement === undefined ? undefined : unwrapExpression(statement.argument);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function nodeRef(file: ProjectFile, node: AstNode | undefined): SourceRef {
|
|
106
|
+
return sourceRef(file.path, file.source, typeof node?.start === "number" ? node.start : 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Schema DSL files live under the project's `db/` directory (root-aware). */
|
|
110
|
+
export function isSchemaFile(graph: ModuleGraph, path: string): boolean {
|
|
111
|
+
const prefix = graph.root.length === 0 ? "db/" : graph.root + "/db/";
|
|
112
|
+
return path.startsWith(prefix);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** View declarations live under `db/views/` (SPEC §11.2, D-157). */
|
|
116
|
+
export function isViewFile(graph: ModuleGraph, path: string): boolean {
|
|
117
|
+
const prefix = graph.root.length === 0 ? "db/views/" : graph.root + "/db/views/";
|
|
118
|
+
return path.startsWith(prefix);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function schemaUnknown(reason: string, source: SourceRef): UnknownFact {
|
|
122
|
+
return { code: "schema-static", reason, source };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function severityOf(code: string): Severity {
|
|
126
|
+
if (code === "fk-target-known") return "warning";
|
|
127
|
+
if (code === "unknown") return "info";
|
|
128
|
+
return "error";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Unknowns are also diagnostics: the fact model never hides what the interpreter could not read. */
|
|
132
|
+
export function schemaDiagnostics(unknowns: readonly UnknownFact[]): Diagnostic[] {
|
|
133
|
+
return unknowns.map((unknown) =>
|
|
134
|
+
diagnostic(unknown.code, severityOf(unknown.code), unknown.reason, unknown.source),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DDL `@facilis.dev/db`'s `fileLinks` expands to, expressed as analyzer facts (D-053).
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the call-site scan: this module mirrors the framework's fixed table shape,
|
|
5
|
+
* while `schema-file-links.ts` interprets the project's arguments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ColumnDefaultFact, ColumnFact, PolicyFact, SourceRef } from "../types.ts";
|
|
9
|
+
import type { TableDraft } from "./schema-tables.ts";
|
|
10
|
+
|
|
11
|
+
const FACILIS_FILES = "facilis_files";
|
|
12
|
+
export const FACILIS_FILES_BINDING = "facilisFiles";
|
|
13
|
+
|
|
14
|
+
interface ColumnInput {
|
|
15
|
+
readonly property: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly builder: string;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
readonly notNull: boolean;
|
|
20
|
+
readonly primaryKey: boolean;
|
|
21
|
+
readonly default: ColumnDefaultFact | null;
|
|
22
|
+
readonly source: SourceRef;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function column(input: ColumnInput): ColumnFact {
|
|
26
|
+
return { ...input, unique: false, unknown: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The generated link table's columns; `parentColumn` is the `column` option. */
|
|
30
|
+
export function linkColumns(parentColumn: string, source: SourceRef): ColumnFact[] {
|
|
31
|
+
return [
|
|
32
|
+
column({
|
|
33
|
+
property: "id",
|
|
34
|
+
name: "id",
|
|
35
|
+
builder: "uuid",
|
|
36
|
+
type: "uuid",
|
|
37
|
+
notNull: true,
|
|
38
|
+
primaryKey: true,
|
|
39
|
+
default: { kind: "random", value: null },
|
|
40
|
+
source,
|
|
41
|
+
}),
|
|
42
|
+
column({
|
|
43
|
+
property: "parentId",
|
|
44
|
+
name: parentColumn,
|
|
45
|
+
builder: "uuid",
|
|
46
|
+
type: "uuid",
|
|
47
|
+
notNull: true,
|
|
48
|
+
primaryKey: false,
|
|
49
|
+
default: null,
|
|
50
|
+
source,
|
|
51
|
+
}),
|
|
52
|
+
column({
|
|
53
|
+
property: "fileId",
|
|
54
|
+
name: "file_id",
|
|
55
|
+
builder: "uuid",
|
|
56
|
+
type: "uuid",
|
|
57
|
+
notNull: true,
|
|
58
|
+
primaryKey: false,
|
|
59
|
+
default: null,
|
|
60
|
+
source,
|
|
61
|
+
}),
|
|
62
|
+
column({
|
|
63
|
+
property: "tenantId",
|
|
64
|
+
name: "tenant_id",
|
|
65
|
+
builder: "tenantColumn",
|
|
66
|
+
type: "uuid",
|
|
67
|
+
notNull: true,
|
|
68
|
+
primaryKey: false,
|
|
69
|
+
default: null,
|
|
70
|
+
source,
|
|
71
|
+
}),
|
|
72
|
+
column({
|
|
73
|
+
property: "createdAt",
|
|
74
|
+
name: "created_at",
|
|
75
|
+
builder: "timestamp",
|
|
76
|
+
type: "timestamp with time zone",
|
|
77
|
+
notNull: true,
|
|
78
|
+
primaryKey: false,
|
|
79
|
+
default: { kind: "now", value: null },
|
|
80
|
+
source,
|
|
81
|
+
}),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function linkPolicies(source: SourceRef): PolicyFact[] {
|
|
86
|
+
return [
|
|
87
|
+
{ name: "tenant_isolation", command: "ALL", restrictive: true, source, unknown: [] },
|
|
88
|
+
{ name: "parent_access", command: "ALL", restrictive: false, source, unknown: [] },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `facilis_files` as an FK target only. The link DDL references it, but a release-ref map
|
|
94
|
+
* does not carry `@facilis.dev/db`'s source, so this lookup-only draft lets the FK resolve; it is
|
|
95
|
+
* never emitted as one of the project's tables.
|
|
96
|
+
*/
|
|
97
|
+
export function facilisFilesDraft(source: SourceRef): TableDraft {
|
|
98
|
+
return {
|
|
99
|
+
binding: FACILIS_FILES_BINDING,
|
|
100
|
+
references: [],
|
|
101
|
+
fact: {
|
|
102
|
+
name: FACILIS_FILES,
|
|
103
|
+
binding: FACILIS_FILES_BINDING,
|
|
104
|
+
columns: [
|
|
105
|
+
column({
|
|
106
|
+
property: "id",
|
|
107
|
+
name: "id",
|
|
108
|
+
builder: "uuid",
|
|
109
|
+
type: "uuid",
|
|
110
|
+
notNull: true,
|
|
111
|
+
primaryKey: true,
|
|
112
|
+
default: { kind: "random", value: null },
|
|
113
|
+
source,
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
primaryKey: ["id"],
|
|
117
|
+
foreignKeys: [],
|
|
118
|
+
policies: [],
|
|
119
|
+
rlsEnabled: false,
|
|
120
|
+
scope: null,
|
|
121
|
+
source,
|
|
122
|
+
unknown: [],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|