@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.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/analyze.d.ts +17 -0
  4. package/dist/analyze.js +99 -0
  5. package/dist/ast/node.d.ts +29 -0
  6. package/dist/ast/node.js +85 -0
  7. package/dist/ast/walk.d.ts +8 -0
  8. package/dist/ast/walk.js +40 -0
  9. package/dist/diagnostics.d.ts +5 -0
  10. package/dist/diagnostics.js +12 -0
  11. package/dist/fs.d.ts +13 -0
  12. package/dist/fs.js +51 -0
  13. package/dist/graph-edges.d.ts +3 -0
  14. package/dist/graph-edges.js +148 -0
  15. package/dist/graph-ids.d.ts +6 -0
  16. package/dist/graph-ids.js +22 -0
  17. package/dist/graph.d.ts +15 -0
  18. package/dist/graph.js +162 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.js +17 -0
  21. package/dist/json.d.ts +8 -0
  22. package/dist/json.js +31 -0
  23. package/dist/module-graph.d.ts +33 -0
  24. package/dist/module-graph.js +86 -0
  25. package/dist/module-info.d.ts +27 -0
  26. package/dist/module-info.js +143 -0
  27. package/dist/node.d.ts +16 -0
  28. package/dist/node.js +41 -0
  29. package/dist/parser.d.ts +16 -0
  30. package/dist/parser.js +1 -0
  31. package/dist/passes/clients.d.ts +22 -0
  32. package/dist/passes/clients.js +167 -0
  33. package/dist/passes/config.d.ts +16 -0
  34. package/dist/passes/config.js +229 -0
  35. package/dist/passes/file-facts.d.ts +13 -0
  36. package/dist/passes/file-facts.js +130 -0
  37. package/dist/passes/i18n.d.ts +14 -0
  38. package/dist/passes/i18n.js +55 -0
  39. package/dist/passes/job-relations-diagnostics.d.ts +9 -0
  40. package/dist/passes/job-relations-diagnostics.js +38 -0
  41. package/dist/passes/job-relations.d.ts +18 -0
  42. package/dist/passes/job-relations.js +52 -0
  43. package/dist/passes/jobs.d.ts +20 -0
  44. package/dist/passes/jobs.js +133 -0
  45. package/dist/passes/jsx-text.d.ts +28 -0
  46. package/dist/passes/jsx-text.js +121 -0
  47. package/dist/passes/jsx.d.ts +24 -0
  48. package/dist/passes/jsx.js +135 -0
  49. package/dist/passes/path-expression.d.ts +26 -0
  50. package/dist/passes/path-expression.js +265 -0
  51. package/dist/passes/route-config.d.ts +24 -0
  52. package/dist/passes/route-config.js +152 -0
  53. package/dist/passes/route-description.d.ts +13 -0
  54. package/dist/passes/route-description.js +23 -0
  55. package/dist/passes/route-elevation.d.ts +18 -0
  56. package/dist/passes/route-elevation.js +136 -0
  57. package/dist/passes/route-handler.d.ts +27 -0
  58. package/dist/passes/route-handler.js +87 -0
  59. package/dist/passes/route-location.d.ts +16 -0
  60. package/dist/passes/route-location.js +58 -0
  61. package/dist/passes/route-relation-resolve.d.ts +51 -0
  62. package/dist/passes/route-relation-resolve.js +172 -0
  63. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  64. package/dist/passes/route-relations-diagnostics.js +79 -0
  65. package/dist/passes/route-relations-follow.d.ts +16 -0
  66. package/dist/passes/route-relations-follow.js +116 -0
  67. package/dist/passes/route-relations-usage.d.ts +50 -0
  68. package/dist/passes/route-relations-usage.js +44 -0
  69. package/dist/passes/route-relations-walk.d.ts +25 -0
  70. package/dist/passes/route-relations-walk.js +199 -0
  71. package/dist/passes/route-relations.d.ts +17 -0
  72. package/dist/passes/route-relations.js +43 -0
  73. package/dist/passes/route-table.d.ts +21 -0
  74. package/dist/passes/route-table.js +104 -0
  75. package/dist/passes/route-tables.d.ts +10 -0
  76. package/dist/passes/route-tables.js +42 -0
  77. package/dist/passes/routes.d.ts +18 -0
  78. package/dist/passes/routes.js +152 -0
  79. package/dist/passes/schema-column-types.d.ts +9 -0
  80. package/dist/passes/schema-column-types.js +55 -0
  81. package/dist/passes/schema-columns.d.ts +35 -0
  82. package/dist/passes/schema-columns.js +197 -0
  83. package/dist/passes/schema-dsl.d.ts +24 -0
  84. package/dist/passes/schema-dsl.js +117 -0
  85. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  86. package/dist/passes/schema-file-links-ddl.js +106 -0
  87. package/dist/passes/schema-file-links.d.ts +19 -0
  88. package/dist/passes/schema-file-links.js +144 -0
  89. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  90. package/dist/passes/schema-foreign-keys.js +80 -0
  91. package/dist/passes/schema-policies.d.ts +15 -0
  92. package/dist/passes/schema-policies.js +201 -0
  93. package/dist/passes/schema-re-exports.d.ts +5 -0
  94. package/dist/passes/schema-re-exports.js +77 -0
  95. package/dist/passes/schema-relations.d.ts +8 -0
  96. package/dist/passes/schema-relations.js +182 -0
  97. package/dist/passes/schema-scope.d.ts +7 -0
  98. package/dist/passes/schema-scope.js +20 -0
  99. package/dist/passes/schema-tables.d.ts +13 -0
  100. package/dist/passes/schema-tables.js +149 -0
  101. package/dist/passes/schema.d.ts +9 -0
  102. package/dist/passes/schema.js +55 -0
  103. package/dist/passes/screen-components.d.ts +22 -0
  104. package/dist/passes/screen-components.js +143 -0
  105. package/dist/passes/screen-intrinsics.d.ts +18 -0
  106. package/dist/passes/screen-intrinsics.js +21 -0
  107. package/dist/passes/screen-preview.d.ts +29 -0
  108. package/dist/passes/screen-preview.js +174 -0
  109. package/dist/passes/screens.d.ts +12 -0
  110. package/dist/passes/screens.js +278 -0
  111. package/dist/passes/static-read.d.ts +47 -0
  112. package/dist/passes/static-read.js +151 -0
  113. package/dist/passes/views.d.ts +20 -0
  114. package/dist/passes/views.js +101 -0
  115. package/dist/passes/workflow-tables.d.ts +18 -0
  116. package/dist/passes/workflow-tables.js +71 -0
  117. package/dist/passes/workflows.d.ts +8 -0
  118. package/dist/passes/workflows.js +160 -0
  119. package/dist/path-resolver.d.ts +20 -0
  120. package/dist/path-resolver.js +134 -0
  121. package/dist/revision.d.ts +2 -0
  122. package/dist/revision.js +23 -0
  123. package/dist/source.d.ts +4 -0
  124. package/dist/source.js +13 -0
  125. package/dist/static-eval.d.ts +36 -0
  126. package/dist/static-eval.js +215 -0
  127. package/dist/types-graph.d.ts +144 -0
  128. package/dist/types-graph.js +4 -0
  129. package/dist/types-jobs.d.ts +28 -0
  130. package/dist/types-jobs.js +14 -0
  131. package/dist/types-relations.d.ts +49 -0
  132. package/dist/types-relations.js +11 -0
  133. package/dist/types-workflows.d.ts +36 -0
  134. package/dist/types-workflows.js +3 -0
  135. package/dist/types.d.ts +226 -0
  136. package/dist/types.js +1 -0
  137. package/package.json +49 -4
  138. package/src/analyze.ts +119 -0
  139. package/src/ast/node.ts +102 -0
  140. package/src/ast/walk.ts +37 -0
  141. package/src/diagnostics.ts +22 -0
  142. package/src/fs.ts +59 -0
  143. package/src/graph-edges.ts +167 -0
  144. package/src/graph-ids.ts +31 -0
  145. package/src/graph.ts +194 -0
  146. package/src/index.ts +26 -0
  147. package/src/json.ts +34 -0
  148. package/src/module-graph.ts +117 -0
  149. package/src/module-info.ts +162 -0
  150. package/src/node.ts +54 -0
  151. package/src/parser.ts +19 -0
  152. package/src/passes/clients.ts +233 -0
  153. package/src/passes/config.ts +278 -0
  154. package/src/passes/file-facts.ts +142 -0
  155. package/src/passes/i18n.ts +66 -0
  156. package/src/passes/job-relations-diagnostics.ts +60 -0
  157. package/src/passes/job-relations.ts +76 -0
  158. package/src/passes/jobs.ts +181 -0
  159. package/src/passes/jsx-text.ts +126 -0
  160. package/src/passes/jsx.ts +142 -0
  161. package/src/passes/path-expression.ts +297 -0
  162. package/src/passes/route-config.ts +188 -0
  163. package/src/passes/route-description.ts +33 -0
  164. package/src/passes/route-elevation.ts +161 -0
  165. package/src/passes/route-handler.ts +105 -0
  166. package/src/passes/route-location.ts +62 -0
  167. package/src/passes/route-relation-resolve.ts +228 -0
  168. package/src/passes/route-relations-diagnostics.ts +115 -0
  169. package/src/passes/route-relations-follow.ts +146 -0
  170. package/src/passes/route-relations-usage.ts +98 -0
  171. package/src/passes/route-relations-walk.ts +255 -0
  172. package/src/passes/route-relations.ts +62 -0
  173. package/src/passes/route-table.ts +123 -0
  174. package/src/passes/route-tables.ts +50 -0
  175. package/src/passes/routes.ts +188 -0
  176. package/src/passes/schema-column-types.ts +62 -0
  177. package/src/passes/schema-columns.ts +287 -0
  178. package/src/passes/schema-dsl.ts +136 -0
  179. package/src/passes/schema-file-links-ddl.ts +125 -0
  180. package/src/passes/schema-file-links.ts +213 -0
  181. package/src/passes/schema-foreign-keys.ts +121 -0
  182. package/src/passes/schema-policies.ts +270 -0
  183. package/src/passes/schema-re-exports.ts +95 -0
  184. package/src/passes/schema-relations.ts +239 -0
  185. package/src/passes/schema-scope.ts +29 -0
  186. package/src/passes/schema-tables.ts +216 -0
  187. package/src/passes/schema.ts +67 -0
  188. package/src/passes/screen-components.ts +162 -0
  189. package/src/passes/screen-intrinsics.ts +29 -0
  190. package/src/passes/screen-preview.ts +295 -0
  191. package/src/passes/screens.ts +291 -0
  192. package/src/passes/static-read.ts +196 -0
  193. package/src/passes/views.ts +125 -0
  194. package/src/passes/workflow-tables.ts +87 -0
  195. package/src/passes/workflows.ts +213 -0
  196. package/src/path-resolver.ts +146 -0
  197. package/src/revision.ts +25 -0
  198. package/src/source.ts +15 -0
  199. package/src/static-eval.ts +242 -0
  200. package/src/types-graph.ts +176 -0
  201. package/src/types-jobs.ts +40 -0
  202. package/src/types-relations.ts +64 -0
  203. package/src/types-workflows.ts +42 -0
  204. package/src/types.ts +299 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Route files -> RouteFacts (docs/findings/ast-dsl-contract.md §3.1).
