@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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +133 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +129 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/generate.d.ts +75 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +183 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +229 -0
- package/dist/init.js.map +1 -0
- package/dist/ir/index.d.ts +7 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +7 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/relation-graph.d.ts +113 -0
- package/dist/ir/relation-graph.d.ts.map +1 -0
- package/dist/ir/relation-graph.js +232 -0
- package/dist/ir/relation-graph.js.map +1 -0
- package/dist/ir/semantic-ir.d.ts +448 -0
- package/dist/ir/semantic-ir.d.ts.map +1 -0
- package/dist/ir/semantic-ir.js +138 -0
- package/dist/ir/semantic-ir.js.map +1 -0
- package/dist/ir/smart-tags.d.ts +24 -0
- package/dist/ir/smart-tags.d.ts.map +1 -0
- package/dist/ir/smart-tags.js +30 -0
- package/dist/ir/smart-tags.js.map +1 -0
- package/dist/lib/conjure.d.ts +431 -0
- package/dist/lib/conjure.d.ts.map +1 -0
- package/dist/lib/conjure.js +697 -0
- package/dist/lib/conjure.js.map +1 -0
- package/dist/lib/field-utils.d.ts +61 -0
- package/dist/lib/field-utils.d.ts.map +1 -0
- package/dist/lib/field-utils.js +132 -0
- package/dist/lib/field-utils.js.map +1 -0
- package/dist/lib/hex.d.ts +117 -0
- package/dist/lib/hex.d.ts.map +1 -0
- package/dist/lib/hex.js +185 -0
- package/dist/lib/hex.js.map +1 -0
- package/dist/plugins/arktype.d.ts +11 -0
- package/dist/plugins/arktype.d.ts.map +1 -0
- package/dist/plugins/arktype.js +207 -0
- package/dist/plugins/arktype.js.map +1 -0
- package/dist/plugins/effect-model.d.ts +10 -0
- package/dist/plugins/effect-model.d.ts.map +1 -0
- package/dist/plugins/effect-model.js +261 -0
- package/dist/plugins/effect-model.js.map +1 -0
- package/dist/plugins/kysely-queries.d.ts +7 -0
- package/dist/plugins/kysely-queries.d.ts.map +1 -0
- package/dist/plugins/kysely-queries.js +380 -0
- package/dist/plugins/kysely-queries.js.map +1 -0
- package/dist/plugins/sql-queries.d.ts +6 -0
- package/dist/plugins/sql-queries.d.ts.map +1 -0
- package/dist/plugins/sql-queries.js +249 -0
- package/dist/plugins/sql-queries.js.map +1 -0
- package/dist/plugins/types.d.ts +18 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +263 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/plugins/zod.d.ts +11 -0
- package/dist/plugins/zod.d.ts.map +1 -0
- package/dist/plugins/zod.js +180 -0
- package/dist/plugins/zod.js.map +1 -0
- package/dist/services/artifact-store.d.ts +55 -0
- package/dist/services/artifact-store.d.ts.map +1 -0
- package/dist/services/artifact-store.js +51 -0
- package/dist/services/artifact-store.js.map +1 -0
- package/dist/services/config-loader.d.ts +45 -0
- package/dist/services/config-loader.d.ts.map +1 -0
- package/dist/services/config-loader.js +113 -0
- package/dist/services/config-loader.js.map +1 -0
- package/dist/services/emissions.d.ts +89 -0
- package/dist/services/emissions.d.ts.map +1 -0
- package/dist/services/emissions.js +194 -0
- package/dist/services/emissions.js.map +1 -0
- package/dist/services/file-builder.d.ts +81 -0
- package/dist/services/file-builder.d.ts.map +1 -0
- package/dist/services/file-builder.js +112 -0
- package/dist/services/file-builder.js.map +1 -0
- package/dist/services/file-writer.d.ts +57 -0
- package/dist/services/file-writer.d.ts.map +1 -0
- package/dist/services/file-writer.js +76 -0
- package/dist/services/file-writer.js.map +1 -0
- package/dist/services/inflection.d.ts +227 -0
- package/dist/services/inflection.d.ts.map +1 -0
- package/dist/services/inflection.js +350 -0
- package/dist/services/inflection.js.map +1 -0
- package/dist/services/introspection.d.ts +46 -0
- package/dist/services/introspection.d.ts.map +1 -0
- package/dist/services/introspection.js +99 -0
- package/dist/services/introspection.js.map +1 -0
- package/dist/services/ir-builder.d.ts +46 -0
- package/dist/services/ir-builder.d.ts.map +1 -0
- package/dist/services/ir-builder.js +923 -0
- package/dist/services/ir-builder.js.map +1 -0
- package/dist/services/ir.d.ts +28 -0
- package/dist/services/ir.d.ts.map +1 -0
- package/dist/services/ir.js +25 -0
- package/dist/services/ir.js.map +1 -0
- package/dist/services/pg-types.d.ts +197 -0
- package/dist/services/pg-types.d.ts.map +1 -0
- package/dist/services/pg-types.js +274 -0
- package/dist/services/pg-types.js.map +1 -0
- package/dist/services/plugin-meta.d.ts +33 -0
- package/dist/services/plugin-meta.d.ts.map +1 -0
- package/dist/services/plugin-meta.js +24 -0
- package/dist/services/plugin-meta.js.map +1 -0
- package/dist/services/plugin-runner.d.ts +52 -0
- package/dist/services/plugin-runner.d.ts.map +1 -0
- package/dist/services/plugin-runner.js +182 -0
- package/dist/services/plugin-runner.js.map +1 -0
- package/dist/services/plugin.d.ts +286 -0
- package/dist/services/plugin.d.ts.map +1 -0
- package/dist/services/plugin.js +132 -0
- package/dist/services/plugin.js.map +1 -0
- package/dist/services/smart-tags-parser.d.ts +37 -0
- package/dist/services/smart-tags-parser.d.ts.map +1 -0
- package/dist/services/smart-tags-parser.js +79 -0
- package/dist/services/smart-tags-parser.js.map +1 -0
- package/dist/services/symbols.d.ts +85 -0
- package/dist/services/symbols.d.ts.map +1 -0
- package/dist/services/symbols.js +128 -0
- package/dist/services/symbols.js.map +1 -0
- package/dist/services/type-hints.d.ts +62 -0
- package/dist/services/type-hints.d.ts.map +1 -0
- package/dist/services/type-hints.js +117 -0
- package/dist/services/type-hints.js.map +1 -0
- package/dist/testing.d.ts +77 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +84 -0
- package/dist/testing.js.map +1 -0
- 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
|