true-pg 0.2.2 → 0.3.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/lib/bin.js +4 -9
- package/lib/extractor/adapter.d.ts +15 -0
- package/lib/extractor/adapter.js +53 -0
- package/lib/extractor/canonicalise.d.ts +64 -0
- package/lib/extractor/canonicalise.js +245 -0
- package/lib/extractor/fetchExtensionItemIds.d.ts +12 -0
- package/lib/extractor/fetchExtensionItemIds.js +43 -0
- package/lib/extractor/fetchTypes.d.ts +4 -0
- package/lib/extractor/fetchTypes.js +65 -0
- package/lib/extractor/index.d.ts +95 -0
- package/lib/extractor/index.js +140 -0
- package/lib/extractor/kinds/composite.d.ts +15 -0
- package/lib/extractor/kinds/composite.js +13 -0
- package/lib/extractor/kinds/domain.d.ts +15 -0
- package/lib/extractor/kinds/domain.js +13 -0
- package/lib/extractor/kinds/enum.d.ts +9 -0
- package/lib/extractor/kinds/enum.js +20 -0
- package/lib/extractor/kinds/function.d.ts +73 -0
- package/lib/extractor/kinds/function.js +179 -0
- package/lib/extractor/kinds/materialized-view.d.ts +17 -0
- package/lib/extractor/kinds/materialized-view.js +64 -0
- package/lib/extractor/kinds/parts/commentMapQueryPart.d.ts +2 -0
- package/lib/extractor/kinds/parts/commentMapQueryPart.js +14 -0
- package/lib/extractor/kinds/parts/indexMapQueryPart.d.ts +2 -0
- package/lib/extractor/kinds/parts/indexMapQueryPart.js +27 -0
- package/lib/extractor/kinds/range.d.ts +15 -0
- package/lib/extractor/kinds/range.js +13 -0
- package/lib/extractor/kinds/table.d.ts +212 -0
- package/lib/extractor/kinds/table.js +217 -0
- package/lib/extractor/kinds/util/parseInlineTable.d.ts +9 -0
- package/lib/extractor/kinds/util/parseInlineTable.js +135 -0
- package/lib/extractor/kinds/util/parseInlineTable.test.d.ts +1 -0
- package/lib/extractor/kinds/util/parseInlineTable.test.js +26 -0
- package/lib/extractor/kinds/view.d.ts +17 -0
- package/lib/extractor/kinds/view.js +63 -0
- package/lib/extractor/pgtype.d.ts +41 -0
- package/lib/extractor/pgtype.js +30 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +38 -28
- package/lib/kysely/builtins.js +6 -4
- package/lib/kysely/index.js +103 -59
- package/lib/types.d.ts +38 -10
- package/lib/types.js +14 -3
- package/lib/util.d.ts +11 -0
- package/lib/util.js +6 -0
- package/lib/zod/builtins.js +11 -9
- package/lib/zod/index.js +107 -60
- package/package.json +3 -4
package/lib/bin.js
CHANGED
@@ -17,8 +17,7 @@ const opts = mri(args, {
|
|
17
17
|
import { cosmiconfig } from "cosmiconfig";
|
18
18
|
const explorer = cosmiconfig("truepg");
|
19
19
|
const result = opts.config ? await explorer.load(opts.config) : await explorer.search();
|
20
|
-
const config = result?.config;
|
21
|
-
console.log(config);
|
20
|
+
const config = result?.config ?? {};
|
22
21
|
const help = opts.help || (!config && !opts.uri);
|
23
22
|
if (help) {
|
24
23
|
// if help is triggered unintentionally, it's a user error
|
@@ -33,7 +32,7 @@ if (help) {
|
|
33
32
|
log(" -o, --out [path] Path to output directory");
|
34
33
|
log(" -a, --adapter [adapter] Output adapter to use (default: 'kysely')");
|
35
34
|
log(" -A, --all-adapters Output all adapters");
|
36
|
-
log(" -c, --config [path] Path to config file
|
35
|
+
log(" -c, --config [path] Path to config file");
|
37
36
|
log(" Defaults to '.truepgrc.json' or '.config/.truepgrc.json'");
|
38
37
|
log("Example:");
|
39
38
|
log(" true-pg -u postgres://user:pass@localhost:5432/my-database -o models -a kysely -a zod");
|
@@ -43,19 +42,15 @@ if (help) {
|
|
43
42
|
else
|
44
43
|
process.exit(1);
|
45
44
|
}
|
46
|
-
if (opts["all-adapters"])
|
45
|
+
if (opts["all-adapters"])
|
47
46
|
opts.adapter = Object.keys(adapters);
|
48
|
-
console.log("Enabling all built-in adapters:", opts.adapter);
|
49
|
-
}
|
50
47
|
if (!(opts.adapter || config.adapters))
|
51
48
|
console.warn('No adapters specified, using default: ["kysely"]');
|
52
|
-
opts.out ??= "models";
|
53
49
|
// allow single adapter or comma-separated list of adapters
|
54
50
|
if (typeof opts.adapter === "string")
|
55
51
|
opts.adapter = opts.adapter.split(",");
|
56
|
-
opts.adapter ??= ["kysely"];
|
57
52
|
// CLI args take precedence over config file
|
58
53
|
config.uri = opts.uri ?? config.uri;
|
59
54
|
config.out = opts.out ?? config.out;
|
60
|
-
config.adapters = opts.adapter ?? config.adapters;
|
55
|
+
config.adapters = opts.adapter ?? config.adapters ?? ["kysely"];
|
61
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,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
|
+
}
|