3
+ *
4
+ * A route is listed even when it does not use the wrapper: the graph is also the integrity
5
+ * view, so an invalid route is reported instead of omitted. The filename is the only source
6
+ * of method/path (Nitro's file router); the default export is the only source of the config.
7
+ */
8
+ import { identifierName, unwrapExpression } from "../ast/node.js";
9
+ import { error, info, warning } from "../diagnostics.js";
10
+ import { defaultExportDeclaration } from "../module-info.js";
11
+ import { sourceRef } from "../source.js";
12
+ import { EMPTY_ROUTE_RELATIONS } from "../types-relations.js";
13
+ import { routeConfigFacts } from "./route-config.js";
14
+ import { scanElevations } from "./route-elevation.js";
15
+ import { directDbWarnings, handlerReadsBody, handlerReadsForm } from "./route-handler.js";
16
+ import { routeLocationFromFile } from "./route-location.js";
17
+ import { defaultExportExpression, isLocallyDeclared, relativePath, sortDiagnostics, } from "./static-read.js";
18
+ const CORE_SPECIFIER = "@facilis.dev/core";
19
+ const ROUTE_DIRS = ["server/routes/", "server/api/"];
20
+ /** The default export must be a `defineAuthorizedHandler(...)` call of the core import. */
21
+ function resolveWrapper(file) {
22
+ const expression = defaultExportExpression(file);
23
+ if (expression === undefined) {
24
+ return { state: "invalid", call: null, reason: "the route file has no default export" };
25
+ }
26
+ if (expression.type !== "CallExpression") {
27
+ return {
28
+ state: "invalid",
29
+ call: null,
30
+ reason: "the default export is not a defineAuthorizedHandler() call",
31
+ };
32
+ }
33
+ const callee = unwrapExpression(expression.callee);
34
+ const name = callee?.type === "Identifier" ? identifierName(callee) : undefined;
35
+ if (name === undefined) {
36
+ return { state: "unknown", call: null, reason: "the handler callee cannot be resolved" };
37
+ }
38
+ if (isLocallyDeclared(file, name)) {
39
+ return {
40
+ state: "invalid",
41
+ call: null,
42
+ reason: '"' + name + '" is shadowed by a local declaration',
43
+ };
44
+ }
45
+ const imported = file.module.imports.find((entry) => entry.local === name);
46
+ if (imported === undefined ||
47
+ imported.specifier !== CORE_SPECIFIER ||
48
+ imported.imported !== "defineAuthorizedHandler") {
49
+ return {
50
+ state: "invalid",
51
+ call: null,
52
+ reason: "the default export is not the defineAuthorizedHandler import from @facilis.dev/core",
53
+ };
54
+ }
55
+ return { state: "valid", call: expression, reason: "" };
56
+ }
57
+ function scanRoute(graph, file, config) {
58
+ const relative = relativePath(graph.root, file.path);
59
+ const dir = ROUTE_DIRS.find((candidate) => relative.startsWith(candidate));
60
+ const declaration = defaultExportDeclaration(file.program);
61
+ const anchor = declaration?.start ?? 0;
62
+ if (dir === undefined)
63
+ return { route: null, diagnostics: [] };
64
+ const location = routeLocationFromFile(relative.slice(dir.length));
65
+ if (location === undefined) {
66
+ return {
67
+ route: null,
68
+ diagnostics: [
69
+ info("unknown", "cannot derive a route method/path from the route file name", sourceRef(file.path, file.source, anchor)),
70
+ ],
71
+ };
72
+ }
73
+ const wrapper = resolveWrapper(file);
74
+ const ref = sourceRef(file.path, file.source, wrapper.call?.start ?? anchor);
75
+ if (wrapper.state !== "valid" || wrapper.call === null) {
76
+ return {
77
+ route: {
78
+ method: location.method,
79
+ path: location.path,
80
+ sourceFile: file.path,
81
+ source: ref,
82
+ wrapper: wrapper.state,
83
+ permission: null,
84
+ crossSite: null,
85
+ dbTables: null,
86
+ dbColumns: null,
87
+ rawSql: null,
88
+ relations: EMPTY_ROUTE_RELATIONS,
89
+ description: null,
90
+ input: { body: false, form: false, query: false, params: false },
91
+ handlerBodyRead: false,
92
+ handlerFormRead: false,
93
+ elevations: [],
94
+ unknown: [{ code: "route-wrapper", reason: wrapper.reason, source: ref }],
95
+ },
96
+ diagnostics: [error("route-wrapper", wrapper.reason, ref)],
97
+ };
98
+ }
99
+ const facts = routeConfigFacts(graph, file, wrapper.call, config);
100
+ const elevation = scanElevations(graph, file, wrapper.call);
101
+ return {
102
+ route: {
103
+ method: location.method,
104
+ path: location.path,
105
+ sourceFile: file.path,
106
+ source: ref,
107
+ wrapper: "valid",
108
+ permission: facts.permission,
109
+ crossSite: facts.crossSite,
110
+ dbTables: facts.dbTables,
111
+ dbColumns: facts.dbColumns,
112
+ rawSql: facts.rawSql,
113
+ relations: EMPTY_ROUTE_RELATIONS,
114
+ description: facts.description,
115
+ input: facts.input,
116
+ handlerBodyRead: handlerReadsBody(wrapper.call),
117
+ handlerFormRead: handlerReadsForm(wrapper.call),
118
+ elevations: elevation.facts,
119
+ unknown: [...facts.unknown, ...elevation.unknown],
120
+ },
121
+ diagnostics: [
122
+ ...facts.diagnostics,
123
+ ...elevation.unknown.map((item) =>
124
+ // An unreadable elevation cannot be shown in the graph; warn without blocking the route.
125
+ warning(item.code, item.reason, item.source)),
126
+ ...directDbWarnings(file, ref),
127
+ ],
128
+ };
129
+ }
130
+ /** The `defineAuthorizedHandler(...)` call of a valid wrapper, for later passes (D-157). */
131
+ export function resolveWrapperCall(file) {
132
+ const wrapper = resolveWrapper(file);
133
+ return wrapper.state === "valid" ? wrapper.call : null;
134
+ }
135
+ /** Read `server/routes/**` and `server/api/**` into route facts. */
136
+ export function buildRouteFacts(graph, config) {
137
+ const files = [...graph.files.values()]
138
+ .filter((file) => ROUTE_DIRS.some((dir) => relativePath(graph.root, file.path).startsWith(dir)))
139
+ .toSorted((left, right) => left.path.localeCompare(right.path));
140
+ const routes = [];
141
+ const diagnostics = [];
142
+ for (const file of files) {
143
+ const scanned = scanRoute(graph, file, config);
144
+ if (scanned.route !== null)
145
+ routes.push(scanned.route);
146
+ diagnostics.push(...scanned.diagnostics);
147
+ }
148
+ routes.sort((left, right) => left.path.localeCompare(right.path) ||
149
+ left.method.localeCompare(right.method) ||
150
+ left.sourceFile.localeCompare(right.sourceFile));
151
+ return { routes, diagnostics: sortDiagnostics(diagnostics) };
152
+ }
@@ -0,0 +1,9 @@
1
+ import type { AstNode } from "../ast/node.js";
2
+ import { type StaticContext } from "../static-eval.js";
3
+ import type { ColumnDefaultFact } from "../types.js";
4
+ /** Static object argument of a builder/method, e.g. `{ withTimezone: true }`. */
5
+ export declare function optionsOf(node: AstNode | undefined, context: StaticContext): Record<string, unknown> | null;
6
+ /** Display type derived from the builder and its static arguments (never a rendered SQL type). */
7
+ export declare function displayType(name: string, args: readonly AstNode[], context: StaticContext): string;
8
+ /** `.default(value)`: only primitives are facts; a SQL expression is an explicit unknown. */
9
+ export declare function readDefault(node: AstNode | undefined, context: StaticContext): ColumnDefaultFact;
@@ -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;