true-pg 0.0.2 → 0.0.3

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