true-pg 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +12 -8
  2. package/lib/bin.js +3 -7
  3. package/lib/extractor/adapter.d.ts +15 -0
  4. package/lib/extractor/adapter.js +53 -0
  5. package/lib/extractor/canonicalise.d.ts +64 -0
  6. package/lib/extractor/canonicalise.js +245 -0
  7. package/lib/extractor/fetchExtensionItemIds.d.ts +12 -0
  8. package/lib/extractor/fetchExtensionItemIds.js +43 -0
  9. package/lib/extractor/fetchTypes.d.ts +4 -0
  10. package/lib/extractor/fetchTypes.js +65 -0
  11. package/lib/extractor/index.d.ts +95 -0
  12. package/lib/extractor/index.js +140 -0
  13. package/lib/extractor/kinds/composite.d.ts +15 -0
  14. package/lib/extractor/kinds/composite.js +13 -0
  15. package/lib/extractor/kinds/domain.d.ts +15 -0
  16. package/lib/extractor/kinds/domain.js +13 -0
  17. package/lib/extractor/kinds/enum.d.ts +9 -0
  18. package/lib/extractor/kinds/enum.js +20 -0
  19. package/lib/extractor/kinds/function.d.ts +73 -0
  20. package/lib/extractor/kinds/function.js +179 -0
  21. package/lib/extractor/kinds/materialized-view.d.ts +17 -0
  22. package/lib/extractor/kinds/materialized-view.js +64 -0
  23. package/lib/extractor/kinds/parts/commentMapQueryPart.d.ts +2 -0
  24. package/lib/extractor/kinds/parts/commentMapQueryPart.js +14 -0
  25. package/lib/extractor/kinds/parts/indexMapQueryPart.d.ts +2 -0
  26. package/lib/extractor/kinds/parts/indexMapQueryPart.js +27 -0
  27. package/lib/extractor/kinds/range.d.ts +15 -0
  28. package/lib/extractor/kinds/range.js +13 -0
  29. package/lib/extractor/kinds/table.d.ts +212 -0
  30. package/lib/extractor/kinds/table.js +217 -0
  31. package/lib/extractor/kinds/util/parseInlineTable.d.ts +9 -0
  32. package/lib/extractor/kinds/util/parseInlineTable.js +135 -0
  33. package/lib/extractor/kinds/util/parseInlineTable.test.d.ts +1 -0
  34. package/lib/extractor/kinds/util/parseInlineTable.test.js +26 -0
  35. package/lib/extractor/kinds/view.d.ts +17 -0
  36. package/lib/extractor/kinds/view.js +63 -0
  37. package/lib/extractor/pgtype.d.ts +41 -0
  38. package/lib/extractor/pgtype.js +30 -0
  39. package/lib/index.d.ts +2 -2
  40. package/lib/index.js +38 -28
  41. package/lib/kysely/builtins.js +6 -4
  42. package/lib/kysely/index.js +103 -59
  43. package/lib/types.d.ts +38 -10
  44. package/lib/types.js +14 -3
  45. package/lib/util.d.ts +11 -0
  46. package/lib/util.js +6 -0
  47. package/lib/zod/builtins.js +11 -9
  48. package/lib/zod/index.js +107 -60
  49. package/package.json +3 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # true-pg
2
2
 
3
- A truthful and complete<sup>†</sup> TypeScript code generator for PostgreSQL database schemas.
3
+ A truthful and complete[1] TypeScript code generator for PostgreSQL database schemas.
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,12 +22,12 @@ true-pg [options]
22
22
 
23
23
  Options:
24
24
 
25
- - `-h, --help` - Show help information
26
- - `-c, --config [path]` - Path to config file (JSON)
27
- - `-u, --uri [uri]` - Database URI (Postgres only!)
28
- - `-o, --out [path]` - Path to output directory (defaults to "models")
29
- - `-a, --adapter [adapter]` - Adapter to use (e.g. `kysely`, `zod`). Can be specified multiple times.
30
- - `-A, --all-adapters` - Enable all built-in adapters
25
+ - `-h, --help` - Show help information
26
+ - `-c, --config [path]` - Path to config file (JSON)
27
+ - `-u, --uri [uri]` - Database URI (Postgres only!)
28
+ - `-o, --out [path]` - Path to output directory (defaults to "models")
29
+ - `-a, --adapter [adapter]` - Adapter to use (e.g. `kysely`, `zod`). Can be specified multiple times.
30
+ - `-A, --all-adapters` - Enable all built-in adapters
31
31
 
