@danielfgray/pg-sourcerer 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 (145) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +104 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config.d.ts +133 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +47 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/errors.d.ts +129 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +41 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/generate.d.ts +75 -0
  14. package/dist/generate.d.ts.map +1 -0
  15. package/dist/generate.js +183 -0
  16. package/dist/generate.js.map +1 -0
  17. package/dist/index.d.ts +35 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +62 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/init.d.ts +4 -0
  22. package/dist/init.d.ts.map +1 -0
  23. package/dist/init.js +229 -0
  24. package/dist/init.js.map +1 -0
  25. package/dist/ir/index.d.ts +7 -0
  26. package/dist/ir/index.d.ts.map +1 -0
  27. package/dist/ir/index.js +7 -0
  28. package/dist/ir/index.js.map +1 -0
  29. package/dist/ir/relation-graph.d.ts +113 -0
  30. package/dist/ir/relation-graph.d.ts.map +1 -0
  31. package/dist/ir/relation-graph.js +232 -0
  32. package/dist/ir/relation-graph.js.map +1 -0
  33. package/dist/ir/semantic-ir.d.ts +448 -0
  34. package/dist/ir/semantic-ir.d.ts.map +1 -0
  35. package/dist/ir/semantic-ir.js +138 -0
  36. package/dist/ir/semantic-ir.js.map +1 -0
  37. package/dist/ir/smart-tags.d.ts +24 -0
  38. package/dist/ir/smart-tags.d.ts.map +1 -0
  39. package/dist/ir/smart-tags.js +30 -0
  40. package/dist/ir/smart-tags.js.map +1 -0
  41. package/dist/lib/conjure.d.ts +431 -0
  42. package/dist/lib/conjure.d.ts.map +1 -0
  43. package/dist/lib/conjure.js +697 -0
  44. package/dist/lib/conjure.js.map +1 -0
  45. package/dist/lib/field-utils.d.ts +61 -0
  46. package/dist/lib/field-utils.d.ts.map +1 -0
  47. package/dist/lib/field-utils.js +132 -0
  48. package/dist/lib/field-utils.js.map +1 -0
  49. package/dist/lib/hex.d.ts +117 -0
  50. package/dist/lib/hex.d.ts.map +1 -0
  51. package/dist/lib/hex.js +185 -0
  52. package/dist/lib/hex.js.map +1 -0
  53. package/dist/plugins/arktype.d.ts +11 -0
  54. package/dist/plugins/arktype.d.ts.map +1 -0
  55. package/dist/plugins/arktype.js +207 -0
  56. package/dist/plugins/arktype.js.map +1 -0
  57. package/dist/plugins/effect-model.d.ts +10 -0
  58. package/dist/plugins/effect-model.d.ts.map +1 -0
  59. package/dist/plugins/effect-model.js +261 -0
  60. package/dist/plugins/effect-model.js.map +1 -0
  61. package/dist/plugins/kysely-queries.d.ts +7 -0
  62. package/dist/plugins/kysely-queries.d.ts.map +1 -0
  63. package/dist/plugins/kysely-queries.js +380 -0
  64. package/dist/plugins/kysely-queries.js.map +1 -0
  65. package/dist/plugins/sql-queries.d.ts +6 -0
  66. package/dist/plugins/sql-queries.d.ts.map +1 -0
  67. package/dist/plugins/sql-queries.js +249 -0
  68. package/dist/plugins/sql-queries.js.map +1 -0
  69. package/dist/plugins/types.d.ts +18 -0
  70. package/dist/plugins/types.d.ts.map +1 -0
  71. package/dist/plugins/types.js +263 -0
  72. package/dist/plugins/types.js.map +1 -0
  73. package/dist/plugins/zod.d.ts +11 -0
  74. package/dist/plugins/zod.d.ts.map +1 -0
  75. package/dist/plugins/zod.js +180 -0
  76. package/dist/plugins/zod.js.map +1 -0
  77. package/dist/services/artifact-store.d.ts +55 -0
  78. package/dist/services/artifact-store.d.ts.map +1 -0
  79. package/dist/services/artifact-store.js +51 -0
  80. package/dist/services/artifact-store.js.map +1 -0
  81. package/dist/services/config-loader.d.ts +45 -0
  82. package/dist/services/config-loader.d.ts.map +1 -0
  83. package/dist/services/config-loader.js +113 -0
  84. package/dist/services/config-loader.js.map +1 -0
  85. package/dist/services/emissions.d.ts +89 -0
  86. package/dist/services/emissions.d.ts.map +1 -0
  87. package/dist/services/emissions.js +194 -0
  88. package/dist/services/emissions.js.map +1 -0
  89. package/dist/services/file-builder.d.ts +81 -0
  90. package/dist/services/file-builder.d.ts.map +1 -0
  91. package/dist/services/file-builder.js +112 -0
  92. package/dist/services/file-builder.js.map +1 -0
  93. package/dist/services/file-writer.d.ts +57 -0
  94. package/dist/services/file-writer.d.ts.map +1 -0
  95. package/dist/services/file-writer.js +76 -0
  96. package/dist/services/file-writer.js.map +1 -0
  97. package/dist/services/inflection.d.ts +227 -0
  98. package/dist/services/inflection.d.ts.map +1 -0
  99. package/dist/services/inflection.js +350 -0
  100. package/dist/services/inflection.js.map +1 -0
  101. package/dist/services/introspection.d.ts +46 -0
  102. package/dist/services/introspection.d.ts.map +1 -0
  103. package/dist/services/introspection.js +99 -0
  104. package/dist/services/introspection.js.map +1 -0
  105. package/dist/services/ir-builder.d.ts +46 -0
  106. package/dist/services/ir-builder.d.ts.map +1 -0
  107. package/dist/services/ir-builder.js +923 -0
  108. package/dist/services/ir-builder.js.map +1 -0
  109. package/dist/services/ir.d.ts +28 -0
  110. package/dist/services/ir.d.ts.map +1 -0
  111. package/dist/services/ir.js +25 -0
  112. package/dist/services/ir.js.map +1 -0
  113. package/dist/services/pg-types.d.ts +197 -0
  114. package/dist/services/pg-types.d.ts.map +1 -0
  115. package/dist/services/pg-types.js +274 -0
  116. package/dist/services/pg-types.js.map +1 -0
  117. package/dist/services/plugin-meta.d.ts +33 -0
  118. package/dist/services/plugin-meta.d.ts.map +1 -0
  119. package/dist/services/plugin-meta.js +24 -0
  120. package/dist/services/plugin-meta.js.map +1 -0
  121. package/dist/services/plugin-runner.d.ts +52 -0
  122. package/dist/services/plugin-runner.d.ts.map +1 -0
  123. package/dist/services/plugin-runner.js +182 -0
  124. package/dist/services/plugin-runner.js.map +1 -0
  125. package/dist/services/plugin.d.ts +286 -0
  126. package/dist/services/plugin.d.ts.map +1 -0
  127. package/dist/services/plugin.js +132 -0
  128. package/dist/services/plugin.js.map +1 -0
  129. package/dist/services/smart-tags-parser.d.ts +37 -0
  130. package/dist/services/smart-tags-parser.d.ts.map +1 -0
  131. package/dist/services/smart-tags-parser.js +79 -0
  132. package/dist/services/smart-tags-parser.js.map +1 -0
  133. package/dist/services/symbols.d.ts +85 -0
  134. package/dist/services/symbols.d.ts.map +1 -0
  135. package/dist/services/symbols.js +128 -0
  136. package/dist/services/symbols.js.map +1 -0
  137. package/dist/services/type-hints.d.ts +62 -0
  138. package/dist/services/type-hints.d.ts.map +1 -0
  139. package/dist/services/type-hints.js +117 -0
  140. package/dist/services/type-hints.js.map +1 -0
  141. package/dist/testing.d.ts +77 -0
  142. package/dist/testing.d.ts.map +1 -0
  143. package/dist/testing.js +84 -0
  144. package/dist/testing.js.map +1 -0
  145. package/package.json +74 -0
