@facilis.dev/analyze 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) 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 +158 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.js +15 -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.d.ts +22 -0
  46. package/dist/passes/jsx.js +89 -0
  47. package/dist/passes/path-expression.d.ts +26 -0
  48. package/dist/passes/path-expression.js +265 -0
  49. package/dist/passes/route-config.d.ts +24 -0
  50. package/dist/passes/route-config.js +152 -0
  51. package/dist/passes/route-description.d.ts +13 -0
  52. package/dist/passes/route-description.js +23 -0
  53. package/dist/passes/route-elevation.d.ts +18 -0
  54. package/dist/passes/route-elevation.js +136 -0
  55. package/dist/passes/route-handler.d.ts +27 -0
  56. package/dist/passes/route-handler.js +87 -0
  57. package/dist/passes/route-location.d.ts +16 -0
  58. package/dist/passes/route-location.js +58 -0
  59. package/dist/passes/route-relation-resolve.d.ts +51 -0
  60. package/dist/passes/route-relation-resolve.js +172 -0
  61. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  62. package/dist/passes/route-relations-diagnostics.js +79 -0
  63. package/dist/passes/route-relations-follow.d.ts +16 -0
  64. package/dist/passes/route-relations-follow.js +116 -0
  65. package/dist/passes/route-relations-usage.d.ts +50 -0
  66. package/dist/passes/route-relations-usage.js +44 -0
  67. package/dist/passes/route-relations-walk.d.ts +25 -0
  68. package/dist/passes/route-relations-walk.js +199 -0
  69. package/dist/passes/route-relations.d.ts +17 -0
  70. package/dist/passes/route-relations.js +43 -0
  71. package/dist/passes/route-table.d.ts +21 -0
  72. package/dist/passes/route-table.js +104 -0
  73. package/dist/passes/route-tables.d.ts +10 -0
  74. package/dist/passes/route-tables.js +42 -0
  75. package/dist/passes/routes.d.ts +18 -0
  76. package/dist/passes/routes.js +152 -0
  77. package/dist/passes/schema-column-types.d.ts +9 -0
  78. package/dist/passes/schema-column-types.js +55 -0
  79. package/dist/passes/schema-columns.d.ts +35 -0
  80. package/dist/passes/schema-columns.js +197 -0
  81. package/dist/passes/schema-dsl.d.ts +24 -0
  82. package/dist/passes/schema-dsl.js +117 -0
  83. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  84. package/dist/passes/schema-file-links-ddl.js +106 -0
  85. package/dist/passes/schema-file-links.d.ts +19 -0
  86. package/dist/passes/schema-file-links.js +144 -0
  87. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  88. package/dist/passes/schema-foreign-keys.js +80 -0
  89. package/dist/passes/schema-policies.d.ts +15 -0
  90. package/dist/passes/schema-policies.js +201 -0
  91. package/dist/passes/schema-re-exports.d.ts +5 -0
  92. package/dist/passes/schema-re-exports.js +77 -0
  93. package/dist/passes/schema-relations.d.ts +8 -0
  94. package/dist/passes/schema-relations.js +182 -0
  95. package/dist/passes/schema-scope.d.ts +7 -0
  96. package/dist/passes/schema-scope.js +20 -0
  97. package/dist/passes/schema-tables.d.ts +13 -0
  98. package/dist/passes/schema-tables.js +149 -0
  99. package/dist/passes/schema.d.ts +9 -0
  100. package/dist/passes/schema.js +55 -0
  101. package/dist/passes/screen-components.d.ts +22 -0
  102. package/dist/passes/screen-components.js +143 -0
  103. package/dist/passes/screen-preview.d.ts +29 -0
  104. package/dist/passes/screen-preview.js +159 -0
  105. package/dist/passes/screens.d.ts +12 -0
  106. package/dist/passes/screens.js +278 -0
  107. package/dist/passes/static-read.d.ts +47 -0
  108. package/dist/passes/static-read.js +151 -0
  109. package/dist/passes/views.d.ts +20 -0
  110. package/dist/passes/views.js +101 -0
  111. package/dist/passes/workflow-tables.d.ts +18 -0
  112. package/dist/passes/workflow-tables.js +71 -0
  113. package/dist/passes/workflows.d.ts +8 -0
  114. package/dist/passes/workflows.js +160 -0
  115. package/dist/path-resolver.d.ts +20 -0
  116. package/dist/path-resolver.js +134 -0
  117. package/dist/revision.d.ts +2 -0
  118. package/dist/revision.js +23 -0
  119. package/dist/source.d.ts +4 -0
  120. package/dist/source.js +13 -0
  121. package/dist/static-eval.d.ts +36 -0
  122. package/dist/static-eval.js +215 -0
  123. package/dist/types-graph.d.ts +130 -0
  124. package/dist/types-graph.js +4 -0
  125. package/dist/types-jobs.d.ts +28 -0
  126. package/dist/types-jobs.js +14 -0
  127. package/dist/types-relations.d.ts +49 -0
  128. package/dist/types-relations.js +11 -0
  129. package/dist/types-workflows.d.ts +36 -0
  130. package/dist/types-workflows.js +3 -0
  131. package/dist/types.d.ts +226 -0
  132. package/dist/types.js +1 -0
  133. package/package.json +49 -4
  134. package/src/analyze.ts +119 -0
  135. package/src/ast/node.ts +102 -0
  136. package/src/ast/walk.ts +37 -0
  137. package/src/diagnostics.ts +22 -0
  138. package/src/fs.ts +59 -0
  139. package/src/graph-edges.ts +167 -0
  140. package/src/graph-ids.ts +31 -0
  141. package/src/graph.ts +190 -0
  142. package/src/index.ts +24 -0
  143. package/src/json.ts +34 -0
  144. package/src/module-graph.ts +117 -0
  145. package/src/module-info.ts +162 -0
  146. package/src/node.ts +54 -0
  147. package/src/parser.ts +19 -0
  148. package/src/passes/clients.ts +233 -0
  149. package/src/passes/config.ts +278 -0
  150. package/src/passes/file-facts.ts +142 -0
  151. package/src/passes/i18n.ts +66 -0
  152. package/src/passes/job-relations-diagnostics.ts +60 -0
  153. package/src/passes/job-relations.ts +76 -0
  154. package/src/passes/jobs.ts +181 -0
  155. package/src/passes/jsx.ts +92 -0
  156. package/src/passes/path-expression.ts +297 -0
  157. package/src/passes/route-config.ts +188 -0
  158. package/src/passes/route-description.ts +33 -0
  159. package/src/passes/route-elevation.ts +161 -0
  160. package/src/passes/route-handler.ts +105 -0
  161. package/src/passes/route-location.ts +62 -0
  162. package/src/passes/route-relation-resolve.ts +228 -0
  163. package/src/passes/route-relations-diagnostics.ts +115 -0
  164. package/src/passes/route-relations-follow.ts +146 -0
  165. package/src/passes/route-relations-usage.ts +98 -0
  166. package/src/passes/route-relations-walk.ts +255 -0
  167. package/src/passes/route-relations.ts +62 -0
  168. package/src/passes/route-table.ts +123 -0
  169. package/src/passes/route-tables.ts +50 -0
  170. package/src/passes/routes.ts +188 -0
  171. package/src/passes/schema-column-types.ts +62 -0
  172. package/src/passes/schema-columns.ts +287 -0
  173. package/src/passes/schema-dsl.ts +136 -0
  174. package/src/passes/schema-file-links-ddl.ts +125 -0
  175. package/src/passes/schema-file-links.ts +213 -0
  176. package/src/passes/schema-foreign-keys.ts +121 -0
  177. package/src/passes/schema-policies.ts +270 -0
  178. package/src/passes/schema-re-exports.ts +95 -0
  179. package/src/passes/schema-relations.ts +239 -0
  180. package/src/passes/schema-scope.ts +29 -0
  181. package/src/passes/schema-tables.ts +216 -0
  182. package/src/passes/schema.ts +67 -0
  183. package/src/passes/screen-components.ts +162 -0
  184. package/src/passes/screen-preview.ts +268 -0
  185. package/src/passes/screens.ts +291 -0
  186. package/src/passes/static-read.ts +196 -0
  187. package/src/passes/views.ts +125 -0
  188. package/src/passes/workflow-tables.ts +87 -0
  189. package/src/passes/workflows.ts +213 -0
  190. package/src/path-resolver.ts +146 -0
  191. package/src/revision.ts +25 -0
  192. package/src/source.ts +15 -0
  193. package/src/static-eval.ts +242 -0
  194. package/src/types-graph.ts +161 -0
  195. package/src/types-jobs.ts +40 -0
  196. package/src/types-relations.ts +64 -0
  197. package/src/types-workflows.ts +42 -0
  198. package/src/types.ts +299 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `fileLinks(parent, { table, column })` -> the synthetic link table it generates (D-053).