32
32
  You can configure true-pg either through command-line arguments or a config file.
33
33
 
@@ -120,8 +120,12 @@ The `SchemaGenerator` interface provides methods to customize code generation:
120
120
  | `formatSchemaType(type)` | Formats schema type names (user_sessions -> UserSessions) |
121
121
  | `formatType(type)` | Formats type names (pg_catalog.int4 -> number) |
122
122
  | `table(types, table)` | Generates code for tables |
123
+ | `view(types, view)` | Generates code for views |
124
+ | `materialisedView(types, view)` | Generates code for materialised views |
123
125
  | `enum(types, en)` | Generates code for enums |
124
126
  | `composite(types, composite)` | Generates code for composite types |
127
+ | `domain(types, domain)` | Generates code for domains |
128
+ | `range(types, range)` | Generates code for ranges |
125
129
  | `function(types, func)` | Generates code for functions |
126
130
  | `schemaKindIndex(schema, kind)` | Generates index for a schema kind (models/public/tables/index.ts) |
127
131
  | `schemaIndex(schema)` | Generates index for a schema (models/public/index.ts) |
@@ -131,4 +135,4 @@ The `SchemaGenerator` interface provides methods to customize code generation:
131
135
 
132
136
  [MIT](LICENSE)
133
137
 
134
- <sup>†</sup> We only support tables, enums, composite types, and functions at the moment, but we're working on adding support for views, materialised views, domains, and more.
138
+ [1]: We support codegen for tables, views, materialized views, enums, composite types, domains, ranges, and functions.
package/lib/bin.js CHANGED
@@ -32,7 +32,7 @@ if (help) {
32
32
  log(" -o, --out [path] Path to output directory");
33
33
  log(" -a, --adapter [adapter] Output adapter to use (default: 'kysely')");
34
34
  log(" -A, --all-adapters Output all adapters");
35
- log(" -c, --config [path] Path to config file (JSON)");
35
+ log(" -c, --config [path] Path to config file");
36
36
  log(" Defaults to '.truepgrc.json' or '.config/.truepgrc.json'");
37
37
  log("Example:");
38
38
  log(" true-pg -u postgres://user:pass@localhost:5432/my-database -o models -a kysely -a zod");
@@ -42,19 +42,15 @@ if (help) {
42
42
  else
43
43
  process.exit(1);
44
44
  }
45
- if (opts["all-adapters"]) {
45
+ if (opts["all-adapters"])
46
46
  opts.adapter = Object.keys(adapters);
47
- console.log("Enabling all built-in adapters:", opts.adapter);
48
- }
49
47
  if (!(opts.adapter || config.adapters))
50
48
  console.warn('No adapters specified, using default: ["kysely"]');
51
- opts.out ??= "models";
52
49
  // allow single adapter or comma-separated list of adapters
53
50
  if (typeof opts.adapter === "string")
54
51
  opts.adapter = opts.adapter.split(",");
55
- opts.adapter ??= ["kysely"];
56
52
  // CLI args take precedence over config file
57
53
  config.uri = opts.uri ?? config.uri;
58
54
  config.out = opts.out ?? config.out;
59
- config.adapters = opts.adapter ?? config.adapters;
55
+ config.adapters = opts.adapter ?? config.adapters ?? ["kysely"];
60
56
  await generate(config);
@@ -0,0 +1,15 @@
1
+ import { Client as Pg } from "pg";
2
+ import { PGlite as Pglite } from "@electric-sql/pglite";
3
+ export declare class DbAdapter {
4
+ private client;
5
+ constructor(client: Pg | Pglite);
6
+ connect(): Promise<void>;
7
+ /**
8
+ * Execute a read query and return just the rows
9
+ */
10
+ query<R, I extends any[] = []>(text: string, params?: I): Promise<R[]>;
11
+ /**
12
+ * Close the connection if needed
13
+ */
14
+ close(): Promise<void>;
15
+ }
@@ -0,0 +1,53 @@
1
+ import { Client as Pg } from "pg";
2
+ import { PGlite as Pglite } from "@electric-sql/pglite";
3
+ export class DbAdapter {
4
+ client;
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+ async connect() {
9
+ if (this.client instanceof Pg) {
10
+ return this.client.connect();
11
+ }
12
+ else if (this.client instanceof Pglite) {
13
+ // Pglite doesn't have an explicit connect method
14
+ }
15
+ }
16
+ /**
17
+ * Execute a read query and return just the rows
18
+ */
19
+ async query(text, params) {
20
+ let stack;
21
+ try {
22
+ stack = new Error().stack;
23
+ stack = stack?.split("\n").slice(3).join("\n");
24
+ // @ts-expect-error The two clients can process our query types similarly
25
+ const result = await this.client.query(text, params);
26
+ return result.rows;
27
+ }
28
+ catch (error) {
29
+ if (error instanceof Error) {
30
+ console.error("Query Error ===");
31
+ console.error("Query:", text);
32
+ console.error("Parameters:", params);
33
+ console.error("\nStack trace:");
34
+ console.error(stack);
35
+ console.error("\nError details:", error.message);
36
+ process.exit(1);
37
+ }
38
+ else
39
+ throw error;
40
+ }
41
+ }
42
+ /**
43
+ * Close the connection if needed
44
+ */
45
+ async close() {
46
+ if (this.client instanceof Pg) {
47
+ await this.client.end();
48
+ }
49
+ else if (this.client instanceof Pglite) {
50
+ await this.client.close();
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,64 @@
1
+ import { DbAdapter } from "./adapter.ts";
2
+ export declare namespace Canonical {
3
+ export enum Kind {
4
+ Base = "base",
5
+ Composite = "composite",
6
+ Domain = "domain",
7
+ Enum = "enum",
8
+ Range = "range",
9
+ Pseudo = "pseudo",
10
+ Unknown = "unknown"
11
+ }
12
+ interface Abstract {
13
+ original_type: string;
14
+ canonical_name: string;
15
+ schema: string;
16
+ name: string;
17
+ kind: Kind;
18
+ dimensions: number;
19
+ modifiers?: string | null;
20
+ }
21
+ export interface Base extends Abstract {
22
+ kind: Kind.Base;
23
+ }
24
+ export interface Enum extends Abstract {
25
+ kind: Kind.Enum;
26
+ enum_values: string[];
27
+ }
28
+ export interface CompositeAttribute {
29
+ name: string;
30
+ index: number;
31
+ type: Canonical;
32
+ comment: string | null;
33
+ defaultValue: any;
34
+ isNullable: boolean;
35
+ /**
36
+ * Whether the attribute is an identity attribute.
37
+ */
38
+ isIdentity: boolean;
39
+ /**
40
+ * Behavior of the generated attribute. "ALWAYS" if always generated,
41
+ * "NEVER" if never generated, "BY DEFAULT" if generated when a value
42
+ * is not provided.
43
+ */
44
+ generated: "ALWAYS" | "NEVER" | "BY DEFAULT";
45
+ }
46
+ export interface Composite extends Abstract {
47
+ kind: Kind.Composite;
48
+ attributes: CompositeAttribute[];
49
+ }
50
+ export interface Domain extends Abstract {
51
+ kind: Kind.Domain;
52
+ domain_base_type: Canonical;
53
+ }
54
+ export interface Range extends Abstract {
55
+ kind: Kind.Range;
56
+ range_subtype: Canonical;
57
+ }
58
+ export interface Pseudo extends Abstract {
59
+ kind: Kind.Pseudo;
60
+ }
61
+ export {};
62
+ }
63
+ export type Canonical = Canonical.Base | Canonical.Enum | Canonical.Composite | Canonical.Domain | Canonical.Range | Canonical.Pseudo;
64
+ export declare const canonicalise: (db: DbAdapter, types: string[]) => Promise<Canonical[]>;
@@ -0,0 +1,245 @@
1
+ import { DbAdapter } from "./adapter.js";
2
+ const removeNulls = (o) => {
3
+ for (const key in o)
4
+ if (o[key] == null)
5
+ delete o[key];
6
+ return o;
7
+ };
8
+ export var Canonical;
9
+ (function (Canonical) {
10
+ let Kind;
11
+ (function (Kind) {
12
+ Kind["Base"] = "base";
13
+ Kind["Composite"] = "composite";
14
+ Kind["Domain"] = "domain";
15
+ Kind["Enum"] = "enum";
16
+ Kind["Range"] = "range";
17
+ Kind["Pseudo"] = "pseudo";
18
+ Kind["Unknown"] = "unknown";
19
+ })(Kind = Canonical.Kind || (Canonical.Kind = {}));
20
+ })(Canonical || (Canonical = {}));
21
+ export const canonicalise = async (db, types) => {
22
+ if (types.length === 0)
23
+ return [];
24
+ const placeholders = types.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(", ");
25
+ const query = `
26
+ WITH RECURSIVE
27
+ -- Parameters with sequence numbers to preserve order
28
+ input(type_name, seq) AS (
29
+ VALUES ${placeholders}
30
+ ),
31
+ -- Parse array dimensions and base type
32
+ type_parts AS (
33
+ SELECT
34
+ type_name,
35
+ seq,
36
+ CASE
37
+ WHEN type_name ~ '\\(.*\\)' THEN regexp_replace(type_name, '\\(.*\\)', '')
38
+ ELSE type_name
39
+ END AS clean_type,
40
+ CASE
41
+ WHEN type_name ~ '\\(.*\\)' THEN substring(type_name from '\\((.*\\?)\\)')
42
+ ELSE NULL
43
+ END AS modifiers
44
+ FROM input
45
+ ),
46
+ array_dimensions AS (
47
+ SELECT
48
+ type_name,
49
+ seq,
50
+ modifiers,
51
+ CASE
52
+ WHEN clean_type ~ '.*\\[\\].*' THEN
53
+ (length(clean_type) - length(regexp_replace(clean_type, '\\[\\]', '', 'g'))) / 2
54
+ ELSE 0
55
+ END AS dimensions,
56
+ regexp_replace(clean_type, '\\[\\]', '', 'g') AS base_type_name
57
+ FROM type_parts
58
+ ),
59
+ -- Get base type information
60
+ base_type_info AS (
61
+ SELECT
62
+ a.type_name,
63
+ a.seq,
64
+ a.modifiers,
65
+ a.dimensions,
66
+ t.oid AS type_oid,
67
+ t.typname AS internal_name,
68
+ n.nspname AS schema_name,
69
+ t.typtype AS type_kind_code,
70
+ t.typbasetype,
71
+ CASE t.typtype
72
+ WHEN 'b' THEN 'base'
73
+ WHEN 'c' THEN 'composite'
74
+ WHEN 'd' THEN 'domain'
75
+ WHEN 'e' THEN 'enum'
76
+ WHEN 'p' THEN 'pseudo'
77
+ WHEN 'r' THEN 'range'
78
+ ELSE 'unknown'
79
+ END AS type_kind
80
+ FROM array_dimensions a
81
+ JOIN pg_type t ON t.oid = a.base_type_name::regtype
82
+ JOIN pg_namespace n ON t.typnamespace = n.oid
83
+ ),
84
+ -- Handle enum values for enum types
85
+ enum_values AS (
86
+ SELECT
87
+ b.type_name,
88
+ jsonb_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
89
+ FROM base_type_info b
90
+ JOIN pg_enum e ON b.type_oid = e.enumtypid
91
+ WHERE b.type_kind_code = 'e'
92
+ GROUP BY b.type_name
93
+ ),
94
+ -- Enhanced composite attributes with additional metadata
95
+ composite_attributes AS (
96
+ SELECT
97
+ b.type_name,
98
+ jsonb_agg(
99
+ jsonb_build_object(
100
+ 'name', a.attname,
101
+ 'index', a.attnum,
102
+ 'type_oid', a.atttypid,
103
+ 'type_name', format_type(a.atttypid, null),
104
+ 'comment', col_description(c.oid, a.attnum::int),
105
+ 'defaultValue', pg_get_expr(d.adbin, d.adrelid),
106
+ 'isNullable', NOT a.attnotnull,
107
+ 'isIdentity', a.attidentity IS NOT NULL AND a.attidentity != '',
108
+ 'generated', CASE
109
+ WHEN a.attidentity = 'a' THEN 'ALWAYS'
110
+ WHEN a.attidentity = 'd' THEN 'BY DEFAULT'
111
+ WHEN a.attgenerated = 's' THEN 'ALWAYS'
112
+ ELSE 'NEVER'
113
+ END
114
+ )
115
+ ORDER BY a.attnum
116
+ ) AS attributes
117
+ FROM base_type_info b
118
+ JOIN pg_type t ON t.oid = b.type_oid
119
+ JOIN pg_class c ON c.oid = t.typrelid
120
+ JOIN pg_attribute a ON a.attrelid = c.oid
121
+ LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
122
+ WHERE b.type_kind_code = 'c' AND a.attnum > 0 AND NOT a.attisdropped
123
+ GROUP BY b.type_name
124
+ ),
125
+ -- Recursive CTE to resolve domain base types
126
+ domain_types AS (
127
+ -- Base case: start with initial domain type
128
+ SELECT
129
+ b.type_name AS original_type,
130
+ b.type_oid AS domain_oid,
131
+ b.typbasetype AS base_type_oid,
132
+ 1 AS level
133
+ FROM base_type_info b
134
+ WHERE b.type_kind_code = 'd'
135
+
136
+ UNION ALL
137
+
138
+ -- Recursive case: follow chain of domains
139
+ SELECT
140
+ d.original_type,
141
+ t.oid AS domain_oid,
142
+ t.typbasetype AS base_type_oid,
143
+ d.level + 1 AS level
144
+ FROM domain_types d
145
+ JOIN pg_type t ON d.base_type_oid = t.oid
146
+ WHERE t.typtype = 'd'-- Only continue if the base is also a domain
147
+ ),
148
+ -- Get ultimate base type for domains
149
+ domain_base_types AS (
150
+ SELECT DISTINCT ON (original_type)
151
+ d.original_type,
152
+ format('%s.%s', n.nspname, t.typname) AS base_canonical_name
153
+ FROM (
154
+ -- Get the max level for each original type
155
+ SELECT original_type, MAX(level) AS max_level
156
+ FROM domain_types
157
+ GROUP BY original_type
158
+ ) m
159
+ JOIN domain_types d ON d.original_type = m.original_type AND d.level = m.max_level
160
+ JOIN pg_type t ON d.base_type_oid = t.oid
161
+ JOIN pg_namespace n ON t.typnamespace = n.oid
162
+ ),
163
+ -- Range type subtype information
164
+ range_subtypes AS (
165
+ SELECT
166
+ b.type_name,
167
+ format('%s.%s', n.nspname, t.typname) AS subtype_canonical_name
168
+ FROM base_type_info b
169
+ JOIN pg_range r ON b.type_oid = r.rngtypid
170
+ JOIN pg_type t ON t.oid = r.rngsubtype -- Join to get subtype details
171
+ JOIN pg_namespace n ON n.oid = t.typnamespace -- Join to get subtype schema
172
+ WHERE b.type_kind_code = 'r'
173
+ )
174
+ -- Final result as JSON
175
+ SELECT jsonb_build_object(
176
+ 'canonical_name', b.schema_name || '.' || b.internal_name,
177
+ 'schema', b.schema_name,
178
+ 'name', b.internal_name,
179
+ 'kind', b.type_kind,
180
+ 'dimensions', b.dimensions,
181
+ 'original_type', b.type_name,
182
+ 'modifiers', b.modifiers,
183
+ 'enum_values', e.values,
184
+ 'attributes', c.attributes,
185
+ 'domain_base_type', CASE
186
+ WHEN b.type_kind_code = 'd' THEN d.base_canonical_name
187
+ ELSE NULL
188
+ END,
189
+ 'range_subtype', CASE
190
+ WHEN b.type_kind_code = 'r' THEN r.subtype_canonical_name
191
+ ELSE NULL
192
+ END
193
+ ) AS type_info,
194
+ b.seq
195
+ FROM base_type_info b
196
+ LEFT JOIN enum_values e ON b.type_name = e.type_name
197
+ LEFT JOIN composite_attributes c ON b.type_name = c.type_name
198
+ LEFT JOIN domain_base_types d ON b.type_name = d.original_type
199
+ LEFT JOIN range_subtypes r ON b.type_name = r.type_name
200
+ ORDER BY b.seq::integer;
201
+ `;
202
+ const resolved = await db.query(query, types.flatMap((type, index) => [type, index]));
203
+ return Promise.all(resolved
204
+ .map(each => each.type_info)
205
+ .map(async (each) => {
206
+ if (each.kind === Canonical.Kind.Composite) {
207
+ const types = each.attributes.map(each => each.type_name);
208
+ const canonical = await canonicalise(db, types);
209
+ const attributes = await Promise.all(each.attributes.map(async (each, index) => {
210
+ return {
211
+ name: each.name,
212
+ index: each.index,
213
+ type: canonical[index],
214
+ comment: each.comment,
215
+ defaultValue: each.defaultValue,
216
+ isNullable: each.isNullable,
217
+ isIdentity: each.isIdentity,
218
+ generated: each.generated,
219
+ };
220
+ }));
221
+ return removeNulls({
222
+ ...each,
223
+ kind: Canonical.Kind.Composite,
224
+ attributes,
225
+ });
226
+ }
227
+ if (each.kind === Canonical.Kind.Domain) {
228
+ const canonical = await canonicalise(db, [each.domain_base_type]);
229
+ return removeNulls({
230
+ ...each,
231
+ kind: Canonical.Kind.Domain,
232
+ domain_base_type: canonical[0],
233
+ });
234
+ }
235
+ if (each.kind === Canonical.Kind.Range) {
236
+ const canonical = await canonicalise(db, [each.range_subtype]);
237
+ return removeNulls({
238
+ ...each,
239
+ kind: Canonical.Kind.Range,
240
+ range_subtype: canonical[0],
241
+ });
242
+ }
243
+ return removeNulls(each);
244
+ }));
245
+ };
@@ -0,0 +1,12 @@
1
+ import { DbAdapter } from "./adapter.ts";
2
+ /**
3
+ * In order to ignore the items (types, views, etc.) that belong to extensions,
4
+ * we use these queries to figure out what the OID's of those are. We can then
5
+ * ignore them in fetchClasses.
6
+ * @returns the oids of the Postgres extension classes and types
7
+ */
8
+ export default function fetchExtensionItemIds(db: DbAdapter): Promise<{
9
+ extClassOids: number[];
10
+ extTypeOids: number[];
11
+ extProcOids: number[];
12
+ }>;
@@ -0,0 +1,43 @@
1
+ import { DbAdapter } from "./adapter.js";
2
+ /**
3
+ * In order to ignore the items (types, views, etc.) that belong to extensions,
4
+ * we use these queries to figure out what the OID's of those are. We can then
5
+ * ignore them in fetchClasses.
6
+ * @returns the oids of the Postgres extension classes and types
7
+ */
8
+ export default async function fetchExtensionItemIds(db) {
9
+ // Query for class OIDs
10
+ const classQuery = `
11
+ SELECT c.oid
12
+ FROM pg_extension AS e
13
+ JOIN pg_depend AS d ON d.refobjid = e.oid
14
+ JOIN pg_class AS c ON c.oid = d.objid
15
+ JOIN pg_namespace AS ns ON ns.oid = e.extnamespace
16
+ WHERE d.deptype = 'e'
17
+ `;
18
+ const classResult = await db.query(classQuery);
19
+ const extClassOids = classResult.map(({ oid }) => oid);
20
+ // Query for type OIDs
21
+ const typeQuery = `
22
+ SELECT t.oid
23
+ FROM pg_extension AS e
24
+ JOIN pg_depend AS d ON d.refobjid = e.oid
25
+ JOIN pg_type AS t ON t.oid = d.objid
26
+ JOIN pg_namespace AS ns ON ns.oid = e.extnamespace
27
+ WHERE d.deptype = 'e'
28
+ `;
29
+ const typeResult = await db.query(typeQuery);
30
+ const extTypeOids = typeResult.map(({ oid }) => oid);
31
+ // Query for procedure OIDs
32
+ const procQuery = `
33
+ SELECT p.oid
34
+ FROM pg_extension AS e
35
+ JOIN pg_depend AS d ON d.refobjid = e.oid
36
+ JOIN pg_proc AS p ON p.oid = d.objid
37
+ JOIN pg_namespace AS ns ON ns.oid = e.extnamespace
38
+ WHERE d.deptype = 'e'
39
+ `;
40
+ const procResult = await db.query(procQuery);
41
+ const extProcOids = procResult.map(({ oid }) => oid);
42
+ return { extClassOids, extTypeOids, extProcOids };
43
+ }
@@ -0,0 +1,4 @@
1
+ import { DbAdapter } from "./adapter.ts";
2
+ import type { PgType } from "./pgtype.ts";
3
+ declare const fetchTypes: (db: DbAdapter, schemaNames: string[]) => Promise<PgType[]>;
4
+ export default fetchTypes;
@@ -0,0 +1,65 @@
1
+ import { DbAdapter } from "./adapter.js";
2
+ import fetchExtensionItemIds from "./fetchExtensionItemIds.js";
3
+ import { classKindMap, typeKindMap } from "./pgtype.js";
4
+ const fetchTypes = async (db, schemaNames) => {
5
+ // We want to ignore everything belonging to etensions. (Maybe this should be optional?)
6
+ const { extClassOids, extTypeOids } = await fetchExtensionItemIds(db);
7
+ const typeQuery = await db.query(`
8
+ SELECT
9
+ typname as "name",
10
+ nspname as "schemaName",
11
+ case typtype
12
+ when 'c' then case relkind
13
+ ${Object.entries(classKindMap)
14
+ .map(([key, classKind]) => `when '${key}' then '${classKind}'`)
15
+ .join("\n")}
16
+ else null
17
+ end
18
+ ${Object.entries(typeKindMap)
19
+ .map(([key, typeKind]) => `when '${key}' then '${typeKind}'`)
20
+ .join("\n")}
21
+ else null
22
+ end as "kind",
23
+ COALESCE(
24
+ obj_description(COALESCE(pg_class.oid, pg_type.oid)),
25
+ obj_description(pg_type.oid)
26
+ ) as "comment"
27
+ FROM pg_catalog.pg_type
28
+ JOIN pg_catalog.pg_namespace ON pg_namespace.oid = pg_type.typnamespace
29
+ FULL OUTER JOIN pg_catalog.pg_class ON pg_type.typrelid = pg_class.oid
30
+ WHERE (
31
+ pg_class.oid IS NULL
32
+ OR (
33
+ pg_class.relispartition = false
34
+ AND pg_class.relkind NOT IN ('S')
35
+ ${extClassOids.length > 0 ? `AND pg_class.oid NOT IN (${extClassOids.join(", ")})` : ""}
36
+ )
37
+ )
38
+ ${extTypeOids.length > 0 ? `AND pg_type.oid NOT IN (${extTypeOids.join(", ")})` : ""}
39
+ AND pg_type.typtype IN ('c', ${Object.keys(typeKindMap)
40
+ .map(key => `'${key}'`)
41
+ .join(", ")})
42
+ AND pg_namespace.nspname IN (${schemaNames.map(name => `'${name}'`).join(", ")})
43
+ `);
44
+ const procQuery = await db.query(`
45
+ SELECT
46
+ proname as "name",
47
+ nspname as "schemaName",
48
+ case prokind
49
+ when 'f' then 'function'
50
+ when 'p' then 'procedure'
51
+ when 'a' then 'aggregate'
52
+ when 'w' then 'window'
53
+ end as "kind",
54
+ obj_description(pg_proc.oid) as "comment"
55
+ FROM pg_catalog.pg_proc
56
+ JOIN pg_catalog.pg_namespace ON pg_namespace.oid = pg_proc.pronamespace
57
+ JOIN pg_catalog.pg_language ON pg_language.oid = pg_proc.prolang
58
+ WHERE ${extClassOids.length > 0 ? `pg_proc.oid NOT IN (${extClassOids.join(", ")}) AND` : ""}
59
+ pg_namespace.nspname IN (${schemaNames.map(name => `'${name}'`).join(", ")})
60
+ AND prokind IN ('f', 'p') -- TODO: Add support for aggregate and window functions
61
+ AND pg_language.lanname != 'internal'
62
+ `);
63
+ return [...typeQuery, ...procQuery];
64
+ };
65
+ export default fetchTypes;
@@ -0,0 +1,95 @@
1
+ import { Client as Pg, type ConnectionConfig } from "pg";
2
+ import { PGlite as Pglite } from "@electric-sql/pglite";
3
+ import { type TableDetails } from "./kinds/table.ts";
4
+ import { type ViewDetails } from "./kinds/view.ts";
5
+ import { type MaterializedViewDetails } from "./kinds/materialized-view.ts";
6
+ import { type EnumDetails } from "./kinds/enum.ts";
7
+ import { type CompositeTypeDetails } from "./kinds/composite.ts";
8
+ import { type FunctionDetails } from "./kinds/function.ts";
9
+ import { type DomainDetails } from "./kinds/domain.ts";
10
+ import { type RangeDetails } from "./kinds/range.ts";
11
+ import type { PgType } from "./pgtype.ts";
12
+ import { Canonical } from "./canonicalise.ts";
13
+ export { Canonical };
14
+ export type { TableDetails, ViewDetails, MaterializedViewDetails, EnumDetails, CompositeTypeDetails, FunctionDetails, DomainDetails, RangeDetails, };
15
+ export type { TableColumn } from "./kinds/table.ts";
16
+ export type { ViewColumn } from "./kinds/view.ts";
17
+ export type { MaterializedViewColumn } from "./kinds/materialized-view.ts";
18
+ export type { FunctionParameter, FunctionReturnType } from "./kinds/function.ts";
19
+ export { FunctionReturnTypeKind } from "./kinds/function.ts";
20
+ /**
21
+ * extractSchemas generates a record of all the schemas extracted, indexed by schema name.
22
+ * The schemas are instances of this type.
23
+ */
24
+ export type Schema = {
25
+ name: string;
26
+ tables: TableDetails[];
27
+ views: ViewDetails[];
28
+ materializedViews: MaterializedViewDetails[];
29
+ enums: EnumDetails[];
30
+ composites: CompositeTypeDetails[];
31
+ functions: FunctionDetails[];
32
+ domains: DomainDetails[];
33
+ ranges: RangeDetails[];
34
+ };
35
+ export type SchemaType = TableDetails | ViewDetails | MaterializedViewDetails | EnumDetails | CompositeTypeDetails | FunctionDetails | DomainDetails | RangeDetails;
36
+ /**
37
+ * This is the options object that can be passed to `extractSchemas`.
38
+ * @see extractSchemas
39
+ */
40
+ export interface ExtractSchemaOptions {
41
+ /**
42
+ * Will contain an array of schema names to extract.
43
+ * If undefined, all non-system schemas will be extracted.
44
+ */
45
+ schemas?: string[];
46
+ /**
47
+ * Filter function that you can use if you want to exclude
48
+ * certain items from the schemas.
49
+ */
50
+ typeFilter?: (pgType: PgType) => boolean;
51
+ /**
52
+ * extractShemas will always attempt to parse view definitions to
53
+ * discover the "source" of each column, i.e. the table or view that it
54
+ * is derived from.
55
+ * If this option is set to `true`, it will attempt to follow this
56
+ * source and copy values like indices, isNullable, etc.
57
+ * so that the view data is closer to what the database reflects.
58
+ */
59
+ resolveViews?: boolean;
60
+ /**
61
+ * Called with the number of types to extract.
62
+ */
63
+ onProgressStart?: (total: number) => void;
64
+ /**
65
+ * Called once for each type that is extracted.
66
+ */
67
+ onProgress?: () => void;
68
+ /**
69
+ * Called when all types have been extracted.
70
+ */
71
+ onProgressEnd?: () => void;
72
+ }
73
+ export declare class Extractor {
74
+ private db;
75
+ /**
76
+ * @param connectionConfig - Connection string or configuration object for Postgres connection
77
+ */
78
+ constructor(opts: {
79
+ pg?: Pg | Pglite;
80
+ uri?: string;
81
+ config?: ConnectionConfig;
82
+ });
83
+ canonicalise(types: string[]): Promise<Canonical[]>;
84
+ getBuiltinTypes(): Promise<{
85
+ name: string;
86
+ format: string;
87
+ kind: string;
88
+ }[]>;
89
+ /**
90
+ * Perform the extraction
91
+ * @param options - Optional options
92
+ * @returns A record of all the schemas extracted, indexed by schema name.
93
+ */
94
+ extractSchemas(options?: ExtractSchemaOptions): Promise<Record<string, Schema>>;
95
+ }