@@ -0,0 +1,923 @@
1
+ /**
2
+ * IR Builder Service
3
+ *
4
+ * Transforms raw pg-introspection output into SemanticIR.
5
+ * Builds entities (tables, views, composites), shapes, fields,
6
+ * relations, and enums.
7
+ */
8
+ import { Context, Effect, Layer, pipe, Array as Arr, Option } from "effect";
9
+ import { entityPermissions } from "@pg-sourcerer/pg-introspection";
10
+ import { createIRBuilder, freezeIR } from "../ir/semantic-ir.js";
11
+ import { Inflection } from "./inflection.js";
12
+ import { parseSmartTags } from "./smart-tags-parser.js";
13
+ /** Service tag */
14
+ export class IRBuilderSvc extends Context.Tag("IRBuilder")() {
15
+ }
16
+ // ============================================================================
17
+ // Shape Comparison
18
+ // ============================================================================
19
+ /**
20
+ * Compare two shapes for structural equality.
21
+ * Shapes are equal if they have the same fields with the same optionality.
22
+ * We compare by field name and optional flag since that's what differs between shapes.
23
+ */
24
+ function shapesEqual(a, b) {
25
+ if (a.fields.length !== b.fields.length)
26
+ return false;
27
+ for (let i = 0; i < a.fields.length; i++) {
28
+ const fieldA = a.fields[i];
29
+ const fieldB = b.fields[i];
30
+ if (fieldA === undefined || fieldB === undefined)
31
+ return false;
32
+ if (fieldA.name !== fieldB.name || fieldA.optional !== fieldB.optional)
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ // ============================================================================
38
+ // Field Building
39
+ // ============================================================================
40
+ /**
41
+ * Check if a shape kind should be omitted based on tags
42
+ */
43
+ function isOmittedForShape(tags, kind) {
44
+ if (tags.omit === true)
45
+ return true;
46
+ if (Array.isArray(tags.omit)) {
47
+ return tags.omit.includes(kind);
48
+ }
49
+ return false;
50
+ }
51
+ // ============================================================================
52
+ // Permissions
53
+ // ============================================================================
54
+ /**
55
+ * Compute field permissions from column ACL or fallback to table-level permissions.
56
+ *
57
+ * PostgreSQL ACL semantics:
58
+ * - If a column has explicit ACL (attacl is not null), column permissions ADD to table permissions
59
+ * - If a column has no explicit ACL (attacl is null), inherit from table-level permissions
60
+ *
61
+ * Column-level grants add to table-level grants (they don't replace them).
62
+ * For example: table SELECT + column UPDATE = both SELECT and UPDATE allowed.
63
+ */
64
+ function computeFieldPermissions(introspection, attr, role) {
65
+ const pgClass = attr.getClass();
66
+ if (!pgClass) {
67
+ return { canSelect: false, canInsert: false, canUpdate: false };
68
+ }
69
+ // Get table-level permissions (always needed)
70
+ const tablePerms = entityPermissions(introspection, pgClass, role);
71
+ // Check if column has explicit ACL
72
+ const hasExplicitColumnAcl = attr.attacl != null && attr.attacl.length > 0;
73
+ if (!hasExplicitColumnAcl) {
74
+ // No column-level ACL, use table permissions only
75
+ return {
76
+ canSelect: tablePerms.select ?? false,
77
+ canInsert: tablePerms.insert ?? false,
78
+ canUpdate: tablePerms.update ?? false,
79
+ };
80
+ }
81
+ // Column has explicit ACL - combine with table permissions (OR semantics)
82
+ const columnPerms = entityPermissions(introspection, attr, role);
83
+ return {
84
+ canSelect: (columnPerms.select ?? false) || (tablePerms.select ?? false),
85
+ canInsert: (columnPerms.insert ?? false) || (tablePerms.insert ?? false),
86
+ canUpdate: (columnPerms.update ?? false) || (tablePerms.update ?? false),
87
+ };
88
+ }
89
+ /**
90
+ * Compute entity permissions from table ACL
91
+ */
92
+ function computeEntityPermissions(introspection, pgClass, role) {
93
+ const perms = entityPermissions(introspection, pgClass, role);
94
+ // For SELECT, INSERT, UPDATE - also check if any column has the permission
95
+ // This handles the case where table-level ACL is null but column ACLs exist
96
+ const basePerms = {
97
+ canSelect: perms.select ?? false,
98
+ canInsert: perms.insert ?? false,
99
+ canUpdate: perms.update ?? false,
100
+ };
101
+ const attributes = pgClass.getAttributes().filter((a) => a.attnum > 0);
102
+ const columnPerms = Arr.reduce(attributes, basePerms, (acc, attr) => {
103
+ if (acc.canSelect && acc.canInsert && acc.canUpdate) {
104
+ return acc; // Already have all permissions
105
+ }
106
+ const attrPerms = entityPermissions(introspection, attr, role);
107
+ return {
108
+ canSelect: acc.canSelect || (attrPerms.select ?? false),
109
+ canInsert: acc.canInsert || (attrPerms.insert ?? false),
110
+ canUpdate: acc.canUpdate || (attrPerms.update ?? false),
111
+ };
112
+ });
113
+ return {
114
+ ...columnPerms,
115
+ canDelete: perms.delete ?? false,
116
+ };
117
+ }
118
+ /**
119
+ * Resolve domain base type information.
120
+ * If the type is a domain (typtype === 'd'), look up the underlying base type.
121
+ * This is needed for proper type mapping of domain types like `username` over `citext`.
122
+ */
123
+ function resolveDomainBaseType(pgType, introspection) {
124
+ return pipe(Option.fromNullable(pgType), Option.filter((t) => t.typtype === "d"), Option.flatMap((t) => Option.fromNullable(t.typbasetype)), Option.flatMap((baseTypeOid) => Option.fromNullable(introspection.getType({ id: String(baseTypeOid) }))), Option.flatMap((baseType) => {
125
+ // If the base type is also a domain, recursively resolve it
126
+ if (baseType.typtype === "d") {
127
+ return Option.fromNullable(resolveDomainBaseType(baseType, introspection));
128
+ }
129
+ return Option.some({
130
+ typeName: baseType.typname,
131
+ typeOid: Number(baseType._id),
132
+ namespaceOid: String(baseType.typnamespace ?? ""),
133
+ category: baseType.typcategory ?? "",
134
+ });
135
+ }), Option.getOrUndefined);
136
+ }
137
+ /**
138
+ * Build a Field from a PgAttribute
139
+ */
140
+ function buildField(attr, tags, kind, introspection, role) {
141
+ return Effect.gen(function* () {
142
+ const inflection = yield* Inflection;
143
+ const pgType = attr.getType();
144
+ // Array handling
145
+ const isArray = pgType?.typcategory === "A";
146
+ const elementType = isArray ? pgType?.getElemType() : undefined;
147
+ // Determine optionality based on shape kind and column properties
148
+ // Note: pg-introspection fields can be null, we default to false
149
+ const hasDefault = attr.atthasdef ?? false;
150
+ const isGenerated = (attr.attgenerated ?? "") !== "";
151
+ const isIdentity = (attr.attidentity ?? "") !== "";
152
+ const nullable = !(attr.attnotnull ?? false);
153
+ // In insert shape, fields with defaults are optional
154
+ // In update shape, all fields are optional
155
+ // In row shape, only nullable fields are optional
156
+ let optional;
157
+ switch (kind) {
158
+ case "insert":
159
+ optional = hasDefault || isGenerated || isIdentity || nullable;
160
+ break;
161
+ case "update":
162
+ optional = true;
163
+ break;
164
+ case "row":
165
+ default:
166
+ optional = nullable;
167
+ break;
168
+ }
169
+ // Compute field permissions from column ACL or fallback to table-level
170
+ const permissions = computeFieldPermissions(introspection, attr, role);
171
+ // Resolve domain base type for proper type mapping
172
+ const domainBaseType = resolveDomainBaseType(pgType, introspection);
173
+ const field = {
174
+ name: inflection.fieldName(attr, tags),
175
+ columnName: attr.attname,
176
+ pgAttribute: attr,
177
+ nullable,
178
+ optional,
179
+ hasDefault,
180
+ isGenerated,
181
+ isIdentity,
182
+ isArray,
183
+ tags,
184
+ extensions: new Map(),
185
+ permissions,
186
+ };
187
+ // Build result with optional properties (exactOptionalPropertyTypes)
188
+ let result = field;
189
+ if (elementType?.typname !== undefined) {
190
+ result = { ...result, elementTypeName: elementType.typname };
191
+ }
192
+ if (domainBaseType !== undefined) {
193
+ result = { ...result, domainBaseType };
194
+ }
195
+ return result;
196
+ });
197
+ }
198
+ /**
199
+ * Check if a field has the required permission for the given shape kind.
200
+ * - row shape requires canSelect
201
+ * - insert shape requires canInsert
202
+ * - update shape requires canUpdate
203
+ */
204
+ function hasPermissionForShape(permissions, kind) {
205
+ switch (kind) {
206
+ case "row":
207
+ return permissions.canSelect;
208
+ case "insert":
209
+ return permissions.canInsert;
210
+ case "update":
211
+ return permissions.canUpdate;
212
+ default:
213
+ return true;
214
+ }
215
+ }
216
+ /**
217
+ * Build a Shape from attributes
218
+ */
219
+ function buildShape(entityName, kind, attributes, attributeTags, introspection, role) {
220
+ return Effect.gen(function* () {
221
+ const inflection = yield* Inflection;
222
+ const filteredAttrs = pipe(attributes, Arr.filter((attr) => {
223
+ const tags = attributeTags.get(attr.attname) ?? {};
224
+ // Filter by @omit tags
225
+ if (isOmittedForShape(tags, kind))
226
+ return false;
227
+ // Filter by permissions - only include fields the role can access for this shape kind
228
+ const permissions = computeFieldPermissions(introspection, attr, role);
229
+ return hasPermissionForShape(permissions, kind);
230
+ }));
231
+ const fields = yield* Effect.forEach(filteredAttrs, (attr) => {
232
+ const tags = attributeTags.get(attr.attname) ?? {};
233
+ return buildField(attr, tags, kind, introspection, role);
234
+ });
235
+ return {
236
+ name: inflection.shapeName(entityName, kind),
237
+ kind,
238
+ fields,
239
+ };
240
+ });
241
+ }
242
+ // ============================================================================
243
+ // Entity Building
244
+ // ============================================================================
245
+ /**
246
+ * Determine entity kind from pg_class relkind
247
+ */
248
+ function entityKind(relkind) {
249
+ switch (relkind) {
250
+ case "r":
251
+ return "table";
252
+ case "v":
253
+ case "m": // materialized view
254
+ return "view";
255
+ default:
256
+ return "table";
257
+ }
258
+ }
259
+ /**
260
+ * Get primary key constraint from pgClass
261
+ */
262
+ function getPrimaryKeyConstraint(pgClass) {
263
+ return pipe(pgClass.getConstraints(), Arr.findFirst((c) => c.contype === "p"), Option.getOrUndefined);
264
+ }
265
+ /**
266
+ * Build primary key info from pgClass
267
+ */
268
+ function buildPrimaryKey(pgClass, tags) {
269
+ // Check for virtual PK from tags first (for views)
270
+ if (tags.primaryKey && tags.primaryKey.length > 0) {
271
+ return {
272
+ columns: tags.primaryKey,
273
+ isVirtual: true,
274
+ };
275
+ }
276
+ // Get real PK constraint
277
+ const pk = getPrimaryKeyConstraint(pgClass);
278
+ if (!pk)
279
+ return undefined;
280
+ const pkColumns = pk.getAttributes();
281
+ if (!pkColumns || pkColumns.length === 0)
282
+ return undefined;
283
+ return {
284
+ columns: pkColumns.map((a) => a.attname),
285
+ isVirtual: false,
286
+ };
287
+ }
288
+ /**
289
+ * Parse all attribute tags for a class
290
+ */
291
+ function parseAttributeTags(pgClass) {
292
+ const attributes = pgClass.getAttributes().filter((a) => a.attnum > 0);
293
+ return Effect.reduce(attributes, new Map(), (map, attr) => {
294
+ const context = {
295
+ objectType: "column",
296
+ objectName: `${pgClass.relname}.${attr.attname}`,
297
+ };
298
+ return parseSmartTags(attr.getDescription(), context).pipe(Effect.map((parsed) => {
299
+ map.set(attr.attname, parsed.tags);
300
+ return map;
301
+ }));
302
+ });
303
+ }
304
+ /**
305
+ * Build a TableEntity from a PgClass
306
+ */
307
+ function buildEntity(pgClass, entityNameLookup, introspection, role) {
308
+ const context = {
309
+ objectType: "table",
310
+ objectName: pgClass.relname,
311
+ };
312
+ return Effect.gen(function* () {
313
+ const inflection = yield* Inflection;
314
+ // Parse table tags
315
+ const tableParsed = yield* parseSmartTags(pgClass.getDescription(), context);
316
+ const tableTags = tableParsed.tags;
317
+ // Parse all column tags
318
+ const attributeTags = yield* parseAttributeTags(pgClass);
319
+ const name = inflection.entityName(pgClass, tableTags);
320
+ const kind = entityKind(pgClass.relkind);
321
+ const schemaName = pgClass.getNamespace()?.nspname ?? "public";
322
+ // Get visible attributes (attnum > 0 excludes system columns)
323
+ const attributes = pgClass.getAttributes().filter((a) => a.attnum > 0);
324
+ // Build shapes - now yields since buildShape returns Effect
325
+ const rowShape = yield* buildShape(name, "row", attributes, attributeTags, introspection, role);
326
+ // Build relations from foreign keys
327
+ const relations = yield* buildRelations(pgClass, entityNameLookup);
328
+ // Build indexes
329
+ const indexes = yield* buildIndexes(pgClass);
330
+ // Build primary key
331
+ const primaryKey = buildPrimaryKey(pgClass, tableTags);
332
+ // Compute entity permissions
333
+ const permissions = computeEntityPermissions(introspection, pgClass, role);
334
+ // Build shapes object conditionally:
335
+ // - Views only get row shape
336
+ // - Tables get insert/update only if:
337
+ // 1. They have fields (role has permission)
338
+ // 2. They're structurally different from previous shape
339
+ // - Patch is always identical to update (both have all fields optional), so we never emit it
340
+ let shapes;
341
+ if (kind === "table") {
342
+ const insertShape = yield* buildShape(name, "insert", attributes, attributeTags, introspection, role);
343
+ const updateShape = yield* buildShape(name, "update", attributes, attributeTags, introspection, role);
344
+ // Only include insert if it has fields and is different from row
345
+ const includeInsert = insertShape.fields.length > 0 && !shapesEqual(rowShape, insertShape);
346
+ // Only include update if it has fields and is different from insert (or row if insert not included)
347
+ const includeUpdate = updateShape.fields.length > 0 && (includeInsert
348
+ ? !shapesEqual(insertShape, updateShape)
349
+ : !shapesEqual(rowShape, updateShape));
350
+ if (includeInsert && includeUpdate) {
351
+ shapes = { row: rowShape, insert: insertShape, update: updateShape };
352
+ }
353
+ else if (includeInsert) {
354
+ shapes = { row: rowShape, insert: insertShape };
355
+ }
356
+ else if (includeUpdate) {
357
+ shapes = { row: rowShape, update: updateShape };
358
+ }
359
+ else {
360
+ shapes = { row: rowShape };
361
+ }
362
+ }
363
+ else {
364
+ shapes = { row: rowShape };
365
+ }
366
+ // Build entity conditionally to satisfy exactOptionalPropertyTypes
367
+ const baseEntity = {
368
+ name,
369
+ pgName: pgClass.relname,
370
+ schemaName,
371
+ kind,
372
+ pgClass,
373
+ shapes,
374
+ relations,
375
+ indexes,
376
+ tags: tableTags,
377
+ permissions,
378
+ };
379
+ // Only include primaryKey if defined
380
+ const entity = primaryKey !== undefined
381
+ ? { ...baseEntity, primaryKey }
382
+ : baseEntity;
383
+ return entity;
384
+ });
385
+ }
386
+ // ============================================================================
387
+ // Relations
388
+ // ============================================================================
389
+ /**
390
+ * Build relations from foreign key constraints
391
+ */
392
+ function buildRelations(pgClass, entityNameLookup) {
393
+ const fks = pgClass.getConstraints().filter((c) => c.contype === "f");
394
+ return Effect.forEach(fks, (fk) => buildRelation(fk, entityNameLookup));
395
+ }
396
+ /**
397
+ * Build a single relation from a FK constraint
398
+ */
399
+ function buildRelation(fk, entityNameLookup) {
400
+ const context = {
401
+ objectType: "constraint",
402
+ objectName: fk.conname,
403
+ };
404
+ return Effect.gen(function* () {
405
+ const inflection = yield* Inflection;
406
+ const parsed = yield* parseSmartTags(fk.getDescription(), context);
407
+ const constraintTags = parsed.tags;
408
+ // Get the foreign table
409
+ const foreignClass = fk.getForeignClass();
410
+ const foreignOid = foreignClass?._id ?? "";
411
+ // Look up the entity name for the foreign table
412
+ const targetEntity = entityNameLookup.get(foreignOid) ?? foreignClass?.relname ?? "Unknown";
413
+ // Get column mappings
414
+ const localAttrs = fk.getAttributes() ?? [];
415
+ const foreignAttrs = fk.getForeignAttributes() ?? [];
416
+ const columns = localAttrs.map((local, i) => ({
417
+ local: local.attname,
418
+ foreign: foreignAttrs[i]?.attname ?? local.attname,
419
+ }));
420
+ // This is the "local" side - we have the FK, so we "belong to" the foreign table
421
+ return {
422
+ kind: "belongsTo",
423
+ targetEntity,
424
+ constraintName: fk.conname,
425
+ columns,
426
+ tags: constraintTags,
427
+ };
428
+ });
429
+ }
430
+ // ============================================================================
431
+ // Indexes
432
+ // ============================================================================
433
+ /**
434
+ * Get the index method from the index class's access method
435
+ */
436
+ function getIndexMethod(pgClass) {
437
+ const accessMethod = pgClass.getAccessMethod();
438
+ if (!accessMethod || !accessMethod.amname) {
439
+ return "btree"; // Default to btree if unknown
440
+ }
441
+ // Map common access method names to our IndexMethod type
442
+ const methodName = accessMethod.amname.toLowerCase();
443
+ switch (methodName) {
444
+ case "btree":
445
+ case "gin":
446
+ case "gist":
447
+ case "hash":
448
+ case "brin":
449
+ case "spgist":
450
+ return methodName;
451
+ default:
452
+ return "btree";
453
+ }
454
+ }
455
+ /**
456
+ * Build IndexDef objects from pg-introspection indexes
457
+ */
458
+ function buildIndexes(pgClass) {
459
+ return Effect.gen(function* () {
460
+ const inflection = yield* Inflection;
461
+ const indexes = pgClass.getIndexes();
462
+ return indexes.map((index) => {
463
+ const indexClass = index.getIndexClass();
464
+ const keys = index.getKeys();
465
+ // Check for expressions (null entries in keys array)
466
+ const hasExpressions = keys.some((k) => k === null);
467
+ // Get column names (filter out nulls for expression columns)
468
+ const columnAttrs = keys.filter((k) => k !== null);
469
+ const columns = columnAttrs.map((attr) => inflection.fieldName(attr, {}));
470
+ const columnNames = columnAttrs.map((attr) => attr.attname);
471
+ // Determine if this is a primary key index (via constraint)
472
+ const isPrimary = index.indisprimary === true;
473
+ // Build the base index definition
474
+ const indexDef = {
475
+ name: indexClass?.relname ?? "unknown",
476
+ columns,
477
+ columnNames,
478
+ isUnique: index.indisunique === true,
479
+ isPrimary,
480
+ isPartial: index.indpred !== null && index.indpred.length > 0,
481
+ method: indexClass ? getIndexMethod(indexClass) : "btree",
482
+ hasExpressions,
483
+ opclassNames: index.indclassnames ?? [],
484
+ };
485
+ // Add predicate only for partial indexes (exactOptionalPropertyTypes)
486
+ if (indexDef.isPartial && index.indpred) {
487
+ return { ...indexDef, predicate: index.indpred };
488
+ }
489
+ return indexDef;
490
+ });
491
+ });
492
+ }
493
+ // ============================================================================
494
+ // Enums
495
+ // ============================================================================
496
+ /**
497
+ * Build an EnumEntity from a PgType
498
+ */
499
+ function buildEnum(pgType) {
500
+ const context = {
501
+ objectType: "type",
502
+ objectName: pgType.typname,
503
+ };
504
+ return Effect.gen(function* () {
505
+ const inflection = yield* Inflection;
506
+ const parsed = yield* parseSmartTags(pgType.getDescription(), context);
507
+ const tags = parsed.tags;
508
+ const schemaName = pgType.getNamespace()?.nspname ?? "public";
509
+ const values = pgType.getEnumValues()?.map((v) => v.enumlabel) ?? [];
510
+ return {
511
+ kind: "enum",
512
+ name: inflection.enumName(pgType, tags),
513
+ pgName: pgType.typname,
514
+ schemaName,
515
+ pgType,
516
+ values,
517
+ tags,
518
+ };
519
+ });
520
+ }
521
+ // ============================================================================
522
+ // Domains
523
+ // ============================================================================
524
+ /**
525
+ * Get domain constraints from pg_constraint.
526
+ * Domain constraints have contypid set to the domain's OID.
527
+ */
528
+ function getDomainConstraints(pgType, introspection) {
529
+ // Find constraints where contypid matches the domain type's OID
530
+ const domainOid = pgType._id;
531
+ return introspection.constraints
532
+ .filter((c) => c.contypid === domainOid && c.contype === "c") // CHECK constraints
533
+ .map((c) => {
534
+ const constraint = {
535
+ name: c.conname,
536
+ };
537
+ // Add expression only if present (exactOptionalPropertyTypes)
538
+ if (c.consrc) {
539
+ return { ...constraint, expression: c.consrc };
540
+ }
541
+ return constraint;
542
+ });
543
+ }
544
+ /**
545
+ * Build a DomainEntity from a PgType
546
+ */
547
+ function buildDomain(pgType, introspection) {
548
+ const context = {
549
+ objectType: "type",
550
+ objectName: pgType.typname,
551
+ };
552
+ return Effect.gen(function* () {
553
+ const inflection = yield* Inflection;
554
+ const parsed = yield* parseSmartTags(pgType.getDescription(), context);
555
+ const tags = parsed.tags;
556
+ const schemaName = pgType.getNamespace()?.nspname ?? "public";
557
+ // Get base type info
558
+ const baseTypeOid = pgType.typbasetype;
559
+ const baseType = baseTypeOid
560
+ ? introspection.getType({ id: String(baseTypeOid) })
561
+ : undefined;
562
+ const baseTypeName = baseType?.typname ?? "unknown";
563
+ // Check for NOT NULL constraint (typnotnull)
564
+ const notNull = pgType.typnotnull === true;
565
+ // Get CHECK constraints
566
+ const constraints = getDomainConstraints(pgType, introspection);
567
+ return {
568
+ kind: "domain",
569
+ name: inflection.enumName(pgType, tags), // enumName works for all PgType
570
+ pgName: pgType.typname,
571
+ schemaName,
572
+ pgType,
573
+ baseTypeName,
574
+ baseTypeOid: baseTypeOid ? Number(baseTypeOid) : 0,
575
+ notNull,
576
+ constraints,
577
+ tags,
578
+ };
579
+ });
580
+ }
581
+ // ============================================================================
582
+ // Composites
583
+ // ============================================================================
584
+ /**
585
+ * Build a Field from a PgAttribute for composite types.
586
+ * Sets sensible defaults for properties that don't apply to composites.
587
+ */
588
+ function buildCompositeField(attr, tags, introspection) {
589
+ return Effect.gen(function* () {
590
+ const inflection = yield* Inflection;
591
+ const pgType = attr.getType();
592
+ // Array handling
593
+ const isArray = pgType?.typcategory === "A";
594
+ const elementType = isArray ? pgType?.getElemType() : undefined;
595
+ // Resolve domain base type for proper type mapping
596
+ const domainBaseType = resolveDomainBaseType(pgType, introspection);
597
+ const nullable = !(attr.attnotnull ?? false);
598
+ // Composite fields use Field interface with defaults for inapplicable properties
599
+ const field = {
600
+ name: inflection.fieldName(attr, tags),
601
+ columnName: attr.attname,
602
+ pgAttribute: attr,
603
+ nullable,
604
+ optional: false, // Composites don't have optional fields
605
+ hasDefault: false, // Composites don't have defaults
606
+ isGenerated: false,
607
+ isIdentity: false,
608
+ isArray,
609
+ tags,
610
+ extensions: new Map(),
611
+ permissions: { canSelect: true, canInsert: true, canUpdate: true },
612
+ };
613
+ // Build result with optional properties (exactOptionalPropertyTypes)
614
+ let result = field;
615
+ if (elementType?.typname !== undefined) {
616
+ result = { ...result, elementTypeName: elementType.typname };
617
+ }
618
+ if (domainBaseType !== undefined) {
619
+ result = { ...result, domainBaseType };
620
+ }
621
+ return result;
622
+ });
623
+ }
624
+ /**
625
+ * Build a CompositeEntity from a PgType
626
+ */
627
+ function buildComposite(pgType, introspection) {
628
+ const context = {
629
+ objectType: "type",
630
+ objectName: pgType.typname,
631
+ };
632
+ return Effect.gen(function* () {
633
+ const inflection = yield* Inflection;
634
+ const parsed = yield* parseSmartTags(pgType.getDescription(), context);
635
+ const tags = parsed.tags;
636
+ const schemaName = pgType.getNamespace()?.nspname ?? "public";
637
+ // Get attributes for the composite type via its associated pg_class
638
+ // Composite types have a pg_class entry with relkind = 'c'
639
+ const pgClass = pgType.getClass();
640
+ const attributes = pgClass?.getAttributes()?.filter((a) => a.attnum > 0) ?? [];
641
+ // Parse attribute tags and build fields
642
+ const fields = yield* Effect.forEach(attributes, (attr) => {
643
+ const attrContext = {
644
+ objectType: "column",
645
+ objectName: `${pgType.typname}.${attr.attname}`,
646
+ };
647
+ return parseSmartTags(attr.getDescription(), attrContext).pipe(Effect.flatMap((attrParsed) => buildCompositeField(attr, attrParsed.tags, introspection)));
648
+ });
649
+ return {
650
+ kind: "composite",
651
+ name: inflection.enumName(pgType, tags), // enumName works for all PgType
652
+ pgName: pgType.typname,
653
+ schemaName,
654
+ pgType,
655
+ fields,
656
+ tags,
657
+ };
658
+ });
659
+ }
660
+ // ============================================================================
661
+ // Main Builder
662
+ // ============================================================================
663
+ /**
664
+ * Build entity name lookup map (oid -> entity name)
665
+ * This is needed for relation building to know target entity names
666
+ */
667
+ function buildEntityNameLookup(classes) {
668
+ return Effect.gen(function* () {
669
+ const inflection = yield* Inflection;
670
+ return yield* Effect.reduce(classes, new Map(), (map, pgClass) => {
671
+ const context = {
672
+ objectType: "table",
673
+ objectName: pgClass.relname,
674
+ };
675
+ return parseSmartTags(pgClass.getDescription(), context).pipe(Effect.map((parsed) => {
676
+ const name = inflection.entityName(pgClass, parsed.tags);
677
+ map.set(pgClass._id, name);
678
+ return map;
679
+ }));
680
+ });
681
+ });
682
+ }
683
+ /**
684
+ * Filter classes to include (tables and views in specified schemas)
685
+ */
686
+ function filterClasses(introspection, schemas) {
687
+ const schemaSet = new Set(schemas);
688
+ return introspection.classes.filter((c) => {
689
+ const namespace = c.getNamespace()?.nspname;
690
+ if (!namespace || !schemaSet.has(namespace))
691
+ return false;
692
+ // Include tables, views, materialized views
693
+ return c.relkind === "r" || c.relkind === "v" || c.relkind === "m";
694
+ });
695
+ }
696
+ /**
697
+ * Filter enum types in specified schemas
698
+ */
699
+ function filterEnums(introspection, schemas) {
700
+ const schemaSet = new Set(schemas);
701
+ return introspection.types.filter((t) => {
702
+ const namespace = t.getNamespace()?.nspname;
703
+ if (!namespace || !schemaSet.has(namespace))
704
+ return false;
705
+ return t.typtype === "e"; // enum type
706
+ });
707
+ }
708
+ /**
709
+ * Filter domain types in specified schemas
710
+ */
711
+ function filterDomains(introspection, schemas) {
712
+ const schemaSet = new Set(schemas);
713
+ return introspection.types.filter((t) => {
714
+ const namespace = t.getNamespace()?.nspname;
715
+ if (!namespace || !schemaSet.has(namespace))
716
+ return false;
717
+ return t.typtype === "d"; // domain type
718
+ });
719
+ }
720
+ /**
721
+ * Filter user-defined composite types in specified schemas.
722
+ * Excludes table/view row types (those have relkind = 'r' or 'v').
723
+ * Only includes composites with relkind = 'c' (standalone composite types).
724
+ */
725
+ function filterComposites(introspection, schemas) {
726
+ const schemaSet = new Set(schemas);
727
+ return introspection.types.filter((t) => {
728
+ const namespace = t.getNamespace()?.nspname;
729
+ if (!namespace || !schemaSet.has(namespace))
730
+ return false;
731
+ // Composite types have typtype === "c"
732
+ if (t.typtype !== "c")
733
+ return false;
734
+ // Get the associated class to check its relkind
735
+ // User-defined composites have relkind = 'c'
736
+ // Table row types have relkind = 'r', view row types have relkind = 'v'
737
+ const pgClass = t.getClass();
738
+ return pgClass?.relkind === "c";
739
+ });
740
+ }
741
+ /**
742
+ * Extract extension info from introspection.
743
+ * Extensions are needed for type mapping (e.g., citext -> string).
744
+ */
745
+ function extractExtensions(introspection) {
746
+ return introspection.extensions.map((ext) => ({
747
+ name: ext.extname,
748
+ namespaceOid: String(ext.extnamespace ?? ""),
749
+ version: ext.extversion,
750
+ }));
751
+ }
752
+ /**
753
+ * Get the fully qualified type name from a PgType.
754
+ */
755
+ function getTypeName(pgType) {
756
+ return pipe(Option.fromNullable(pgType), Option.map((t) => {
757
+ const ns = t.getNamespace()?.nspname;
758
+ return ns ? `${ns}.${t.typname}` : t.typname;
759
+ }), Option.getOrElse(() => "unknown"));
760
+ }
761
+ /**
762
+ * Filter functions in specified schemas.
763
+ * Only includes functions (prokind = 'f'), not procedures.
764
+ *
765
+ * @param introspection - The introspection result
766
+ * @param schemas - Schemas to include functions from
767
+ * @param options - Optional filter settings
768
+ */
769
+ function filterFunctions(introspection, schemas, options) {
770
+ const schemaSet = new Set(schemas);
771
+ const excludeExtensions = options?.excludeExtensions ?? true;
772
+ // Build set of extension namespace OIDs (always build this for tracking)
773
+ const extensionNamespaceOids = new Set(introspection.extensions
774
+ .filter((ext) => ext.extnamespace)
775
+ .map((ext) => ext.extnamespace));
776
+ return introspection.procs
777
+ .map((proc) => {
778
+ const namespace = proc.getNamespace();
779
+ if (!namespace)
780
+ return null;
781
+ const namespaceName = namespace.nspname;
782
+ if (!namespaceName || !schemaSet.has(namespaceName))
783
+ return null;
784
+ // Only include functions, not procedures
785
+ if (proc.prokind !== "f")
786
+ return null;
787
+ // Check if function belongs to an extension
788
+ const isFromExtension = extensionNamespaceOids.has(namespace._id);
789
+ // Exclude extension functions
790
+ if (excludeExtensions && isFromExtension) {
791
+ return null;
792
+ }
793
+ return { pgProc: proc, isFromExtension };
794
+ })
795
+ .filter((item) => item !== null);
796
+ }
797
+ /**
798
+ * Build a FunctionEntity from a PgProc.
799
+ */
800
+ function buildFunction(pgProc, introspection, role, isFromExtension) {
801
+ const context = {
802
+ objectType: "type",
803
+ objectName: pgProc.proname,
804
+ };
805
+ return Effect.gen(function* () {
806
+ const inflection = yield* Inflection;
807
+ const parsed = yield* parseSmartTags(pgProc.getDescription(), context);
808
+ const tags = parsed.tags;
809
+ const schemaName = pgProc.getNamespace()?.nspname ?? "public";
810
+ const returnType = pgProc.getReturnType();
811
+ const returnTypeName = returnType ? getTypeName(returnType) : "void";
812
+ const args = pgProc.getArguments().map((arg) => ({
813
+ name: arg.name ?? "",
814
+ typeName: getTypeName(arg.type),
815
+ hasDefault: arg.hasDefault,
816
+ }));
817
+ const volatilityMap = {
818
+ i: "immutable",
819
+ s: "stable",
820
+ v: "volatile",
821
+ };
822
+ const perms = entityPermissions(introspection, pgProc, role);
823
+ const canExecute = perms.execute ?? false;
824
+ const volatilityKey = pgProc.provolatile ?? "v";
825
+ const volatility = volatilityMap[volatilityKey] ?? "volatile";
826
+ return {
827
+ kind: "function",
828
+ name: inflection.functionName(pgProc, tags),
829
+ pgName: pgProc.proname,
830
+ schemaName,
831
+ pgProc,
832
+ returnTypeName,
833
+ returnsSet: pgProc.proretset ?? false,
834
+ argCount: pgProc.pronargs ?? 0,
835
+ args,
836
+ volatility,
837
+ isStrict: pgProc.proisstrict ?? false,
838
+ canExecute,
839
+ isFromExtension,
840
+ tags,
841
+ };
842
+ });
843
+ }
844
+ /**
845
+ * Create the live IR builder implementation
846
+ */
847
+ function createIRBuilderImpl() {
848
+ return {
849
+ build: (introspection, options) => Effect.gen(function* () {
850
+ const classes = filterClasses(introspection, options.schemas);
851
+ const enumTypes = filterEnums(introspection, options.schemas);
852
+ const domainTypes = filterDomains(introspection, options.schemas);
853
+ const compositeTypes = filterComposites(introspection, options.schemas);
854
+ const functionProcs = filterFunctions(introspection, options.schemas, {
855
+ excludeExtensions: options.excludeExtensionFunctions ?? true,
856
+ });
857
+ // Get the role for permission checks
858
+ // If options.role is specified, look it up. Otherwise fall back to current user.
859
+ const fallbackRole = {
860
+ rolname: "unknown",
861
+ rolsuper: false,
862
+ rolinherit: false,
863
+ rolcreaterole: false,
864
+ rolcreatedb: false,
865
+ rolcanlogin: false,
866
+ rolreplication: false,
867
+ rolconnlimit: -1,
868
+ rolpassword: null,
869
+ rolvaliduntil: null,
870
+ rolbypassrls: false,
871
+ rolconfig: null,
872
+ _id: "0",
873
+ };
874
+ const role = options.role
875
+ ? introspection.roles.find((r) => r.rolname === options.role) ?? fallbackRole
876
+ : introspection.getCurrentUser() ?? fallbackRole;
877
+ // Build entity name lookup first (needed for relations)
878
+ const entityNameLookup = yield* buildEntityNameLookup(classes);
879
+ // Build table/view entities
880
+ const entities = yield* Effect.forEach(classes, (pgClass) => buildEntity(pgClass, entityNameLookup, introspection, role));
881
+ // Build enums
882
+ const enums = yield* Effect.forEach(enumTypes, (pgType) => buildEnum(pgType));
883
+ // Build domains
884
+ const domains = yield* Effect.forEach(domainTypes, (pgType) => buildDomain(pgType, introspection));
885
+ // Build composites
886
+ const composites = yield* Effect.forEach(compositeTypes, (pgType) => buildComposite(pgType, introspection));
887
+ // Build functions
888
+ const functions = yield* Effect.forEach(functionProcs, ({ pgProc, isFromExtension }) => buildFunction(pgProc, introspection, role, isFromExtension));
889
+ // Extract extensions for type mapping
890
+ const extensions = extractExtensions(introspection);
891
+ // Assemble IR
892
+ const builder = createIRBuilder(options.schemas);
893
+ const allEntities = [
894
+ ...entities,
895
+ ...enums,
896
+ ...domains,
897
+ ...composites,
898
+ ...functions,
899
+ ];
900
+ Arr.forEach(allEntities, (entity) => {
901
+ builder.entities.set(entity.name, entity);
902
+ });
903
+ builder.extensions.push(...extensions);
904
+ return freezeIR(builder);
905
+ }),
906
+ };
907
+ }
908
+ // ============================================================================
909
+ // Layers
910
+ // ============================================================================
911
+ /**
912
+ * Live layer - provides IRBuilder service
913
+ * Note: IRBuilder.build() requires Inflection to be provided at call time
914
+ */
915
+ export const IRBuilderLive = Layer.succeed(IRBuilderSvc, createIRBuilderImpl());
916
+ /**
917
+ * Factory function for creating IR builder
918
+ * Note: The returned builder's build() method requires Inflection in the Effect context
919
+ */
920
+ export function createIRBuilderService() {
921
+ return createIRBuilderImpl();
922
+ }
923
+ //# sourceMappingURL=ir-builder.js.map