3
+ *
4
+ * The call is framework DSL rather than a `pgTable(...)` literal, so the table pass cannot
5
+ * see the link table it returns. The options come from the static subset and the parent
6
+ * table from the declaring file's own drafts; a value the interpreter cannot read becomes an
7
+ * explicit unknown instead of aborting the pass.
8
+ */
9
+ import { callArguments, identifierName, unwrapExpression } from "../ast/node.js";
10
+ import { optionsOf } from "./schema-column-types.js";
11
+ import { createDslResolver, nodeRef, schemaDiagnostics, schemaUnknown, topLevelDeclarators, } from "./schema-dsl.js";
12
+ import { FACILIS_FILES_BINDING, facilisFilesDraft, linkColumns, linkPolicies, } from "./schema-file-links-ddl.js";
13
+ const DB_PACKAGE = "facilis-db";
14
+ const FILE_LINKS = "fileLinks";
15
+ const DEFAULT_PARENT_COLUMN = "parent_id";
16
+ function readOptions(call, binding, context, source) {
17
+ const label = binding ?? "unknown";
18
+ const options = optionsOf(callArguments(call)[1], context);
19
+ const unknowns = [];
20
+ if (options === null) {
21
+ unknowns.push(schemaUnknown('fileLinks options of "' + label + '" are not a static object', source));
22
+ return { table: null, parentColumn: DEFAULT_PARENT_COLUMN, unknowns };
23
+ }
24
+ const rawTable = options["table"];
25
+ let table = null;
26
+ if (typeof rawTable === "string" && rawTable.length > 0) {
27
+ table = rawTable;
28
+ }
29
+ else {
30
+ unknowns.push(schemaUnknown('fileLinks table name of "' + label + '" is not a static string', source));
31
+ }
32
+ const rawColumn = options["column"];
33
+ let parentColumn = DEFAULT_PARENT_COLUMN;
34
+ if (rawColumn !== undefined) {
35
+ if (typeof rawColumn === "string" && rawColumn.length > 0) {
36
+ parentColumn = rawColumn;
37
+ }
38
+ else {
39
+ unknowns.push(schemaUnknown('fileLinks column name of "' + label + '" is not a static string', source));
40
+ }
41
+ }
42
+ return { table, parentColumn, unknowns };
43
+ }
44
+ function readParent(call, drafts, binding, source) {
45
+ const label = binding ?? "unknown";
46
+ const parentNode = unwrapExpression(callArguments(call)[0]);
47
+ const parentBinding = parentNode?.type === "Identifier" ? identifierName(parentNode) : undefined;
48
+ if (parentBinding === undefined) {
49
+ return {
50
+ target: null,
51
+ unknowns: [schemaUnknown('fileLinks parent of "' + label + '" is not an identifier', source)],
52
+ };
53
+ }
54
+ const draft = drafts.find((entry) => entry.binding === parentBinding);
55
+ if (draft === undefined) {
56
+ return {
57
+ target: null,
58
+ unknowns: [
59
+ schemaUnknown('fileLinks parent "' + parentBinding + '" is not a table declared in this file', source),
60
+ ],
61
+ };
62
+ }
63
+ const key = draft.fact.primaryKey[0];
64
+ if (draft.fact.primaryKey.length !== 1 || key === undefined) {
65
+ return {
66
+ target: null,
67
+ unknowns: [
68
+ schemaUnknown('fileLinks parent "' + parentBinding + '" needs a single-column primary key', source),
69
+ ],
70
+ };
71
+ }
72
+ return { target: { binding: parentBinding, property: key }, unknowns: [] };
73
+ }
74
+ function buildLinkDraft(input) {
75
+ const { call, binding, drafts, context, source } = input;
76
+ const options = readOptions(call, binding, context, source);
77
+ const parent = readParent(call, drafts, binding, source);
78
+ const references = [];
79
+ if (parent.target !== null) {
80
+ references.push({
81
+ column: "parentId",
82
+ target: {
83
+ binding: parent.target.binding,
84
+ property: parent.target.property,
85
+ onDelete: "cascade",
86
+ onUpdate: null,
87
+ source,
88
+ },
89
+ });
90
+ }
91
+ // The generated fileId column references facilis_files regardless of how the parent reads.
92
+ references.push({
93
+ column: "fileId",
94
+ target: {
95
+ binding: FACILIS_FILES_BINDING,
96
+ property: "id",
97
+ onDelete: "cascade",
98
+ onUpdate: null,
99
+ source,
100
+ },
101
+ });
102
+ const unknowns = [...options.unknowns, ...parent.unknowns];
103
+ const fact = {
104
+ name: options.table ?? binding ?? "unknown",
105
+ binding,
106
+ columns: linkColumns(options.parentColumn, source),
107
+ primaryKey: ["id"],
108
+ foreignKeys: [],
109
+ policies: linkPolicies(source),
110
+ rlsEnabled: true,
111
+ scope: { column: "tenantId", kind: "tenant" },
112
+ source,
113
+ unknown: unknowns,
114
+ };
115
+ return { draft: { fact, binding, references }, external: facilisFilesDraft(source), unknowns };
116
+ }
117
+ /** Read the top-level `fileLinks(...)` declarators of one schema file into table drafts. */
118
+ export function collectFileLinkDrafts(graph, file, drafts) {
119
+ const resolver = createDslResolver(graph, file);
120
+ const context = { graph, file: file.path };
121
+ const links = [];
122
+ const externalTables = [];
123
+ const diagnostics = [];
124
+ for (const declarator of topLevelDeclarators(file.program)) {
125
+ const binding = identifierName(declarator.id) ?? null;
126
+ const init = unwrapExpression(declarator.init);
127
+ if (init?.type !== "CallExpression")
128
+ continue;
129
+ const symbol = resolver(init.callee);
130
+ if (symbol?.pkg !== DB_PACKAGE || symbol.name !== FILE_LINKS)
131
+ continue;
132
+ const result = buildLinkDraft({
133
+ call: init,
134
+ binding,
135
+ drafts,
136
+ context,
137
+ source: nodeRef(file, init),
138
+ });
139
+ links.push(result.draft);
140
+ externalTables.push(result.external);
141
+ diagnostics.push(...schemaDiagnostics(result.unknowns));
142
+ }
143
+ return { drafts: links, externalTables, diagnostics };
144
+ }
@@ -0,0 +1,21 @@
1
+ import type { ModuleGraph } from "../module-graph.js";
2
+ import type { Diagnostic, TableFact } from "../types.js";
3
+ import type { TableDraft } from "./schema-tables.js";
4
+ export interface TableLookup {
5
+ /** `file#binding` -> table name; the key keeps same-named bindings in different modules apart. */
6
+ readonly byBinding: ReadonlyMap<string, string>;
7
+ readonly byName: ReadonlyMap<string, TableDraft>;
8
+ }
9
+ export declare function buildTableLookup(drafts: readonly TableDraft[]): TableLookup;
10
+ /**
11
+ * Add lookups for tables a DSL call expands to but the analysis does not emit: a generated
12
+ * `fileLinks` FK points at the framework's `facilis_files`, whose source is absent from a
13
+ * release-ref map. The extra tables resolve FK targets only; they never reach `tables`.
14
+ */
15
+ export declare function withFrameworkTables(lookup: TableLookup, tables: readonly TableDraft[]): TableLookup;
16
+ /** Resolve a table identifier used in the DSL (local const or any project/framework import). */
17
+ export declare function resolveTableBinding(graph: ModuleGraph, filePath: string, local: string, lookup: TableLookup): string | null;
18
+ export declare function finalizeTables(graph: ModuleGraph, drafts: readonly TableDraft[], lookup: TableLookup): {
19
+ tables: TableFact[];
20
+ diagnostics: Diagnostic[];
21
+ };
@@ -0,0 +1,80 @@
1
+ import { diagnostic } from "../diagnostics.js";
2
+ export function buildTableLookup(drafts) {
3
+ const byBinding = new Map();
4
+ const byName = new Map();
5
+ for (const draft of drafts) {
6
+ if (draft.binding !== null) {
7
+ byBinding.set(draft.fact.source.file + "#" + draft.binding, draft.fact.name);
8
+ }
9
+ byName.set(draft.fact.name, draft);
10
+ }
11
+ return { byBinding, byName };
12
+ }
13
+ /**
14
+ * Add lookups for tables a DSL call expands to but the analysis does not emit: a generated
15
+ * `fileLinks` FK points at the framework's `facilis_files`, whose source is absent from a
16
+ * release-ref map. The extra tables resolve FK targets only; they never reach `tables`.
17
+ */
18
+ export function withFrameworkTables(lookup, tables) {
19
+ if (tables.length === 0)
20
+ return lookup;
21
+ const byBinding = new Map(lookup.byBinding);
22
+ const byName = new Map(lookup.byName);
23
+ for (const table of tables) {
24
+ if (!byName.has(table.fact.name))
25
+ byName.set(table.fact.name, table);
26
+ if (table.binding === null)
27
+ continue;
28
+ const key = table.fact.source.file + "#" + table.binding;
29
+ if (!byBinding.has(key))
30
+ byBinding.set(key, table.fact.name);
31
+ }
32
+ return { byBinding, byName };
33
+ }
34
+ /** Resolve a table identifier used in the DSL (local const or any project/framework import). */
35
+ export function resolveTableBinding(graph, filePath, local, lookup) {
36
+ const binding = graph.resolveBinding(filePath, local);
37
+ if (binding !== null) {
38
+ const table = lookup.byBinding.get(binding.file + "#" + binding.name);
39
+ if (table !== undefined)
40
+ return table;
41
+ }
42
+ return lookup.byBinding.get(filePath + "#" + local) ?? null;
43
+ }
44
+ export function finalizeTables(graph, drafts, lookup) {
45
+ const tables = [];
46
+ const diagnostics = [];
47
+ for (const draft of drafts) {
48
+ const foreignKeys = [];
49
+ for (const reference of draft.references) {
50
+ const table = draft.fact.name;
51
+ const targetTable = resolveTableBinding(graph, draft.fact.source.file, reference.target.binding, lookup);
52
+ if (targetTable === null) {
53
+ diagnostics.push(diagnostic("fk-target-known", "warning", 'table "' + table + '" references unknown table "' + reference.target.binding + '"', reference.target.source));
54
+ continue;
55
+ }
56
+ const targetDraft = lookup.byName.get(targetTable);
57
+ const knownColumn = targetDraft?.fact.columns.some((column) => column.property === reference.target.property);
58
+ if (knownColumn !== true) {
59
+ diagnostics.push(diagnostic("fk-target-known", "warning", 'table "' +
60
+ table +
61
+ '" references unknown column "' +
62
+ targetTable +
63
+ "." +
64
+ reference.target.property +
65
+ '"', reference.target.source));
66
+ continue;
67
+ }
68
+ foreignKeys.push({
69
+ columns: [reference.column],
70
+ targetTable,
71
+ targetColumns: [reference.target.property],
72
+ onDelete: reference.target.onDelete,
73
+ onUpdate: reference.target.onUpdate,
74
+ source: reference.target.source,
75
+ });
76
+ }
77
+ tables.push({ ...draft.fact, foreignKeys });
78
+ }
79
+ return { tables, diagnostics };
80
+ }
@@ -0,0 +1,15 @@
1
+ import { type AstNode } from "../ast/node.js";
2
+ import type { ModuleGraph, ProjectFile } from "../module-graph.js";
3
+ import type { Diagnostic, PolicyFact } from "../types.js";
4
+ import { type DslResolver } from "./schema-dsl.js";
5
+ export interface PolicyInput {
6
+ readonly extras: AstNode | undefined;
7
+ readonly file: ProjectFile;
8
+ readonly graph: ModuleGraph;
9
+ readonly resolver: DslResolver;
10
+ }
11
+ export interface PolicyScan {
12
+ readonly policies: readonly PolicyFact[];
13
+ readonly diagnostics: readonly Diagnostic[];
14
+ }
15
+ export declare function buildPolicyFacts(input: PolicyInput): PolicyScan;
@@ -0,0 +1,201 @@
1
+ import { callArguments, identifierName, memberName, nodeArray, unwrapExpression, } from "../ast/node.js";
2
+ import { staticString } from "../static-eval.js";
3
+ import { optionsOf } from "./schema-column-types.js";
4
+ import { callbackExpression, nodeRef, schemaDiagnostics, schemaUnknown, } from "./schema-dsl.js";
5
+ const COMMANDS = {
6
+ select: "SELECT",
7
+ insert: "INSERT",
8
+ update: "UPDATE",
9
+ delete: "DELETE",
10
+ all: "ALL",
11
+ };
12
+ function scan(policies) {
13
+ return {
14
+ policies,
15
+ diagnostics: schemaDiagnostics(policies.flatMap((policy) => policy.unknown)),
16
+ };
17
+ }
18
+ function fixedPolicy(name, command, restrictive, source, unknown = []) {
19
+ return { name, command, restrictive, source, unknown };
20
+ }
21
+ function unknownPolicyScan(name, reason, source) {
22
+ const unknown = schemaUnknown(reason, source);
23
+ return {
24
+ policies: [{ name, command: "ALL", restrictive: false, source, unknown: [unknown] }],
25
+ diagnostics: schemaDiagnostics([unknown]),
26
+ };
27
+ }
28
+ function calleeLabel(callee) {
29
+ const node = unwrapExpression(callee);
30
+ if (node === undefined)
31
+ return "policy";
32
+ return identifierName(node) ?? memberName(node) ?? "policy";
33
+ }
34
+ function readPgPolicy(call, context, file) {
35
+ const source = nodeRef(file, call);
36
+ const args = callArguments(call);
37
+ const unknowns = [];
38
+ let name = "unknown";
39
+ const nameResult = staticString(args[0], context);
40
+ if (nameResult.ok) {
41
+ name = nameResult.value;
42
+ }
43
+ else {
44
+ unknowns.push(schemaUnknown("pgPolicy name is not a static string", source));
45
+ }
46
+ let command = "ALL";
47
+ let restrictive = false;
48
+ if (args[1] !== undefined) {
49
+ const options = optionsOf(args[1], context);
50
+ if (options === null) {
51
+ unknowns.push(schemaUnknown("pgPolicy options are not static", source));
52
+ }
53
+ else {
54
+ const rawFor = options["for"];
55
+ if (rawFor !== undefined) {
56
+ const parsed = typeof rawFor === "string" ? (COMMANDS[rawFor.toLowerCase()] ?? null) : null;
57
+ if (parsed === null) {
58
+ unknowns.push(schemaUnknown('pgPolicy "for" is not a policy command', source));
59
+ }
60
+ else {
61
+ command = parsed;
62
+ }
63
+ }
64
+ const rawAs = options["as"];
65
+ if (rawAs === "restrictive") {
66
+ restrictive = true;
67
+ }
68
+ else if (rawAs !== undefined && rawAs !== "permissive") {
69
+ unknowns.push(schemaUnknown('pgPolicy "as" is not restrictive or permissive', source));
70
+ }
71
+ }
72
+ }
73
+ return scan([fixedPolicy(name, command, restrictive, source, unknowns)]);
74
+ }
75
+ /** `rlsForTable({...})` expands to the same policy set `@facilis.dev/db` builds at runtime. */
76
+ function expandRlsTable(call, context, file) {
77
+ const source = nodeRef(file, call);
78
+ const args = callArguments(call);
79
+ let options = {};
80
+ if (args[0] !== undefined) {
81
+ const evaluated = optionsOf(args[0], context);
82
+ if (evaluated === null) {
83
+ const unknown = schemaUnknown("rlsForTable options are not static", source);
84
+ return {
85
+ policies: [fixedPolicy("tenant_isolation", "ALL", true, source, [unknown])],
86
+ diagnostics: schemaDiagnostics([unknown]),
87
+ };
88
+ }
89
+ options = evaluated;
90
+ }
91
+ const policies = [fixedPolicy("tenant_isolation", "ALL", true, source)];
92
+ const ownerColumn = options["ownerColumn"];
93
+ if (Object.hasOwn(options, "ownerColumn")) {
94
+ const unknown = typeof ownerColumn === "string"
95
+ ? []
96
+ : [schemaUnknown("rlsForTable ownerColumn is not a string", source)];
97
+ policies.push(fixedPolicy("owner_isolation", "ALL", false, source, unknown));
98
+ }
99
+ const rolesColumn = options["rolesColumn"];
100
+ if (Object.hasOwn(options, "rolesColumn")) {
101
+ const unknown = typeof rolesColumn === "string"
102
+ ? []
103
+ : [schemaUnknown("rlsForTable rolesColumn is not a string", source)];
104
+ policies.push(fixedPolicy("role_access", "ALL", false, source, unknown));
105
+ }
106
+ return scan(policies);
107
+ }
108
+ function readRlsExpansion(node, context, file, resolver) {
109
+ if (node === undefined)
110
+ return null;
111
+ let call;
112
+ if (node.type === "CallExpression") {
113
+ call = node;
114
+ }
115
+ else if (node.type === "MemberExpression" && memberName(node) === "policies") {
116
+ const object = unwrapExpression(node.object);
117
+ if (object?.type === "CallExpression")
118
+ call = object;
119
+ }
120
+ if (call === undefined)
121
+ return null;
122
+ const symbol = resolver(call.callee);
123
+ if (symbol?.pkg !== "facilis-db" ||
124
+ (symbol.name !== "rlsForTable" && symbol.name !== "withRls")) {
125
+ return null;
126
+ }
127
+ return expandRlsTable(call, context, file);
128
+ }
129
+ function readPolicyEntry(element, context, file, resolver) {
130
+ const node = unwrapExpression(element);
131
+ const source = nodeRef(file, node ?? element);
132
+ const expansion = readRlsExpansion(node, context, file, resolver);
133
+ if (expansion !== null)
134
+ return expansion;
135
+ if (node?.type !== "CallExpression") {
136
+ return unknownPolicyScan("policy", "table extras entry is not a policy call", source);
137
+ }
138
+ const symbol = resolver(node.callee);
139
+ if (symbol?.pkg === "facilis-db") {
140
+ if (symbol.name === "tenantIsolationPolicy") {
141
+ return scan([fixedPolicy("tenant_isolation", "ALL", true, source)]);
142
+ }
143
+ if (symbol.name === "ownerIsolationPolicy") {
144
+ return scan([fixedPolicy("owner_isolation", "ALL", false, source)]);
145
+ }
146
+ if (symbol.name === "rolePolicy") {
147
+ const args = callArguments(node);
148
+ const unknowns = [];
149
+ let name = "role_access";
150
+ if (args[1] !== undefined) {
151
+ const evaluated = staticString(args[1], context);
152
+ if (evaluated.ok) {
153
+ name = evaluated.value;
154
+ }
155
+ else {
156
+ unknowns.push(schemaUnknown("rolePolicy name is not a static string", source));
157
+ }
158
+ }
159
+ return scan([fixedPolicy(name, "ALL", false, source, unknowns)]);
160
+ }
161
+ }
162
+ if (symbol?.pkg === "pg-core" && symbol.name === "pgPolicy") {
163
+ return readPgPolicy(node, context, file);
164
+ }
165
+ return unknownPolicyScan(calleeLabel(node.callee), 'policy helper "' + calleeLabel(node.callee) + '" is outside the documented DSL', source);
166
+ }
167
+ function readPolicies(expression, context, file, resolver) {
168
+ const node = unwrapExpression(expression);
169
+ if (node === undefined) {
170
+ return unknownPolicyScan("policy", "table extras are not a policy expression", nodeRef(file, expression));
171
+ }
172
+ if (node.type === "ArrayExpression") {
173
+ const policies = [];
174
+ const diagnostics = [];
175
+ for (const element of nodeArray(node, "elements")) {
176
+ const spreadArgument = element.type === "SpreadElement" ? unwrapExpression(element.argument) : undefined;
177
+ const nested = element.type === "SpreadElement"
178
+ ? spreadArgument === undefined
179
+ ? unknownPolicyScan("policy", "spread policy entry is not an expression", nodeRef(file, element))
180
+ : readPolicies(spreadArgument, context, file, resolver)
181
+ : readPolicyEntry(element, context, file, resolver);
182
+ policies.push(...nested.policies);
183
+ diagnostics.push(...nested.diagnostics);
184
+ }
185
+ return { policies, diagnostics };
186
+ }
187
+ const expansion = readRlsExpansion(node, context, file, resolver);
188
+ if (expansion !== null)
189
+ return expansion;
190
+ return readPolicyEntry(node, context, file, resolver);
191
+ }
192
+ export function buildPolicyFacts(input) {
193
+ if (input.extras === undefined)
194
+ return { policies: [], diagnostics: [] };
195
+ const expression = callbackExpression(input.extras);
196
+ if (expression === undefined) {
197
+ return unknownPolicyScan("policy", "table extras are not a readable callback", nodeRef(input.file, input.extras));
198
+ }
199
+ const context = { graph: input.graph, file: input.file.path };
200
+ return readPolicies(expression, context, input.file, input.resolver);
201
+ }
@@ -0,0 +1,5 @@
1
+ import type { ModuleGraph, ProjectFile } from "../module-graph.js";
2
+ import type { ConfigFacts } from "../types.js";
3
+ import { type TableScan } from "./schema-tables.js";
4
+ /** Merge the tables of `export { users } from "@facilis.dev/auth"` (framework source, recursively). */
5
+ export declare function collectReExportedDrafts(graph: ModuleGraph, file: ProjectFile, config: ConfigFacts, seenNames: Set<string>, visitedFiles: Set<string>): TableScan;
@@ -0,0 +1,77 @@
1
+ import { diagnostic } from "../diagnostics.js";
2
+ import { identifierName, literalString, nodeArray } from "../ast/node.js";
3
+ import { nodeRef } from "./schema-dsl.js";
4
+ import { collectTableDrafts } from "./schema-tables.js";
5
+ /** Merge the tables of `export { users } from "@facilis.dev/auth"` (framework source, recursively). */
6
+ export function collectReExportedDrafts(graph, file, config, seenNames, visitedFiles) {
7
+ const drafts = [];
8
+ const diagnostics = [];
9
+ for (const statement of nodeArray(file.program, "body")) {
10
+ const hasSource = statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration";
11
+ const specifier = hasSource ? literalString(statement.source) : undefined;
12
+ if (specifier === undefined)
13
+ continue;
14
+ const targetPath = graph.resolve(file.path, specifier);
15
+ const target = targetPath === null ? undefined : graph.file(targetPath);
16
+ if (target === undefined) {
17
+ diagnostics.push(diagnostic("unknown", "info", 'cannot resolve re-exported module "' + specifier + '"', nodeRef(file, statement)));
18
+ continue;
19
+ }
20
+ if (visitedFiles.has(target.path))
21
+ continue;
22
+ visitedFiles.add(target.path);
23
+ const local = collectTableDrafts(graph, target, config);
24
+ diagnostics.push(...local.diagnostics);
25
+ for (const draft of local.drafts) {
26
+ if (seenNames.has(draft.fact.name))
27
+ continue;
28
+ seenNames.add(draft.fact.name);
29
+ drafts.push(draft);
30
+ }
31
+ const nested = collectReExportedDrafts(graph, target, config, seenNames, visitedFiles);
32
+ drafts.push(...nested.drafts);
33
+ diagnostics.push(...nested.diagnostics);
34
+ }
35
+ // The example project imports framework tables and re-exports the local bindings:
36
+ // import { users } from "@facilis.dev/auth"; export { users };
37
+ // Follow the import, keep only the tables whose binding was re-exported.
38
+ for (const statement of nodeArray(file.program, "body")) {
39
+ if (statement.type !== "ExportNamedDeclaration" ||
40
+ literalString(statement.source) !== undefined)
41
+ continue;
42
+ for (const item of nodeArray(statement, "specifiers")) {
43
+ if (item.type !== "ExportSpecifier")
44
+ continue;
45
+ const local = identifierName(item.local);
46
+ if (local === undefined)
47
+ continue;
48
+ const imported = file.module.imports.find((entry) => entry.local === local);
49
+ if (imported === undefined)
50
+ continue;
51
+ const targetPath = graph.resolve(file.path, imported.specifier);
52
+ const target = targetPath === null ? undefined : graph.file(targetPath);
53
+ if (target === undefined) {
54
+ diagnostics.push(diagnostic("unknown", "info", 'cannot resolve re-exported module "' + imported.specifier + '"', nodeRef(file, item)));
55
+ continue;
56
+ }
57
+ if (visitedFiles.has(target.path))
58
+ continue;
59
+ visitedFiles.add(target.path);
60
+ const localScan = collectTableDrafts(graph, target, config);
61
+ diagnostics.push(...localScan.diagnostics);
62
+ for (const draft of localScan.drafts) {
63
+ const importedName = imported.imported ?? local;
64
+ if (draft.binding !== importedName && draft.fact.name !== importedName)
65
+ continue;
66
+ if (seenNames.has(draft.fact.name))
67
+ continue;
68
+ seenNames.add(draft.fact.name);
69
+ drafts.push(draft);
70
+ }
71
+ const nested = collectReExportedDrafts(graph, target, config, seenNames, visitedFiles);
72
+ drafts.push(...nested.drafts);
73
+ diagnostics.push(...nested.diagnostics);
74
+ }
75
+ }
76
+ return { drafts, diagnostics };
77
+ }
@@ -0,0 +1,8 @@
1
+ import type { ModuleGraph, ProjectFile } from "../module-graph.js";
2
+ import type { Diagnostic, RelationFact } from "../types.js";
3
+ import { type TableLookup } from "./schema-foreign-keys.js";
4
+ export interface RelationScan {
5
+ readonly relations: readonly RelationFact[];
6
+ readonly diagnostics: readonly Diagnostic[];
7
+ }
8
+ export declare function buildRelationFacts(graph: ModuleGraph, files: readonly ProjectFile[], lookup: TableLookup): RelationScan;