@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,270 @@
1
+ import {
2
+ callArguments,
3
+ identifierName,
4
+ memberName,
5
+ nodeArray,
6
+ unwrapExpression,
7
+ type AstNode,
8
+ } from "../ast/node.ts";
9
+ import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
10
+ import { staticString, type StaticContext } from "../static-eval.ts";
11
+ import type { Diagnostic, PolicyCommand, PolicyFact, SourceRef, UnknownFact } from "../types.ts";
12
+ import { optionsOf } from "./schema-column-types.ts";
13
+ import {
14
+ callbackExpression,
15
+ nodeRef,
16
+ schemaDiagnostics,
17
+ schemaUnknown,
18
+ type DslResolver,
19
+ type DslSymbol,
20
+ } from "./schema-dsl.ts";
21
+
22
+ export interface PolicyInput {
23
+ readonly extras: AstNode | undefined;
24
+ readonly file: ProjectFile;
25
+ readonly graph: ModuleGraph;
26
+ readonly resolver: DslResolver;
27
+ }
28
+
29
+ export interface PolicyScan {
30
+ readonly policies: readonly PolicyFact[];
31
+ readonly diagnostics: readonly Diagnostic[];
32
+ }
33
+
34
+ const COMMANDS: Readonly<Record<string, PolicyCommand>> = {
35
+ select: "SELECT",
36
+ insert: "INSERT",
37
+ update: "UPDATE",
38
+ delete: "DELETE",
39
+ all: "ALL",
40
+ };
41
+
42
+ function scan(policies: readonly PolicyFact[]): PolicyScan {
43
+ return {
44
+ policies,
45
+ diagnostics: schemaDiagnostics(policies.flatMap((policy) => policy.unknown)),
46
+ };
47
+ }
48
+
49
+ function fixedPolicy(
50
+ name: string,
51
+ command: PolicyCommand,
52
+ restrictive: boolean,
53
+ source: SourceRef,
54
+ unknown: readonly UnknownFact[] = [],
55
+ ): PolicyFact {
56
+ return { name, command, restrictive, source, unknown };
57
+ }
58
+
59
+ function unknownPolicyScan(name: string, reason: string, source: SourceRef): PolicyScan {
60
+ const unknown = schemaUnknown(reason, source);
61
+ return {
62
+ policies: [{ name, command: "ALL", restrictive: false, source, unknown: [unknown] }],
63
+ diagnostics: schemaDiagnostics([unknown]),
64
+ };
65
+ }
66
+
67
+ function calleeLabel(callee: unknown): string {
68
+ const node = unwrapExpression(callee);
69
+ if (node === undefined) return "policy";
70
+ return identifierName(node) ?? memberName(node) ?? "policy";
71
+ }
72
+
73
+ function readPgPolicy(call: AstNode, context: StaticContext, file: ProjectFile): PolicyScan {
74
+ const source = nodeRef(file, call);
75
+ const args = callArguments(call);
76
+ const unknowns: UnknownFact[] = [];
77
+ let name = "unknown";
78
+ const nameResult = staticString(args[0], context);
79
+ if (nameResult.ok) {
80
+ name = nameResult.value;
81
+ } else {
82
+ unknowns.push(schemaUnknown("pgPolicy name is not a static string", source));
83
+ }
84
+ let command: PolicyCommand = "ALL";
85
+ let restrictive = false;
86
+ if (args[1] !== undefined) {
87
+ const options = optionsOf(args[1], context);
88
+ if (options === null) {
89
+ unknowns.push(schemaUnknown("pgPolicy options are not static", source));
90
+ } else {
91
+ const rawFor = options["for"];
92
+ if (rawFor !== undefined) {
93
+ const parsed = typeof rawFor === "string" ? (COMMANDS[rawFor.toLowerCase()] ?? null) : null;
94
+ if (parsed === null) {
95
+ unknowns.push(schemaUnknown('pgPolicy "for" is not a policy command', source));
96
+ } else {
97
+ command = parsed;
98
+ }
99
+ }
100
+ const rawAs = options["as"];
101
+ if (rawAs === "restrictive") {
102
+ restrictive = true;
103
+ } else if (rawAs !== undefined && rawAs !== "permissive") {
104
+ unknowns.push(schemaUnknown('pgPolicy "as" is not restrictive or permissive', source));
105
+ }
106
+ }
107
+ }
108
+ return scan([fixedPolicy(name, command, restrictive, source, unknowns)]);
109
+ }
110
+
111
+ /** `rlsForTable({...})` expands to the same policy set `@facilis.dev/db` builds at runtime. */
112
+ function expandRlsTable(call: AstNode, context: StaticContext, file: ProjectFile): PolicyScan {
113
+ const source = nodeRef(file, call);
114
+ const args = callArguments(call);
115
+ let options: Record<string, unknown> = {};
116
+ if (args[0] !== undefined) {
117
+ const evaluated = optionsOf(args[0], context);
118
+ if (evaluated === null) {
119
+ const unknown = schemaUnknown("rlsForTable options are not static", source);
120
+ return {
121
+ policies: [fixedPolicy("tenant_isolation", "ALL", true, source, [unknown])],
122
+ diagnostics: schemaDiagnostics([unknown]),
123
+ };
124
+ }
125
+ options = evaluated;
126
+ }
127
+ const policies: PolicyFact[] = [fixedPolicy("tenant_isolation", "ALL", true, source)];
128
+ const ownerColumn = options["ownerColumn"];
129
+ if (Object.hasOwn(options, "ownerColumn")) {
130
+ const unknown =
131
+ typeof ownerColumn === "string"
132
+ ? []
133
+ : [schemaUnknown("rlsForTable ownerColumn is not a string", source)];
134
+ policies.push(fixedPolicy("owner_isolation", "ALL", false, source, unknown));
135
+ }
136
+ const rolesColumn = options["rolesColumn"];
137
+ if (Object.hasOwn(options, "rolesColumn")) {
138
+ const unknown =
139
+ typeof rolesColumn === "string"
140
+ ? []
141
+ : [schemaUnknown("rlsForTable rolesColumn is not a string", source)];
142
+ policies.push(fixedPolicy("role_access", "ALL", false, source, unknown));
143
+ }
144
+ return scan(policies);
145
+ }
146
+
147
+ function readRlsExpansion(
148
+ node: AstNode | undefined,
149
+ context: StaticContext,
150
+ file: ProjectFile,
151
+ resolver: DslResolver,
152
+ ): PolicyScan | null {
153
+ if (node === undefined) return null;
154
+ let call: AstNode | undefined;
155
+ if (node.type === "CallExpression") {
156
+ call = node;
157
+ } else if (node.type === "MemberExpression" && memberName(node) === "policies") {
158
+ const object = unwrapExpression(node.object);
159
+ if (object?.type === "CallExpression") call = object;
160
+ }
161
+ if (call === undefined) return null;
162
+ const symbol: DslSymbol | null = resolver(call.callee);
163
+ if (
164
+ symbol?.pkg !== "facilis-db" ||
165
+ (symbol.name !== "rlsForTable" && symbol.name !== "withRls")
166
+ ) {
167
+ return null;
168
+ }
169
+ return expandRlsTable(call, context, file);
170
+ }
171
+
172
+ function readPolicyEntry(
173
+ element: AstNode,
174
+ context: StaticContext,
175
+ file: ProjectFile,
176
+ resolver: DslResolver,
177
+ ): PolicyScan {
178
+ const node = unwrapExpression(element);
179
+ const source = nodeRef(file, node ?? element);
180
+ const expansion = readRlsExpansion(node, context, file, resolver);
181
+ if (expansion !== null) return expansion;
182
+ if (node?.type !== "CallExpression") {
183
+ return unknownPolicyScan("policy", "table extras entry is not a policy call", source);
184
+ }
185
+ const symbol = resolver(node.callee);
186
+ if (symbol?.pkg === "facilis-db") {
187
+ if (symbol.name === "tenantIsolationPolicy") {
188
+ return scan([fixedPolicy("tenant_isolation", "ALL", true, source)]);
189
+ }
190
+ if (symbol.name === "ownerIsolationPolicy") {
191
+ return scan([fixedPolicy("owner_isolation", "ALL", false, source)]);
192
+ }
193
+ if (symbol.name === "rolePolicy") {
194
+ const args = callArguments(node);
195
+ const unknowns: UnknownFact[] = [];
196
+ let name = "role_access";
197
+ if (args[1] !== undefined) {
198
+ const evaluated = staticString(args[1], context);
199
+ if (evaluated.ok) {
200
+ name = evaluated.value;
201
+ } else {
202
+ unknowns.push(schemaUnknown("rolePolicy name is not a static string", source));
203
+ }
204
+ }
205
+ return scan([fixedPolicy(name, "ALL", false, source, unknowns)]);
206
+ }
207
+ }
208
+ if (symbol?.pkg === "pg-core" && symbol.name === "pgPolicy") {
209
+ return readPgPolicy(node, context, file);
210
+ }
211
+ return unknownPolicyScan(
212
+ calleeLabel(node.callee),
213
+ 'policy helper "' + calleeLabel(node.callee) + '" is outside the documented DSL',
214
+ source,
215
+ );
216
+ }
217
+
218
+ function readPolicies(
219
+ expression: AstNode,
220
+ context: StaticContext,
221
+ file: ProjectFile,
222
+ resolver: DslResolver,
223
+ ): PolicyScan {
224
+ const node = unwrapExpression(expression);
225
+ if (node === undefined) {
226
+ return unknownPolicyScan(
227
+ "policy",
228
+ "table extras are not a policy expression",
229
+ nodeRef(file, expression),
230
+ );
231
+ }
232
+ if (node.type === "ArrayExpression") {
233
+ const policies: PolicyFact[] = [];
234
+ const diagnostics: Diagnostic[] = [];
235
+ for (const element of nodeArray(node, "elements")) {
236
+ const spreadArgument =
237
+ element.type === "SpreadElement" ? unwrapExpression(element.argument) : undefined;
238
+ const nested =
239
+ element.type === "SpreadElement"
240
+ ? spreadArgument === undefined
241
+ ? unknownPolicyScan(
242
+ "policy",
243
+ "spread policy entry is not an expression",
244
+ nodeRef(file, element),
245
+ )
246
+ : readPolicies(spreadArgument, context, file, resolver)
247
+ : readPolicyEntry(element, context, file, resolver);
248
+ policies.push(...nested.policies);
249
+ diagnostics.push(...nested.diagnostics);
250
+ }
251
+ return { policies, diagnostics };
252
+ }
253
+ const expansion = readRlsExpansion(node, context, file, resolver);
254
+ if (expansion !== null) return expansion;
255
+ return readPolicyEntry(node, context, file, resolver);
256
+ }
257
+
258
+ export function buildPolicyFacts(input: PolicyInput): PolicyScan {
259
+ if (input.extras === undefined) return { policies: [], diagnostics: [] };
260
+ const expression = callbackExpression(input.extras);
261
+ if (expression === undefined) {
262
+ return unknownPolicyScan(
263
+ "policy",
264
+ "table extras are not a readable callback",
265
+ nodeRef(input.file, input.extras),
266
+ );
267
+ }
268
+ const context: StaticContext = { graph: input.graph, file: input.file.path };
269
+ return readPolicies(expression, context, input.file, input.resolver);
270
+ }
@@ -0,0 +1,95 @@
1
+ import { diagnostic } from "../diagnostics.ts";
2
+ import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
3
+ import { identifierName, literalString, nodeArray } from "../ast/node.ts";
4
+ import type { Diagnostic, ConfigFacts } from "../types.ts";
5
+ import { nodeRef } from "./schema-dsl.ts";
6
+ import { collectTableDrafts, type TableDraft, type TableScan } from "./schema-tables.ts";
7
+
8
+ /** Merge the tables of `export { users } from "@facilis.dev/auth"` (framework source, recursively). */
9
+ export function collectReExportedDrafts(
10
+ graph: ModuleGraph,
11
+ file: ProjectFile,
12
+ config: ConfigFacts,
13
+ seenNames: Set<string>,
14
+ visitedFiles: Set<string>,
15
+ ): TableScan {
16
+ const drafts: TableDraft[] = [];
17
+ const diagnostics: Diagnostic[] = [];
18
+ for (const statement of nodeArray(file.program, "body")) {
19
+ const hasSource =
20
+ statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration";
21
+ const specifier = hasSource ? literalString(statement.source) : undefined;
22
+ if (specifier === undefined) continue;
23
+ const targetPath = graph.resolve(file.path, specifier);
24
+ const target = targetPath === null ? undefined : graph.file(targetPath);
25
+ if (target === undefined) {
26
+ diagnostics.push(
27
+ diagnostic(
28
+ "unknown",
29
+ "info",
30
+ 'cannot resolve re-exported module "' + specifier + '"',
31
+ nodeRef(file, statement),
32
+ ),
33
+ );
34
+ continue;
35
+ }
36
+ if (visitedFiles.has(target.path)) continue;
37
+ visitedFiles.add(target.path);
38
+ const local = collectTableDrafts(graph, target, config);
39
+ diagnostics.push(...local.diagnostics);
40
+ for (const draft of local.drafts) {
41
+ if (seenNames.has(draft.fact.name)) continue;
42
+ seenNames.add(draft.fact.name);
43
+ drafts.push(draft);
44
+ }
45
+ const nested = collectReExportedDrafts(graph, target, config, seenNames, visitedFiles);
46
+ drafts.push(...nested.drafts);
47
+ diagnostics.push(...nested.diagnostics);
48
+ }
49
+
50
+ // The example project imports framework tables and re-exports the local bindings:
51
+ // import { users } from "@facilis.dev/auth"; export { users };
52
+ // Follow the import, keep only the tables whose binding was re-exported.
53
+ for (const statement of nodeArray(file.program, "body")) {
54
+ if (
55
+ statement.type !== "ExportNamedDeclaration" ||
56
+ literalString(statement.source) !== undefined
57
+ )
58
+ continue;
59
+ for (const item of nodeArray(statement, "specifiers")) {
60
+ if (item.type !== "ExportSpecifier") continue;
61
+ const local = identifierName(item.local);
62
+ if (local === undefined) continue;
63
+ const imported = file.module.imports.find((entry) => entry.local === local);
64
+ if (imported === undefined) continue;
65
+ const targetPath = graph.resolve(file.path, imported.specifier);
66
+ const target = targetPath === null ? undefined : graph.file(targetPath);
67
+ if (target === undefined) {
68
+ diagnostics.push(
69
+ diagnostic(
70
+ "unknown",
71
+ "info",
72
+ 'cannot resolve re-exported module "' + imported.specifier + '"',
73
+ nodeRef(file, item),
74
+ ),
75
+ );
76
+ continue;
77
+ }
78
+ if (visitedFiles.has(target.path)) continue;
79
+ visitedFiles.add(target.path);
80
+ const localScan = collectTableDrafts(graph, target, config);
81
+ diagnostics.push(...localScan.diagnostics);
82
+ for (const draft of localScan.drafts) {
83
+ const importedName = imported.imported ?? local;
84
+ if (draft.binding !== importedName && draft.fact.name !== importedName) continue;
85
+ if (seenNames.has(draft.fact.name)) continue;
86
+ seenNames.add(draft.fact.name);
87
+ drafts.push(draft);
88
+ }
89
+ const nested = collectReExportedDrafts(graph, target, config, seenNames, visitedFiles);
90
+ drafts.push(...nested.drafts);
91
+ diagnostics.push(...nested.diagnostics);
92
+ }
93
+ }
94
+ return { drafts, diagnostics };
95
+ }
@@ -0,0 +1,239 @@
1
+ import {
2
+ callArguments,
3
+ identifierName,
4
+ memberName,
5
+ nodeArray,
6
+ objectEntries,
7
+ propertyName,
8
+ propertyValue,
9
+ unwrapExpression,
10
+ type AstNode,
11
+ } from "../ast/node.ts";
12
+ import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
13
+ import type { Diagnostic, RelationFact, UnknownFact } from "../types.ts";
14
+ import {
15
+ callbackExpression,
16
+ createDslResolver,
17
+ nodeRef,
18
+ schemaDiagnostics,
19
+ schemaUnknown,
20
+ topLevelDeclarators,
21
+ type DslResolver,
22
+ } from "./schema-dsl.ts";
23
+ import { resolveTableBinding, type TableLookup } from "./schema-foreign-keys.ts";
24
+
25
+ export interface RelationScan {
26
+ readonly relations: readonly RelationFact[];
27
+ readonly diagnostics: readonly Diagnostic[];
28
+ }
29
+
30
+ interface HelperBindings {
31
+ readonly one: ReadonlySet<string>;
32
+ readonly many: ReadonlySet<string>;
33
+ }
34
+
35
+ /** Map the callback's destructured `one`/`many` helpers to their local names. */
36
+ function helperBindings(callback: AstNode | undefined): HelperBindings | null {
37
+ const fn = unwrapExpression(callback);
38
+ if (fn === undefined) return null;
39
+ if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return null;
40
+ const param = unwrapExpression(nodeArray(fn, "params")[0]);
41
+ if (param?.type !== "ObjectPattern") return null;
42
+ const one = new Set<string>();
43
+ const many = new Set<string>();
44
+ for (const property of objectEntries(param)) {
45
+ if (property.type === "SpreadElement") continue;
46
+ const key = propertyName(property);
47
+ const local = identifierName(propertyValue(property));
48
+ if (local === undefined) continue;
49
+ if (key === "one") one.add(local);
50
+ if (key === "many") many.add(local);
51
+ }
52
+ return { one, many };
53
+ }
54
+
55
+ function readColumnList(
56
+ options: AstNode | undefined,
57
+ file: ProjectFile,
58
+ unknowns: UnknownFact[],
59
+ ): { fields: string[]; references: string[] } {
60
+ const empty = { fields: [] as string[], references: [] as string[] };
61
+ if (options === undefined) return empty;
62
+ const node = unwrapExpression(options);
63
+ if (node?.type !== "ObjectExpression") {
64
+ unknowns.push(
65
+ schemaUnknown("relation options are not an object literal", nodeRef(file, options)),
66
+ );
67
+ return empty;
68
+ }
69
+ const readList = (key: string): string[] => {
70
+ const property = objectEntries(node).find((entry) => propertyName(entry) === key);
71
+ if (property === undefined) return [];
72
+ const value = unwrapExpression(propertyValue(property));
73
+ if (value?.type !== "ArrayExpression") {
74
+ unknowns.push(
75
+ schemaUnknown("relation " + key + " is not an array literal", nodeRef(file, property)),
76
+ );
77
+ return [];
78
+ }
79
+ const columns: string[] = [];
80
+ for (const element of nodeArray(value, "elements")) {
81
+ const item = unwrapExpression(element);
82
+ const name = item?.type === "MemberExpression" ? memberName(item) : identifierName(item);
83
+ if (name === undefined) {
84
+ unknowns.push(
85
+ schemaUnknown(
86
+ "relation " + key + " entry is not a column reference",
87
+ nodeRef(file, item),
88
+ ),
89
+ );
90
+ continue;
91
+ }
92
+ columns.push(name);
93
+ }
94
+ return columns;
95
+ };
96
+ return { fields: readList("fields"), references: readList("references") };
97
+ }
98
+
99
+ function readRelationEntry(input: {
100
+ readonly entry: AstNode;
101
+ readonly from: string;
102
+ readonly graph: ModuleGraph;
103
+ readonly file: ProjectFile;
104
+ readonly lookup: TableLookup;
105
+ readonly helpers: HelperBindings;
106
+ }): { relation: RelationFact | null; diagnostics: readonly Diagnostic[] } {
107
+ const { entry, from, graph, file, lookup, helpers } = input;
108
+ const unknowns: UnknownFact[] = [];
109
+ const value = unwrapExpression(propertyValue(entry));
110
+ const source = nodeRef(file, entry);
111
+ if (value?.type !== "CallExpression") {
112
+ return {
113
+ relation: null,
114
+ diagnostics: schemaDiagnostics([
115
+ schemaUnknown("relations() entry is not a one()/many() call", source),
116
+ ]),
117
+ };
118
+ }
119
+ const calleeLocal = identifierName(unwrapExpression(value.callee));
120
+ const cardinality =
121
+ calleeLocal !== undefined && helpers.one.has(calleeLocal)
122
+ ? "one"
123
+ : calleeLocal !== undefined && helpers.many.has(calleeLocal)
124
+ ? "many"
125
+ : null;
126
+ if (cardinality === null) {
127
+ return {
128
+ relation: null,
129
+ diagnostics: schemaDiagnostics([
130
+ schemaUnknown("relations() entry is not a one()/many() call", source),
131
+ ]),
132
+ };
133
+ }
134
+ const args = callArguments(value);
135
+ const targetLocal = identifierName(unwrapExpression(args[0]));
136
+ const targetTable =
137
+ targetLocal === undefined ? null : resolveTableBinding(graph, file.path, targetLocal, lookup);
138
+ if (targetTable === null) {
139
+ unknowns.push(schemaUnknown("relation target table is not resolvable statically", source));
140
+ }
141
+ const columns = readColumnList(args[1], file, unknowns);
142
+ const relation: RelationFact = {
143
+ from,
144
+ to: targetTable ?? targetLocal ?? "unknown",
145
+ cardinality,
146
+ fields: columns.fields,
147
+ references: columns.references,
148
+ source,
149
+ unknown: unknowns,
150
+ };
151
+ return { relation, diagnostics: schemaDiagnostics(unknowns) };
152
+ }
153
+
154
+ function readRelations(input: {
155
+ readonly graph: ModuleGraph;
156
+ readonly file: ProjectFile;
157
+ readonly call: AstNode;
158
+ readonly lookup: TableLookup;
159
+ }): RelationScan {
160
+ const { graph, file, call, lookup } = input;
161
+ const source = nodeRef(file, call);
162
+ const args = callArguments(call);
163
+ const fromLocal = identifierName(unwrapExpression(args[0]));
164
+ const fromTable =
165
+ fromLocal === undefined ? null : resolveTableBinding(graph, file.path, fromLocal, lookup);
166
+ const from = fromTable ?? fromLocal ?? "unknown";
167
+ const callback = args[1];
168
+ const body = callbackExpression(callback);
169
+ const helpers = helperBindings(callback);
170
+ if (body?.type !== "ObjectExpression" || helpers === null) {
171
+ return {
172
+ relations: [],
173
+ diagnostics: schemaDiagnostics([
174
+ schemaUnknown("relations() callback is not a destructured one/many callback", source),
175
+ ]),
176
+ };
177
+ }
178
+ const relations: RelationFact[] = [];
179
+ const diagnostics: Diagnostic[] = [];
180
+ if (fromTable === null) {
181
+ const unknown = schemaUnknown("relation source table is not resolvable statically", source);
182
+ diagnostics.push(...schemaDiagnostics([unknown]));
183
+ }
184
+ for (const entry of objectEntries(body)) {
185
+ if (propertyName(entry) === undefined) {
186
+ diagnostics.push(
187
+ ...schemaDiagnostics([
188
+ schemaUnknown("computed relation key is outside the DSL", nodeRef(file, entry)),
189
+ ]),
190
+ );
191
+ continue;
192
+ }
193
+ const result = readRelationEntry({ entry, from, graph, file, lookup, helpers });
194
+ if (result.relation !== null) relations.push(result.relation);
195
+ diagnostics.push(...result.diagnostics);
196
+ }
197
+ return { relations, diagnostics };
198
+ }
199
+
200
+ export function buildRelationFacts(
201
+ graph: ModuleGraph,
202
+ files: readonly ProjectFile[],
203
+ lookup: TableLookup,
204
+ ): RelationScan {
205
+ const relations: RelationFact[] = [];
206
+ const diagnostics: Diagnostic[] = [];
207
+ for (const file of files) {
208
+ const resolver: DslResolver = createDslResolver(graph, file);
209
+ for (const declarator of topLevelDeclarators(file.program)) {
210
+ const call = unwrapExpression(declarator.init);
211
+ if (call?.type !== "CallExpression") continue;
212
+ const symbol = resolver(call.callee);
213
+ if (symbol?.pkg !== "drizzle-orm" || symbol.name !== "relations") continue;
214
+ const scan = readRelations({ graph, file, call, lookup });
215
+ relations.push(...scan.relations);
216
+ diagnostics.push(...scan.diagnostics);
217
+ }
218
+ }
219
+ return { relations: dedupeRelations(relations), diagnostics };
220
+ }
221
+
222
+ /** Duplicate declarations (e.g. split files) must not duplicate edges. */
223
+ function dedupeRelations(relations: readonly RelationFact[]): RelationFact[] {
224
+ const seen = new Set<string>();
225
+ const out: RelationFact[] = [];
226
+ for (const relation of relations) {
227
+ const key = [
228
+ relation.from,
229
+ relation.to,
230
+ relation.cardinality,
231
+ relation.fields.join(","),
232
+ relation.references.join(","),
233
+ ].join("|");
234
+ if (seen.has(key)) continue;
235
+ seen.add(key);
236
+ out.push(relation);
237
+ }
238
+ return out;
239
+ }
@@ -0,0 +1,29 @@
1
+ import type { ColumnFact, ConfigFacts, ScopeFact } from "../types.ts";
2
+
3
+ export interface ScopedColumn {
4
+ readonly column: ColumnFact;
5
+ readonly tenant: boolean;
6
+ }
7
+
8
+ function normalized(value: string): string {
9
+ return value.replaceAll("_", "").replaceAll("-", "").toLowerCase();
10
+ }
11
+
12
+ function matchesConfig(column: ColumnFact, names: readonly string[]): boolean {
13
+ const property = normalized(column.property);
14
+ const sqlName = normalized(column.name);
15
+ return names.some((candidate) => {
16
+ const value = normalized(candidate);
17
+ return value === property || value === sqlName;
18
+ });
19
+ }
20
+
21
+ /** Tenant scope wins over owner scope: both can be declared on the same table. */
22
+ export function scopeOf(columns: readonly ScopedColumn[], config: ConfigFacts): ScopeFact | null {
23
+ const tenant =
24
+ columns.find((entry) => entry.tenant) ??
25
+ columns.find((entry) => matchesConfig(entry.column, config.tenantColumns));
26
+ if (tenant !== undefined) return { column: tenant.column.property, kind: "tenant" };
27
+ const owner = columns.find((entry) => matchesConfig(entry.column, config.ownerColumns));
28
+ return owner === undefined ? null : { column: owner.column.property, kind: "owner" };
29
+ }