@facilis.dev/analyze 0.0.0-stage → 0.1.1
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 +162 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +17 -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-text.d.ts +28 -0
- package/dist/passes/jsx-text.js +121 -0
- package/dist/passes/jsx.d.ts +24 -0
- package/dist/passes/jsx.js +135 -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-intrinsics.d.ts +18 -0
- package/dist/passes/screen-intrinsics.js +21 -0
- package/dist/passes/screen-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +174 -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 +144 -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 +194 -0
- package/src/index.ts +26 -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-text.ts +126 -0
- package/src/passes/jsx.ts +142 -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-intrinsics.ts +29 -0
- package/src/passes/screen-preview.ts +295 -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 +176 -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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the analyzer passes: the static object reader (§4 of the AST DSL
|
|
3
|
+
* contract) and deterministic diagnostic ordering.
|
|
4
|
+
*
|
|
5
|
+
* The reader is deliberately partial: every property is evaluated on its own so one computed
|
|
6
|
+
* value cannot hide its siblings. It never executes project code and it never defaults an
|
|
7
|
+
* unreadable value - the caller decides whether the unknown is a security error.
|
|
8
|
+
*/
|
|
9
|
+
import { identifierName, objectEntries, propertyName, propertyValue, unwrapExpression, } from "../ast/node.js";
|
|
10
|
+
import { declarationNames, defaultExportDeclaration, localInitializer } from "../module-info.js";
|
|
11
|
+
import { sourceRef } from "../source.js";
|
|
12
|
+
import { evaluateStatic } from "../static-eval.js";
|
|
13
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
14
|
+
export function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
export function projectPath(root, path) {
|
|
18
|
+
return root.length === 0 ? path : root + "/" + path;
|
|
19
|
+
}
|
|
20
|
+
/** Project-relative path with the graph root stripped (the opposite of projectPath). */
|
|
21
|
+
export function relativePath(root, path) {
|
|
22
|
+
return root.length === 0 || !path.startsWith(root + "/") ? path : path.slice(root.length + 1);
|
|
23
|
+
}
|
|
24
|
+
export function refOf(context, node) {
|
|
25
|
+
const file = context.graph.file(context.file);
|
|
26
|
+
const offset = node !== undefined && typeof node.start === "number" ? node.start : 0;
|
|
27
|
+
return sourceRef(context.file, file?.source ?? "", offset);
|
|
28
|
+
}
|
|
29
|
+
function objectLiteral(node, context, depth = 0) {
|
|
30
|
+
if (node === undefined || depth > MAX_RESOLVE_DEPTH)
|
|
31
|
+
return undefined;
|
|
32
|
+
const unwrapped = unwrapExpression(node);
|
|
33
|
+
if (unwrapped === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (unwrapped.type === "ObjectExpression")
|
|
36
|
+
return unwrapped;
|
|
37
|
+
const file = context.graph.file(context.file);
|
|
38
|
+
const name = identifierName(unwrapped);
|
|
39
|
+
if (file === undefined || name === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
return objectLiteral(localInitializer(file.program, name), context, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
/** Partial, non-executing reader for object literals in the static subset. */
|
|
44
|
+
export function readStaticObject(node, context, options) {
|
|
45
|
+
const fields = new Map();
|
|
46
|
+
const unknown = [];
|
|
47
|
+
if (node === undefined)
|
|
48
|
+
return { fields, unknown };
|
|
49
|
+
const record = (reason, at, structural) => {
|
|
50
|
+
if (structural || options.report) {
|
|
51
|
+
unknown.push({ code: options.code, reason, source: refOf(context, at) });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const literal = objectLiteral(node, context);
|
|
55
|
+
if (literal !== undefined) {
|
|
56
|
+
for (const entry of objectEntries(literal)) {
|
|
57
|
+
if (entry.type === "SpreadElement") {
|
|
58
|
+
record("spread in an object literal cannot be read statically", entry, true);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const key = propertyName(entry);
|
|
62
|
+
if (key === undefined) {
|
|
63
|
+
record("computed object key cannot be read statically", entry, true);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const valueNode = propertyValue(entry);
|
|
67
|
+
const evaluated = evaluateStatic(valueNode, context);
|
|
68
|
+
if (evaluated.ok) {
|
|
69
|
+
fields.set(key, { value: evaluated.value, ok: true, node: valueNode, reason: null });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
fields.set(key, { value: undefined, ok: false, node: valueNode, reason: evaluated.reason });
|
|
73
|
+
record(evaluated.reason, valueNode ?? entry, false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { fields, unknown };
|
|
77
|
+
}
|
|
78
|
+
const evaluated = evaluateStatic(node, context);
|
|
79
|
+
if (evaluated.ok && isRecord(evaluated.value)) {
|
|
80
|
+
for (const [key, value] of Object.entries(evaluated.value)) {
|
|
81
|
+
fields.set(key, { value, ok: true, node: undefined, reason: null });
|
|
82
|
+
}
|
|
83
|
+
return { fields, unknown };
|
|
84
|
+
}
|
|
85
|
+
record(evaluated.ok ? "value is not an object" : evaluated.reason, node, true);
|
|
86
|
+
return { fields, unknown };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The default export, unwrapped and resolved through a same-file const or a
|
|
90
|
+
* `export { handler as default }` statement.
|
|
91
|
+
*/
|
|
92
|
+
export function defaultExportExpression(file) {
|
|
93
|
+
const declaration = defaultExportDeclaration(file.program);
|
|
94
|
+
if (declaration !== undefined) {
|
|
95
|
+
const unwrapped = unwrapExpression(declaration);
|
|
96
|
+
const name = unwrapped?.type === "Identifier" ? identifierName(unwrapped) : undefined;
|
|
97
|
+
return name === undefined ? unwrapped : unwrapExpression(localInitializer(file.program, name));
|
|
98
|
+
}
|
|
99
|
+
const entry = file.module.exports.find((item) => item.exported === "default" && item.local !== null);
|
|
100
|
+
if (entry === undefined || entry.local === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
return unwrapExpression(localInitializer(file.program, entry.local));
|
|
103
|
+
}
|
|
104
|
+
/** A top-level function/class declaration by name (localInitializer covers variables only). */
|
|
105
|
+
export function findDeclaration(program, name) {
|
|
106
|
+
for (const raw of Array.isArray(program.body) ? program.body : []) {
|
|
107
|
+
if (typeof raw !== "object" || raw === null)
|
|
108
|
+
continue;
|
|
109
|
+
const statement = raw;
|
|
110
|
+
const declaration = statement.type === "ExportNamedDeclaration" &&
|
|
111
|
+
statement.declaration !== null &&
|
|
112
|
+
typeof statement.declaration === "object"
|
|
113
|
+
? statement.declaration
|
|
114
|
+
: statement;
|
|
115
|
+
if ((declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") &&
|
|
116
|
+
identifierName(declaration.id) === name) {
|
|
117
|
+
return declaration;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/** True when the module declares the name itself: a local shadow invalidates an import. */
|
|
123
|
+
export function isLocallyDeclared(file, name) {
|
|
124
|
+
const body = Array.isArray(file.program.body) ? file.program.body : [];
|
|
125
|
+
for (const raw of body) {
|
|
126
|
+
if (typeof raw !== "object" || raw === null)
|
|
127
|
+
continue;
|
|
128
|
+
const statement = raw;
|
|
129
|
+
const declaration = statement.type === "ExportNamedDeclaration" &&
|
|
130
|
+
statement.declaration !== null &&
|
|
131
|
+
typeof statement.declaration === "object"
|
|
132
|
+
? statement.declaration
|
|
133
|
+
: statement;
|
|
134
|
+
if (declarationNames(declaration).includes(name))
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/** Stable diagnostic order (file, line, code, message) so snapshots never flap. */
|
|
140
|
+
export function sortDiagnostics(diagnostics) {
|
|
141
|
+
return [...diagnostics].toSorted((left, right) => {
|
|
142
|
+
const fileOrder = (left.source?.file ?? "").localeCompare(right.source?.file ?? "");
|
|
143
|
+
if (fileOrder !== 0)
|
|
144
|
+
return fileOrder;
|
|
145
|
+
const lineOrder = (left.source?.line ?? 0) - (right.source?.line ?? 0);
|
|
146
|
+
if (lineOrder !== 0)
|
|
147
|
+
return lineOrder;
|
|
148
|
+
const codeOrder = left.code.localeCompare(right.code);
|
|
149
|
+
return codeOrder !== 0 ? codeOrder : left.message.localeCompare(right.message);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `db/views/**` -> ViewFacts (D-157, SPEC §11.2).
|
|
3
|
+
*
|
|
4
|
+
* A view is declared one per file with `defineView({ name, columns, query, tables?,
|
|
5
|
+
* permission? })` from @facilis.dev/db (D-097/D-098). The route-relations pass resolves a view
|
|
6
|
+
* object used in `from(...)` to this fact, so a route declares the view's SQL name in
|
|
7
|
+
* `dbTables` instead of its base tables. D-160 adds the view nodes and their edges to the
|
|
8
|
+
* project graph; this pass stays facts-only.
|
|
9
|
+
*
|
|
10
|
+
* The declaration is read through the bounded static subset; an unreadable name/tables/
|
|
11
|
+
* permission is an explicit unknown, never a guessed value.
|
|
12
|
+
*/
|
|
13
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
14
|
+
import type { Diagnostic, ViewFact } from "../types.js";
|
|
15
|
+
export interface ViewScan {
|
|
16
|
+
readonly views: readonly ViewFact[];
|
|
17
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
18
|
+
}
|
|
19
|
+
/** Read `db/views/**` into view facts. */
|
|
20
|
+
export declare function buildViewFacts(graph: ModuleGraph): ViewScan;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `db/views/**` -> ViewFacts (D-157, SPEC §11.2).
|
|
3
|
+
*
|
|
4
|
+
* A view is declared one per file with `defineView({ name, columns, query, tables?,
|
|
5
|
+
* permission? })` from @facilis.dev/db (D-097/D-098). The route-relations pass resolves a view
|
|
6
|
+
* object used in `from(...)` to this fact, so a route declares the view's SQL name in
|
|
7
|
+
* `dbTables` instead of its base tables. D-160 adds the view nodes and their edges to the
|
|
8
|
+
* project graph; this pass stays facts-only.
|
|
9
|
+
*
|
|
10
|
+
* The declaration is read through the bounded static subset; an unreadable name/tables/
|
|
11
|
+
* permission is an explicit unknown, never a guessed value.
|
|
12
|
+
*/
|
|
13
|
+
import { callArguments, identifierName, unwrapExpression } from "../ast/node.js";
|
|
14
|
+
import { createDslResolver, isViewFile, nodeRef, schemaDiagnostics, topLevelDeclarators, } from "./schema-dsl.js";
|
|
15
|
+
import { readStaticObject } from "./static-read.js";
|
|
16
|
+
function viewUnknown(reason, source) {
|
|
17
|
+
return { code: "view-static", reason, source };
|
|
18
|
+
}
|
|
19
|
+
function stringArray(value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return null;
|
|
22
|
+
return value.every((entry) => typeof entry === "string") ? [...value] : null;
|
|
23
|
+
}
|
|
24
|
+
/** The `defineView(...)` call an initializer holds, when it is the @facilis.dev/db DSL. */
|
|
25
|
+
function viewCall(init, resolver) {
|
|
26
|
+
const expression = unwrapExpression(init);
|
|
27
|
+
if (expression?.type !== "CallExpression")
|
|
28
|
+
return null;
|
|
29
|
+
const symbol = resolver(expression.callee);
|
|
30
|
+
return symbol?.pkg === "facilis-db" && symbol.name === "defineView" ? expression : null;
|
|
31
|
+
}
|
|
32
|
+
function buildViewDraft(graph, file, declarator, call) {
|
|
33
|
+
const source = nodeRef(file, call);
|
|
34
|
+
const binding = identifierName(declarator.id) ?? null;
|
|
35
|
+
const unknowns = [];
|
|
36
|
+
const options = readStaticObject(callArguments(call)[0], { graph, file: file.path }, { code: "view-static", report: false });
|
|
37
|
+
unknowns.push(...options.unknown);
|
|
38
|
+
const nameField = options.fields.get("name");
|
|
39
|
+
let name = binding ?? "unknown";
|
|
40
|
+
if (nameField !== undefined && nameField.ok && typeof nameField.value === "string") {
|
|
41
|
+
if (nameField.value.length > 0)
|
|
42
|
+
name = nameField.value;
|
|
43
|
+
else
|
|
44
|
+
unknowns.push(viewUnknown("view name is not a static string", source));
|
|
45
|
+
}
|
|
46
|
+
else if (nameField !== undefined && !nameField.ok) {
|
|
47
|
+
unknowns.push(viewUnknown("view name is missing or not statically readable", source));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
unknowns.push(viewUnknown("view name is missing or not a static string", source));
|
|
51
|
+
}
|
|
52
|
+
let tables = [];
|
|
53
|
+
const tablesField = options.fields.get("tables");
|
|
54
|
+
if (tablesField !== undefined) {
|
|
55
|
+
const read = tablesField.ok ? stringArray(tablesField.value) : null;
|
|
56
|
+
if (read === null) {
|
|
57
|
+
unknowns.push(viewUnknown("view tables are not a static string[]", source));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
tables = read;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const permissionField = options.fields.get("permission");
|
|
64
|
+
let permission = null;
|
|
65
|
+
let access = "invoker";
|
|
66
|
+
if (permissionField !== undefined) {
|
|
67
|
+
if (permissionField.ok && typeof permissionField.value === "string") {
|
|
68
|
+
permission = permissionField.value;
|
|
69
|
+
access = "definer";
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// We cannot tell an invoker declaration from a definer one: keep the mode unknown.
|
|
73
|
+
access = null;
|
|
74
|
+
unknowns.push(viewUnknown("view permission is not a static string", source));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
view: { name, binding, tables, permission, access, source, unknown: unknowns },
|
|
79
|
+
diagnostics: schemaDiagnostics(unknowns),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Read `db/views/**` into view facts. */
|
|
83
|
+
export function buildViewFacts(graph) {
|
|
84
|
+
const files = [...graph.files.values()]
|
|
85
|
+
.filter((file) => isViewFile(graph, file.path))
|
|
86
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
87
|
+
const views = [];
|
|
88
|
+
const diagnostics = [];
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const resolver = createDslResolver(graph, file);
|
|
91
|
+
for (const declarator of topLevelDeclarators(file.program)) {
|
|
92
|
+
const call = viewCall(declarator.init, resolver);
|
|
93
|
+
if (call === null)
|
|
94
|
+
continue;
|
|
95
|
+
const draft = buildViewDraft(graph, file, declarator, call);
|
|
96
|
+
views.push(draft.view);
|
|
97
|
+
diagnostics.push(...draft.diagnostics);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { views, diagnostics };
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow table surface, derived from the jobs its steps reference (user decision: derived,
|
|
3
|
+
* never declared). A workflow declares no tables of its own: the union of the referenced jobs'
|
|
4
|
+
* `dbTables` is what the workflow can reach, and the runtime inherits each job's guard.
|
|
5
|
+
*
|
|
6
|
+
* Approval steps contribute nothing. A referenced job with an unreadable `dbTables` makes the
|
|
7
|
+
* union explicitly unknown; a referenced job with no definition keeps its name visible in
|
|
8
|
+
* `unknownJobs` and is a warning, never a silent hole.
|
|
9
|
+
*/
|
|
10
|
+
import type { Diagnostic, WorkflowFact } from "../types.js";
|
|
11
|
+
import type { JobFact } from "../types-jobs.js";
|
|
12
|
+
import type { WorkflowTablesFact } from "../types-workflows.js";
|
|
13
|
+
export interface WorkflowTablesResult {
|
|
14
|
+
readonly facts: readonly WorkflowTablesFact[];
|
|
15
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
/** Derive every workflow's table surface from its job steps. */
|
|
18
|
+
export declare function buildWorkflowTableFacts(workflows: readonly WorkflowFact[], jobs: readonly JobFact[]): WorkflowTablesResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow table surface, derived from the jobs its steps reference (user decision: derived,
|
|
3
|
+
* never declared). A workflow declares no tables of its own: the union of the referenced jobs'
|
|
4
|
+
* `dbTables` is what the workflow can reach, and the runtime inherits each job's guard.
|
|
5
|
+
*
|
|
6
|
+
* Approval steps contribute nothing. A referenced job with an unreadable `dbTables` makes the
|
|
7
|
+
* union explicitly unknown; a referenced job with no definition keeps its name visible in
|
|
8
|
+
* `unknownJobs` and is a warning, never a silent hole.
|
|
9
|
+
*/
|
|
10
|
+
import { warning } from "../diagnostics.js";
|
|
11
|
+
import { sortDiagnostics } from "./static-read.js";
|
|
12
|
+
/** One job definition per name; the manifest enforces uniqueness, first wins here. */
|
|
13
|
+
function jobRegistry(jobs) {
|
|
14
|
+
const byName = new Map();
|
|
15
|
+
for (const job of jobs) {
|
|
16
|
+
if (!byName.has(job.name))
|
|
17
|
+
byName.set(job.name, job);
|
|
18
|
+
}
|
|
19
|
+
return byName;
|
|
20
|
+
}
|
|
21
|
+
/** Derive every workflow's table surface from its job steps. */
|
|
22
|
+
export function buildWorkflowTableFacts(workflows, jobs) {
|
|
23
|
+
const registry = jobRegistry(jobs);
|
|
24
|
+
const facts = [];
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
for (const workflow of workflows) {
|
|
27
|
+
const tables = new Set();
|
|
28
|
+
const unknownJobs = new Set();
|
|
29
|
+
const missingAt = new Map();
|
|
30
|
+
let unknown = false;
|
|
31
|
+
for (const step of workflow.steps) {
|
|
32
|
+
if (step.kind !== "job")
|
|
33
|
+
continue;
|
|
34
|
+
// A job step whose name is unreadable is already a workflow-definition unknown, but the
|
|
35
|
+
// derived surface must not pretend it is complete.
|
|
36
|
+
if (step.name === null) {
|
|
37
|
+
unknown = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const job = registry.get(step.name);
|
|
41
|
+
if (job === undefined) {
|
|
42
|
+
unknownJobs.add(step.name);
|
|
43
|
+
if (!missingAt.has(step.name))
|
|
44
|
+
missingAt.set(step.name, step.source);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (job.dbTables === null) {
|
|
48
|
+
unknown = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const table of job.dbTables)
|
|
52
|
+
tables.add(table);
|
|
53
|
+
}
|
|
54
|
+
for (const name of [...unknownJobs].toSorted()) {
|
|
55
|
+
diagnostics.push(warning("workflow-job-unknown", 'workflow "' +
|
|
56
|
+
workflow.name +
|
|
57
|
+
'" references job "' +
|
|
58
|
+
name +
|
|
59
|
+
'" but no definition exists in server/jobs', missingAt.get(name) ?? workflow.source));
|
|
60
|
+
}
|
|
61
|
+
facts.push({
|
|
62
|
+
name: workflow.name,
|
|
63
|
+
sourceFile: workflow.sourceFile,
|
|
64
|
+
source: workflow.source,
|
|
65
|
+
tables: [...tables].toSorted(),
|
|
66
|
+
unknownJobs: [...unknownJobs].toSorted(),
|
|
67
|
+
unknown,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { facts, diagnostics: sortDiagnostics(diagnostics) };
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModuleGraph } from "../module-graph.js";
|
|
2
|
+
import type { Diagnostic, WorkflowFact } from "../types.js";
|
|
3
|
+
export interface WorkflowResult {
|
|
4
|
+
readonly workflows: readonly WorkflowFact[];
|
|
5
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
6
|
+
}
|
|
7
|
+
/** Read every workflow definition the project declares; invalid files become warnings. */
|
|
8
|
+
export declare function buildWorkflowFacts(graph: ModuleGraph): WorkflowResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// `server/workflows/**` definitions -> WorkflowFacts (docs/WORKFLOWS.md §6, D-135).
|
|
2
|
+
//
|
|
3
|
+
// The default export is the only source: a `defineWorkflow()` call from `@facilis.dev/core`
|
|
4
|
+
// whose argument is an object literal. Resolvers (`input`, `params`) are functions, so the
|
|
5
|
+
// reader takes the static fields one by one instead of evaluating the whole definition.
|
|
6
|
+
import { callArguments, identifierName, nodeArray, objectEntries, propertyName, propertyValue, unwrapExpression, } from "../ast/node.js";
|
|
7
|
+
import { warning } from "../diagnostics.js";
|
|
8
|
+
import { sourceRef } from "../source.js";
|
|
9
|
+
import { evaluateStatic } from "../static-eval.js";
|
|
10
|
+
import { defaultExportExpression, isLocallyDeclared, isRecord, relativePath, sortDiagnostics, } from "./static-read.js";
|
|
11
|
+
const CORE_SPECIFIER = "@facilis.dev/core";
|
|
12
|
+
const WORKFLOW_DIR = "server/workflows/";
|
|
13
|
+
const STATIC_CODE = "workflow-definition-static";
|
|
14
|
+
/** Read every workflow definition the project declares; invalid files become warnings. */
|
|
15
|
+
export function buildWorkflowFacts(graph) {
|
|
16
|
+
const workflows = [];
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const file of graph.files.values()) {
|
|
19
|
+
const relative = relativePath(graph.root, file.path);
|
|
20
|
+
if (!relative.startsWith(WORKFLOW_DIR) || relative.endsWith(".test.ts"))
|
|
21
|
+
continue;
|
|
22
|
+
scanFile(graph, file, workflows, diagnostics);
|
|
23
|
+
}
|
|
24
|
+
return { workflows, diagnostics: sortDiagnostics(diagnostics) };
|
|
25
|
+
}
|
|
26
|
+
function scanFile(graph, file, workflows, diagnostics) {
|
|
27
|
+
const context = { graph, file: file.path };
|
|
28
|
+
const expression = defaultExportExpression(file);
|
|
29
|
+
const ref = sourceRef(file.path, file.source, expression?.start ?? 0);
|
|
30
|
+
const problem = definitionProblem(file, expression);
|
|
31
|
+
if (problem !== null || expression === undefined || expression.type !== "CallExpression") {
|
|
32
|
+
diagnostics.push(warning("workflow-definition", problem ?? "the default export is not a defineWorkflow() call", ref));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const literal = objectLiteral(callArguments(expression)[0]);
|
|
36
|
+
if (literal === undefined) {
|
|
37
|
+
diagnostics.push(warning("workflow-definition", "the definition argument is not an object literal", ref));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const fields = fieldMap(literal);
|
|
41
|
+
const name = staticString(fields.get("name"), context);
|
|
42
|
+
if (name === null) {
|
|
43
|
+
diagnostics.push(warning("workflow-definition", "the workflow name cannot be read statically", ref));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const unknown = [];
|
|
47
|
+
const steps = readSteps(fields.get("steps"), context, file.path, file.source, unknown);
|
|
48
|
+
workflows.push({ name, sourceFile: file.path, source: ref, steps, unknown });
|
|
49
|
+
}
|
|
50
|
+
/** The default export must be the `defineWorkflow` import from `@facilis.dev/core`. */
|
|
51
|
+
function definitionProblem(file, expression) {
|
|
52
|
+
if (expression === undefined)
|
|
53
|
+
return "the workflow file has no default export";
|
|
54
|
+
if (expression.type !== "CallExpression") {
|
|
55
|
+
return "the default export is not a defineWorkflow() call";
|
|
56
|
+
}
|
|
57
|
+
const callee = unwrapExpression(expression.callee);
|
|
58
|
+
const name = callee?.type === "Identifier" ? identifierName(callee) : undefined;
|
|
59
|
+
if (name === undefined)
|
|
60
|
+
return "the defineWorkflow callee cannot be resolved";
|
|
61
|
+
if (isLocallyDeclared(file, name))
|
|
62
|
+
return '"' + name + '" is shadowed by a local declaration';
|
|
63
|
+
const imported = file.module.imports.find((entry) => entry.local === name);
|
|
64
|
+
if (imported === undefined ||
|
|
65
|
+
imported.specifier !== CORE_SPECIFIER ||
|
|
66
|
+
imported.imported !== "defineWorkflow") {
|
|
67
|
+
return "the default export is not the defineWorkflow import from @facilis.dev/core";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function readSteps(node, context, file, source, unknown) {
|
|
72
|
+
const array = node === undefined ? undefined : unwrapExpression(node);
|
|
73
|
+
if (array === undefined || array.type !== "ArrayExpression") {
|
|
74
|
+
unknown.push({
|
|
75
|
+
code: STATIC_CODE,
|
|
76
|
+
reason: "steps must be an array literal",
|
|
77
|
+
source: sourceRef(file, source, array?.start ?? 0),
|
|
78
|
+
});
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const steps = [];
|
|
82
|
+
nodeArray(array, "elements").forEach((element, index) => {
|
|
83
|
+
const ref = sourceRef(file, source, element.start ?? 0);
|
|
84
|
+
const literal = objectLiteral(element);
|
|
85
|
+
const fields = literal === undefined ? new Map() : fieldMap(literal);
|
|
86
|
+
const job = fields.get("job");
|
|
87
|
+
const approval = fields.get("approval");
|
|
88
|
+
if (job !== undefined) {
|
|
89
|
+
const name = staticString(job, context);
|
|
90
|
+
unknownIfNull(unknown, ref, index, "job name", name);
|
|
91
|
+
steps.push({ index, kind: "job", name, permission: null, onRejectStep: null, source: ref });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (approval !== undefined) {
|
|
95
|
+
const approvalLiteral = objectLiteral(approval);
|
|
96
|
+
const approvalFields = approvalLiteral === undefined ? new Map() : fieldMap(approvalLiteral);
|
|
97
|
+
const titleKey = staticString(approvalFields.get("titleKey"), context);
|
|
98
|
+
const permission = staticString(approvalFields.get("permission"), context);
|
|
99
|
+
const onRejectStep = rejectStepOf(approvalFields.get("onReject"), context);
|
|
100
|
+
unknownIfNull(unknown, ref, index, "approval title key", titleKey);
|
|
101
|
+
steps.push({
|
|
102
|
+
index,
|
|
103
|
+
kind: "approval",
|
|
104
|
+
name: titleKey,
|
|
105
|
+
permission,
|
|
106
|
+
onRejectStep,
|
|
107
|
+
source: ref,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
unknown.push({
|
|
112
|
+
code: STATIC_CODE,
|
|
113
|
+
reason: "step " + index + " is neither a job nor an approval",
|
|
114
|
+
source: ref,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return steps;
|
|
118
|
+
}
|
|
119
|
+
function rejectStepOf(node, context) {
|
|
120
|
+
if (node === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const evaluated = evaluateStatic(node, context);
|
|
123
|
+
if (!evaluated.ok || !isRecord(evaluated.value))
|
|
124
|
+
return null;
|
|
125
|
+
const step = evaluated.value["step"];
|
|
126
|
+
return typeof step === "number" && Number.isInteger(step) && step >= 0 ? step : null;
|
|
127
|
+
}
|
|
128
|
+
function unknownIfNull(unknown, ref, index, label, value) {
|
|
129
|
+
if (value !== null)
|
|
130
|
+
return;
|
|
131
|
+
unknown.push({
|
|
132
|
+
code: STATIC_CODE,
|
|
133
|
+
reason: "step " + index + ": " + label + " cannot be read statically",
|
|
134
|
+
source: ref,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function staticString(node, context) {
|
|
138
|
+
if (node === undefined)
|
|
139
|
+
return null;
|
|
140
|
+
const evaluated = evaluateStatic(node, context);
|
|
141
|
+
return evaluated.ok && typeof evaluated.value === "string" && evaluated.value.length > 0
|
|
142
|
+
? evaluated.value
|
|
143
|
+
: null;
|
|
144
|
+
}
|
|
145
|
+
function objectLiteral(node) {
|
|
146
|
+
const value = node === undefined ? undefined : unwrapExpression(node);
|
|
147
|
+
return value?.type === "ObjectExpression" ? value : undefined;
|
|
148
|
+
}
|
|
149
|
+
function fieldMap(literal) {
|
|
150
|
+
const fields = new Map();
|
|
151
|
+
for (const entry of objectEntries(literal)) {
|
|
152
|
+
if (entry.type === "SpreadElement")
|
|
153
|
+
continue;
|
|
154
|
+
const key = propertyName(entry);
|
|
155
|
+
const value = propertyValue(entry);
|
|
156
|
+
if (key !== undefined && value !== undefined)
|
|
157
|
+
fields.set(key, value);
|
|
158
|
+
}
|
|
159
|
+
return fields;
|
|
160
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Project-relative path handling and module specifier resolution (no Node APIs). */
|
|
2
|
+
export interface PathAlias {
|
|
3
|
+
readonly prefix: string;
|
|
4
|
+
readonly target: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PathResolverInput {
|
|
7
|
+
readonly exists: (path: string) => boolean;
|
|
8
|
+
readonly readJson: (path: string) => unknown;
|
|
9
|
+
readonly root: string;
|
|
10
|
+
readonly aliases?: readonly PathAlias[];
|
|
11
|
+
}
|
|
12
|
+
export interface PathResolver {
|
|
13
|
+
resolve(from: string, specifier: string): string | null;
|
|
14
|
+
tryFile(path: string): string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare const SOURCE_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
17
|
+
export declare function normalizePath(path: string): string;
|
|
18
|
+
export declare function dirname(path: string): string;
|
|
19
|
+
export declare function joinPath(base: string, relative: string): string;
|
|
20
|
+
export declare function createPathResolver(input: PathResolverInput): PathResolver;
|