true-pg 0.0.2 → 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/lib/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/lib/bin.js ADDED
@@ -0,0 +1,67 @@
1
+ import mri from "mri";
2
+ import { existsSync } from "fs";
3
+ import { generate, adapters } from "./index.js";
4
+ const args = process.argv.slice(2);
5
+ const opts = mri(args, {
6
+ boolean: ["help", "all-adapters"],
7
+ string: ["config", "uri", "out", "adapter"],
8
+ alias: {
9
+ h: "help",
10
+ c: "config",
11
+ u: "uri",
12
+ o: "out",
13
+ a: "adapter",
14
+ A: "all-adapters",
15
+ },
16
+ });
17
+ const help = opts.help || (!opts.config && !opts.uri);
18
+ if (help) {
19
+ // if help is triggered unintentionally, it's a user error
20
+ const type = opts.help ? "log" : "error";
21
+ const log = console[type];
22
+ log();
23
+ log("Usage: true-pg [options]");
24
+ log();
25
+ log("Options:");
26
+ log(" -h, --help Show help");
27
+ log(" -u, --uri [uri] Database URI (Postgres only!)");
28
+ log(" -o, --out [path] Path to output directory");
29
+ log(" -a, --adapter [adapter] Output adapter to use (default: 'kysely')");
30
+ log(" -A, --all-adapters Output all adapters");
31
+ log(" -c, --config [path] Path to config file (JSON)");
32
+ log(" Defaults to '.truepgrc.json' or '.config/.truepgrc.json'");
33
+ log("Example:");
34
+ log(" true-pg -u postgres://user:pass@localhost:5432/my-database -o models -a kysely -a zod");
35
+ log();
36
+ if (opts.help)
37
+ process.exit(0);
38
+ else
39
+ process.exit(1);
40
+ }
41
+ let configfile = opts.config;
42
+ if (!configfile) {
43
+ const candidates = [".truepgrc.json", ".config/.truepgrc.json"];
44
+ for (const candidate of candidates) {
45
+ if (await existsSync(candidate)) {
46
+ configfile = candidate;
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ const config = configfile ? await Bun.file(configfile).json() : {};
52
+ if (opts["all-adapters"]) {
53
+ opts.adapter = Object.keys(adapters);
54
+ console.log("Enabling all built-in adapters:", opts.adapter);
55
+ }
56
+ if (!(opts.adapter || config.adapters))
57
+ console.warn('No adapters specified, using default: ["kysely"]');
58
+ opts.out ??= "models";
59
+ // allow single adapter or comma-separated list of adapters
60
+ if (typeof opts.adapter === "string")
61
+ opts.adapter = opts.adapter.split(",");
62
+ opts.adapter ??= ["kysely"];
63
+ // CLI args take precedence over config file
64
+ config.uri = opts.uri ?? config.uri;
65
+ config.out = opts.out ?? config.out;
66
+ config.adapters = opts.adapter ?? config.adapters;
67
+ await generate(config);
@@ -0,0 +1,23 @@
1
+ export declare namespace True {
2
+ type Column<Selectable, Insertable = Selectable, Updateable = Selectable> = {
3
+ "@true-pg/insert": Insertable;
4
+ "@true-pg/update": Updateable;
5
+ "@true-pg/select": Selectable;
6
+ };
7
+ type Generated<T> = Column<T, T | undefined, T | undefined>;
8
+ type HasDefault<T> = Column<T, T | undefined, T | undefined>;
9
+ type AlwaysGenerated<T> = Column<T, never, never>;
10
+ type DrainOuterGeneric<T> = [T] extends [unknown] ? T : never;
11
+ type Simplify<T> = DrainOuterGeneric<{
12
+ [K in keyof T]: T[K];
13
+ } & {}>;
14
+ type Selectable<T extends Record<string, any>> = Simplify<{
15
+ [K in keyof T]: T[K] extends Record<string, any> ? Selectable<T[K]> : T[K] extends Column<infer S, infer I, infer U> ? S : T[K];
16
+ }>;
17
+ type Insertable<T extends Record<string, any>> = Simplify<{
18
+ [K in keyof T]: T[K] extends Record<string, any> ? Insertable<T[K]> : T[K] extends Column<infer S, infer I, infer U> ? I : T[K];
19
+ }>;
20
+ type Updateable<T extends Record<string, any>> = Simplify<{
21
+ [K in keyof T]?: T[K] extends Record<string, any> ? Updateable<T[K]> : T[K] extends Column<infer S, infer I, infer U> ? U : T[K];
22
+ }>;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type TruePGOpts, type createGenerator } from "./types.ts";
2
+ export declare const adapters: Record<string, createGenerator>;
3
+ export * from "./consumer.ts";
4
+ export declare function generate(opts: TruePGOpts, generators?: createGenerator[]): Promise<void>;
package/lib/index.js ADDED
@@ -0,0 +1,150 @@
1
+ import { Extractor } from "pg-extract";
2
+ import { rm, mkdir, writeFile } from "fs/promises";
3
+ import { Nodes, allowed_kind_names } from "./types.js";
4
+ import { existsSync } from "fs";
5
+ import { join } from "./util.js";
6
+ import { Kysely } from "./kysely/index.js";
7
+ import { Zod } from "./zod/index.js";
8
+ export const adapters = {
9
+ kysely: Kysely,
10
+ zod: Zod,
11
+ };
12
+ export * from "./consumer.js";
13
+ const filter_function = (func, warnings) => {
14
+ const typesToFilter = [
15
+ "pg_catalog.trigger",
16
+ "pg_catalog.event_trigger",
17
+ "pg_catalog.internal",
18
+ "pg_catalog.language_handler",
19
+ "pg_catalog.fdw_handler",
20
+ "pg_catalog.index_am_handler",
21
+ "pg_catalog.tsm_handler",
22
+ ];
23
+ const warn = (type) => warnings.push(`Skipping function ${func.name}: cannot represent ${type} (safe to ignore)`);
24
+ if (func.returnType.kind === "table") {
25
+ for (const col of func.returnType.columns) {
26
+ if (typesToFilter.includes(col.type.canonical_name)) {
27
+ warn(col.type.canonical_name);
28
+ return false;
29
+ }
30
+ }
31
+ }
32
+ else {
33
+ if (typesToFilter.includes(func.returnType.type.canonical_name)) {
34
+ warn(func.returnType.type.canonical_name);
35
+ return false;
36
+ }
37
+ }
38
+ for (const param of func.parameters) {
39
+ if (typesToFilter.includes(param.type.canonical_name)) {
40
+ warn(param.type.canonical_name);
41
+ return false;
42
+ }
43
+ }
44
+ return func;
45
+ };
46
+ const write = (filename, file) => writeFile(filename, file + "\n");
47
+ const multifile = async (generators, schemas, opts) => {
48
+ const { out } = opts;
49
+ const warnings = [];
50
+ const gens = generators.map(g => g({ ...opts, warnings }));
51
+ const def_gen = gens[0];
52
+ const files = {
53
+ name: out,
54
+ type: "root",
55
+ children: Object.fromEntries(Object.values(schemas).map(schema => [
56
+ schema.name,
57
+ {
58
+ name: schema.name,
59
+ type: "schema",
60
+ children: Object.fromEntries(allowed_kind_names.map(kind => [
61
+ kind,
62
+ {
63
+ kind: kind,
64
+ type: "kind",
65
+ children: Object.fromEntries(schema[kind].map(item => [
66
+ item.name,
67
+ {
68
+ name: def_gen.formatSchemaType(item),
69
+ type: "type",
70
+ },
71
+ ])),
72
+ },
73
+ ])),
74
+ },
75
+ ])),
76
+ };
77
+ const start = performance.now();
78
+ for (const schema of Object.values(schemas)) {
79
+ console.log("Selected schema '%s':\n", schema.name);
80
+ const schemaDir = `${out}/${schema.name}`;
81
+ // skip functions that cannot be represented in JavaScript
82
+ schema.functions = schema.functions.filter(f => filter_function(f, warnings));
83
+ for (const kind of allowed_kind_names) {
84
+ if (schema[kind].length < 1)
85
+ continue;
86
+ await mkdir(`${schemaDir}/${kind}`, { recursive: true });
87
+ console.log(" Creating %s:\n", kind);
88
+ for (const [i, item] of schema[kind].entries()) {
89
+ const index = "[" + (i + 1 + "]").padEnd(3, " ");
90
+ const filename = `${schemaDir}/${kind}/${def_gen.formatSchemaType(item)}.ts`;
91
+ const exists = await existsSync(filename);
92
+ if (exists) {
93
+ warnings.push(`Skipping ${item.kind} "${item.name}": formatted name clashes. Wanted to create ${filename}`);
94
+ continue;
95
+ }
96
+ const start = performance.now();
97
+ let file = "";
98
+ const imports = new Nodes.ImportList([]);
99
+ if (item.kind === "table")
100
+ file += join(gens.map(gen => gen.table(imports, item)));
101
+ if (item.kind === "composite")
102
+ file += join(gens.map(gen => gen.composite(imports, item)));
103
+ if (item.kind === "enum")
104
+ file += join(gens.map(gen => gen.enum(imports, item)));
105
+ if (item.kind === "function")
106
+ file += join(gens.map(gen => gen.function(imports, item)));
107
+ const parts = [];
108
+ parts.push(imports.stringify(filename, files));
109
+ parts.push(file);
110
+ file = join(parts);
111
+ await write(filename, file);
112
+ const end = performance.now();
113
+ console.log(" %s %s \x1b[32m(%sms)\x1B[0m", index, filename, (end - start).toFixed(2));
114
+ }
115
+ const kindIndex = join(gens.map(gen => gen.schemaKindIndex(schema, kind, def_gen)));
116
+ const kindIndexFilename = `${schemaDir}/${kind}/index.ts`;
117
+ await write(kindIndexFilename, kindIndex);
118
+ console.log(' ✅ Created "%s" %s index: %s\n', schema.name, kind, kindIndexFilename);
119
+ }
120
+ const index = join(gens.map(gen => gen.schemaIndex(schema, def_gen)));
121
+ const indexFilename = `${out}/${schema.name}/index.ts`;
122
+ await write(indexFilename, index);
123
+ console.log(" Created schema index: %s\n", indexFilename);
124
+ }
125
+ const fullIndex = join(gens.map(gen => gen.fullIndex(Object.values(schemas))));
126
+ const fullIndexFilename = `${out}/index.ts`;
127
+ await write(fullIndexFilename, fullIndex);
128
+ console.log("Created full index: %s", fullIndexFilename);
129
+ const end = performance.now();
130
+ console.log("Completed in \x1b[32m%sms\x1b[0m", (end - start).toFixed(2));
131
+ if (warnings.length > 0) {
132
+ console.log("\nWarnings generated:");
133
+ console.log(warnings.map(warning => "* " + warning).join("\n"));
134
+ }
135
+ };
136
+ export async function generate(opts, generators) {
137
+ const out = opts.out || "./models";
138
+ const extractor = new Extractor(opts.uri);
139
+ const schemas = await extractor.extractSchemas();
140
+ generators ??= opts.adapters.map(adapter => {
141
+ const selected = adapters[adapter];
142
+ if (!selected)
143
+ throw new Error(`Requested adapter ${adapter} not found`);
144
+ return selected;
145
+ });
146
+ console.log("Clearing directory and generating schemas at '%s'", out);
147
+ await rm(out, { recursive: true, force: true });
148
+ await mkdir(out, { recursive: true });
149
+ await multifile(generators, schemas, { ...opts, out });
150
+ }
@@ -0,0 +1 @@
1
+ export declare const builtins: Record<string, string>;
@@ -0,0 +1,35 @@
1
+ // extended from https://github.com/kristiandupont/kanel/blob/e9332f03ff5e38f5b844dd7a4563580c0d9d1444/packages/kanel/src/defaultTypeMap.ts
2
+ export const builtins = {
3
+ "pg_catalog.int2": "number",
4
+ "pg_catalog.int4": "number",
5
+ // JS numbers are always floating point, so there is only 53 bits of precision
6
+ // for the integer part. Thus, storing a 64-bit integer in a JS number will
7
+ // result in potential data loss. We therefore use strings for 64-bit integers
8
+ // the same way that the pg driver does.
9
+ "pg_catalog.int8": "string",
10
+ "pg_catalog.float4": "number",
11
+ "pg_catalog.float8": "number",
12
+ "pg_catalog.numeric": "string",
13
+ "pg_catalog.bool": "boolean",
14
+ "pg_catalog.json": "unknown",
15
+ "pg_catalog.jsonb": "unknown",
16
+ "pg_catalog.char": "string",
17
+ "pg_catalog.bpchar": "string",
18
+ "pg_catalog.varchar": "string",
19
+ "pg_catalog.text": "string",
20
+ "pg_catalog.uuid": "string",
21
+ "pg_catalog.inet": "string",
22
+ "pg_catalog.date": "Date",
23
+ "pg_catalog.time": "Date",
24
+ "pg_catalog.timetz": "Date",
25
+ "pg_catalog.timestamp": "Date",
26
+ "pg_catalog.timestamptz": "Date",
27
+ "pg_catalog.int4range": "string",
28
+ "pg_catalog.int8range": "string",
29
+ "pg_catalog.numrange": "string",
30
+ "pg_catalog.tsrange": "string",
31
+ "pg_catalog.tstzrange": "string",
32
+ "pg_catalog.daterange": "string",
33
+ "pg_catalog.record": "Record<string, unknown>",
34
+ "pg_catalog.void": "void",
35
+ };
@@ -0,0 +1,2 @@
1
+ import { createGenerator } from "../types.ts";
2
+ export declare const Kysely: createGenerator;
@@ -0,0 +1,273 @@
1
+ import { allowed_kind_names, createGenerator, Nodes } from "../types.js";
2
+ import { builtins } from "./builtins.js";
3
+ const isIdentifierInvalid = (str) => {
4
+ const invalid = str.match(/[^a-zA-Z0-9_]/);
5
+ return invalid !== null;
6
+ };
7
+ const toPascalCase = (str) => str
8
+ .replace(/^[^a-zA-Z]+/, "") // remove leading non-alphabetic characters
9
+ .replace(/[^a-zA-Z0-9_]+/g, "") // remove non-alphanumeric/underscore characters
10
+ .replace(" ", "_") // replace spaces with underscores
11
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) // capitalize letters after underscores
12
+ .replace(/^([a-z])/, (_, letter) => letter.toUpperCase()); // capitalize first letter
13
+ export const Kysely = createGenerator(opts => {
14
+ const defaultSchema = opts?.defaultSchema ?? "public";
15
+ const ky = (imports, name) => imports.add(new Nodes.ExternalImport({
16
+ name,
17
+ module: "kysely",
18
+ typeOnly: true,
19
+ star: false,
20
+ }));
21
+ const add = (imports, type) => {
22
+ if (type.schema === "pg_catalog")
23
+ return;
24
+ imports.add(new Nodes.InternalImport({
25
+ name: generator.formatType(type),
26
+ canonical_type: type,
27
+ typeOnly: true,
28
+ star: false,
29
+ }));
30
+ };
31
+ const column = (
32
+ /** "this" */
33
+ generator,
34
+ /** @out Append used types to this array */
35
+ imports,
36
+ /** Information about the column */
37
+ col) => {
38
+ let base = generator.formatType(col.type);
39
+ if (col.type.dimensions > 0)
40
+ base += "[]".repeat(col.type.dimensions);
41
+ if (col.isNullable)
42
+ base += " | null";
43
+ let qualified = base;
44
+ if (col.generated === "ALWAYS") {
45
+ qualified = `GeneratedAlways<${qualified}>`;
46
+ ky(imports, "GeneratedAlways");
47
+ }
48
+ else if (col.generated === "BY DEFAULT") {
49
+ qualified = `Generated<${qualified}>`;
50
+ ky(imports, "Generated");
51
+ }
52
+ else if (col.defaultValue) {
53
+ qualified = `Generated<${qualified}>`;
54
+ ky(imports, "Generated");
55
+ }
56
+ let out = col.comment ? `/** ${col.comment} */\n\t` : "";
57
+ out += col.name;
58
+ // TODO: update imports for non-primitive types
59
+ out += `: ${qualified}`;
60
+ add(imports, col.type);
61
+ return `\t${out};\n`;
62
+ };
63
+ const composite_attribute = (generator, imports, attr) => {
64
+ let out = attr.name;
65
+ if (attr.isNullable)
66
+ out += "?";
67
+ out += `: ${generator.formatType(attr.type)}`;
68
+ add(imports, attr.type);
69
+ if (attr.type.dimensions > 0)
70
+ out += "[]".repeat(attr.type.dimensions);
71
+ if (attr.isNullable)
72
+ out += " | null";
73
+ return out;
74
+ };
75
+ const generator = {
76
+ formatSchema(name) {
77
+ return toPascalCase(name) + "Schema";
78
+ },
79
+ formatSchemaType(type) {
80
+ return toPascalCase(type.name);
81
+ },
82
+ formatType(type) {
83
+ if (type.schema === "pg_catalog") {
84
+ const name = type.canonical_name;
85
+ const format = builtins[name];
86
+ if (format)
87
+ return format;
88
+ opts?.warnings?.push(`Unknown builtin type: ${name}! Pass customBuiltinMap to map this type. Defaulting to "unknown".`);
89
+ return "unknown";
90
+ }
91
+ return toPascalCase(type.name);
92
+ },
93
+ table(imports, table) {
94
+ let out = "";
95
+ if (table.comment)
96
+ out += `/** ${table.comment} */\n`;
97
+ out += `export interface ${this.formatSchemaType(table)} {\n`;
98
+ for (const col of table.columns)
99
+ out += column(this, imports, col);
100
+ out += "}";
101
+ return out;
102
+ },
103
+ enum(imports, en) {
104
+ let out = "";
105
+ if (en.comment)
106
+ out += `/** ${en.comment} */\n`;
107
+ out += `export type ${this.formatSchemaType(en)} = ${en.values.map(v => `"${v}"`).join(" | ")};`;
108
+ return out;
109
+ },
110
+ composite(imports, type) {
111
+ let out = "";
112
+ if (type.comment)
113
+ out += `/** ${type.comment} */\n`;
114
+ out += `export interface ${this.formatSchemaType(type)} {\n`;
115
+ const props = type.canonical.attributes.map(c => composite_attribute(this, imports, c)).map(t => `\t${t};`);
116
+ out += props.join("\n");
117
+ out += "\n}";
118
+ return out;
119
+ },
120
+ function(imports, type) {
121
+ let out = "";
122
+ out += "/**\n";
123
+ if (type.comment)
124
+ out += ` * ${type.comment}\n`;
125
+ out += ` * @volatility ${type.volatility}\n`;
126
+ out += ` * @parallelSafety ${type.parallelSafety}\n`;
127
+ out += ` * @isStrict ${type.isStrict}\n`;
128
+ out += " */\n";
129
+ out += `export interface ${this.formatSchemaType(type)} {\n\t`;
130
+ // Get the input parameters (those that appear in function signature)
131
+ const inputParams = type.parameters.filter(p => p.mode === "IN" || p.mode === "INOUT" || p.mode === "VARIADIC");
132
+ if (inputParams.length === 0) {
133
+ out += "(): ";
134
+ }
135
+ else if (inputParams.length === 1) {
136
+ out += "(";
137
+ out += inputParams[0].name;
138
+ out += ": ";
139
+ out += this.formatType(inputParams[0].type);
140
+ add(imports, inputParams[0].type);
141
+ if (inputParams[0].type.dimensions > 0)
142
+ out += "[]".repeat(inputParams[0].type.dimensions);
143
+ out += "): ";
144
+ }
145
+ else if (inputParams.length > 0) {
146
+ out += "(\n";
147
+ for (const param of inputParams) {
148
+ // Handle variadic parameters differently if needed
149
+ const isVariadic = param.mode === "VARIADIC";
150
+ const paramName = isVariadic ? `...${param.name}` : param.name;
151
+ out += `\t\t${paramName}`;
152
+ if (param.hasDefault && !isVariadic)
153
+ out += "?";
154
+ out += `: ${this.formatType(param.type)}`;
155
+ add(imports, param.type);
156
+ if (param.type.dimensions > 0)
157
+ out += "[]".repeat(param.type.dimensions);
158
+ if (!isVariadic)
159
+ out += ",";
160
+ out += "\n";
161
+ }
162
+ out += "\t): ";
163
+ }
164
+ if (type.returnType.kind === "table") {
165
+ out += "{\n";
166
+ for (const col of type.returnType.columns) {
167
+ out += `\t\t${col.name}: `;
168
+ out += this.formatType(col.type);
169
+ add(imports, col.type);
170
+ if (col.type.dimensions > 0)
171
+ out += "[]".repeat(col.type.dimensions);
172
+ out += `;\n`;
173
+ }
174
+ out += "\t}";
175
+ }
176
+ else {
177
+ out += this.formatType(type.returnType.type);
178
+ add(imports, type.returnType.type);
179
+ if (type.returnType.type.dimensions > 0)
180
+ out += "[]".repeat(type.returnType.type.dimensions);
181
+ }
182
+ // Add additional array brackets if it returns a set
183
+ if (type.returnType.isSet) {
184
+ out += "[]";
185
+ }
186
+ out += ";\n}";
187
+ return out;
188
+ },
189
+ schemaKindIndex(schema, kind, main_generator) {
190
+ const generator = main_generator ?? this;
191
+ const imports = schema[kind];
192
+ if (imports.length === 0)
193
+ return "";
194
+ return imports
195
+ .map(each => {
196
+ const name = this.formatSchemaType(each);
197
+ const file = generator.formatSchemaType(each);
198
+ return `export type { ${name} } from "./${file}.ts";`;
199
+ })
200
+ .join("\n");
201
+ },
202
+ schemaIndex(schema) {
203
+ let out = allowed_kind_names.map(kind => `import type * as ${kind} from "./${kind}/index.ts";`).join("\n");
204
+ out += "\n\n";
205
+ out += `export interface ${this.formatSchema(schema.name)} {\n`;
206
+ for (const kind of allowed_kind_names) {
207
+ const items = schema[kind];
208
+ if (items.length === 0)
209
+ continue;
210
+ out += `\t${kind}: {\n`;
211
+ const formatted = items
212
+ .map(each => {
213
+ const formatted = this.formatSchemaType(each);
214
+ return { ...each, formatted };
215
+ })
216
+ .filter(x => x !== undefined);
217
+ out += formatted
218
+ .map(t => {
219
+ let name = t.name;
220
+ if (isIdentifierInvalid(name))
221
+ name = `"${name}"`;
222
+ return `\t\t${name}: ${t.kind}s.${t.formatted};`;
223
+ })
224
+ .join("\n");
225
+ out += "\n\t};\n";
226
+ }
227
+ out += "}";
228
+ return out;
229
+ },
230
+ fullIndex(schemas) {
231
+ let out = "";
232
+ out += schemas
233
+ .map(s => `import type { ${this.formatSchema(s.name)} } from "./${s.name}/index.ts";`)
234
+ .join("\n");
235
+ out += "\n\n";
236
+ out += `export interface Database {\n`;
237
+ out += schemas
238
+ .map(schema => {
239
+ // Kysely only wants tables
240
+ const tables = schema.tables;
241
+ let out = "";
242
+ const seen = new Set();
243
+ const formatted = tables
244
+ .map(each => {
245
+ const formatted = this.formatSchemaType(each);
246
+ // skip clashing names
247
+ if (seen.has(formatted))
248
+ return;
249
+ seen.add(formatted);
250
+ return { ...each, formatted };
251
+ })
252
+ .filter(x => x !== undefined);
253
+ if (out.length)
254
+ out += "\n\n";
255
+ out += formatted
256
+ .map(t => {
257
+ const prefix = defaultSchema === schema.name ? "" : schema.name + ".";
258
+ let qualified = prefix + t.name;
259
+ if (isIdentifierInvalid(qualified))
260
+ qualified = `"${qualified}"`;
261
+ return `\t${qualified}: ${this.formatSchema(schema.name)}["${t.kind}s"]["${t.name}"];`;
262
+ })
263
+ .join("\n");
264
+ return out;
265
+ })
266
+ .join("");
267
+ out += "\n}\n\n";
268
+ out += schemas.map(s => `export type { ${this.formatSchema(s.name)} };`).join("\n");
269
+ return out;
270
+ },
271
+ };
272
+ return generator;
273
+ });