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