@tsqx/kit 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +9 -0
- package/dist/postgres/pg/index.cjs +425 -0
- package/dist/postgres/pg/index.d.cts +6 -0
- package/dist/postgres/pg/index.d.mts +6 -0
- package/dist/postgres/pg/index.mjs +424 -0
- package/package.json +39 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _tsqx_core = require("@tsqx/core");
|
|
3
|
+
//#region src/config.ts
|
|
4
|
+
function defineConfig(config) {
|
|
5
|
+
const result = (0, _tsqx_core.parseConfig)(config);
|
|
6
|
+
if (result.isErr()) throw result.error;
|
|
7
|
+
return result.value;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
exports.defineConfig = defineConfig;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _tsqx_core = require("@tsqx/core");
|
|
3
|
+
let neverthrow = require("neverthrow");
|
|
4
|
+
//#region src/postgres/pg/parser.ts
|
|
5
|
+
function stripComments(sql) {
|
|
6
|
+
sql = sql.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
7
|
+
sql = sql.replace(/--.*$/gm, "");
|
|
8
|
+
return sql;
|
|
9
|
+
}
|
|
10
|
+
function parseColumnDef(raw) {
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
if (!trimmed) return null;
|
|
13
|
+
const match = trimmed.match(/^("(\w+)"|(\w+))\s+(.+)$/is);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const name = match[2] ?? match[3].toLowerCase();
|
|
16
|
+
const rest = match[4].trim();
|
|
17
|
+
const typeMatch = rest.match(/^(\w+(?:\s+(?:PRECISION|VARYING|ZONE|WITHOUT|WITH(?:\s+TIME\s+ZONE)?))?(?:\(\s*\d+(?:\s*,\s*\d+)?\s*\))?(?:\[\])?)/i);
|
|
18
|
+
if (!typeMatch) return null;
|
|
19
|
+
const type = typeMatch[1].toUpperCase();
|
|
20
|
+
const modifiers = rest.slice(typeMatch[0].length).trim().toUpperCase();
|
|
21
|
+
const nullable = !modifiers.includes("NOT NULL");
|
|
22
|
+
const primaryKey = modifiers.includes("PRIMARY KEY");
|
|
23
|
+
const unique = modifiers.includes("UNIQUE");
|
|
24
|
+
let defaultValue;
|
|
25
|
+
const defaultMatch = rest.slice(typeMatch[0].length).match(/DEFAULT\s+(.+?)(?:\s+(?:NOT\s+NULL|NULL|PRIMARY\s+KEY|UNIQUE|REFERENCES|CHECK|CONSTRAINT)|\s*$)/i);
|
|
26
|
+
if (defaultMatch) defaultValue = defaultMatch[1].trim();
|
|
27
|
+
let references;
|
|
28
|
+
const restModifiers = rest.slice(typeMatch[0].length).trim();
|
|
29
|
+
const refMatch = restModifiers.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*"?(\w+)"?\s*\)/i);
|
|
30
|
+
if (refMatch) references = {
|
|
31
|
+
table: restModifiers.includes(`"${refMatch[1]}"`) ? refMatch[1] : refMatch[1].toLowerCase(),
|
|
32
|
+
column: restModifiers.includes(`"${refMatch[2]}"`) ? refMatch[2] : refMatch[2].toLowerCase()
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
name,
|
|
36
|
+
type,
|
|
37
|
+
nullable: primaryKey ? false : nullable,
|
|
38
|
+
...defaultValue !== void 0 && { default: defaultValue },
|
|
39
|
+
primaryKey,
|
|
40
|
+
unique,
|
|
41
|
+
...references && { references }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isTableConstraint(line) {
|
|
45
|
+
const upper = line.trim().toUpperCase();
|
|
46
|
+
return upper.startsWith("PRIMARY KEY") || upper.startsWith("UNIQUE") || upper.startsWith("FOREIGN KEY") || upper.startsWith("CONSTRAINT") || upper.startsWith("CHECK");
|
|
47
|
+
}
|
|
48
|
+
function parseTableConstraint(raw) {
|
|
49
|
+
let working = raw.trim().toUpperCase();
|
|
50
|
+
let name;
|
|
51
|
+
const constraintMatch = raw.trim().match(/^CONSTRAINT\s+"?(\w+)"?\s+(.+)$/is);
|
|
52
|
+
if (constraintMatch) {
|
|
53
|
+
name = constraintMatch[1].toLowerCase();
|
|
54
|
+
working = constraintMatch[2].trim().toUpperCase();
|
|
55
|
+
}
|
|
56
|
+
const colsMatch = working.match(/\(\s*(.+?)\s*\)/);
|
|
57
|
+
if (!colsMatch) return null;
|
|
58
|
+
const columns = colsMatch[1].split(",").map((c) => {
|
|
59
|
+
const trimmed = c.trim();
|
|
60
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return trimmed.slice(1, -1);
|
|
61
|
+
return trimmed.replace(/"/g, "").toLowerCase();
|
|
62
|
+
});
|
|
63
|
+
if (working.startsWith("PRIMARY KEY")) return {
|
|
64
|
+
type: "primary_key",
|
|
65
|
+
...name && { name },
|
|
66
|
+
columns
|
|
67
|
+
};
|
|
68
|
+
if (working.startsWith("UNIQUE")) return {
|
|
69
|
+
type: "unique",
|
|
70
|
+
...name && { name },
|
|
71
|
+
columns
|
|
72
|
+
};
|
|
73
|
+
if (working.startsWith("FOREIGN KEY")) {
|
|
74
|
+
const refMatch = working.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*(.+?)\s*\)/i);
|
|
75
|
+
if (!refMatch) return null;
|
|
76
|
+
const refColumns = refMatch[2].split(",").map((c) => {
|
|
77
|
+
const trimmed = c.trim();
|
|
78
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return trimmed.slice(1, -1);
|
|
79
|
+
return trimmed.replace(/"/g, "").toLowerCase();
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
type: "foreign_key",
|
|
83
|
+
...name && { name },
|
|
84
|
+
columns,
|
|
85
|
+
references: {
|
|
86
|
+
table: refMatch[1].toLowerCase(),
|
|
87
|
+
columns: refColumns
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function parseCreateTable(sql) {
|
|
94
|
+
const tableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("(\w+)"|(\w+))\s*\(([\s\S]+)\)/i);
|
|
95
|
+
if (!tableMatch) return (0, neverthrow.err)(new _tsqx_core.SchemaError(`Failed to parse CREATE TABLE statement`));
|
|
96
|
+
const tableName = tableMatch[2] ?? tableMatch[3].toLowerCase();
|
|
97
|
+
const body = tableMatch[4];
|
|
98
|
+
const lines = [];
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let current = "";
|
|
101
|
+
for (const char of body) {
|
|
102
|
+
if (char === "(") depth++;
|
|
103
|
+
else if (char === ")") depth--;
|
|
104
|
+
if (char === "," && depth === 0) {
|
|
105
|
+
lines.push(current.trim());
|
|
106
|
+
current = "";
|
|
107
|
+
} else current += char;
|
|
108
|
+
}
|
|
109
|
+
if (current.trim()) lines.push(current.trim());
|
|
110
|
+
const columns = [];
|
|
111
|
+
const constraints = [];
|
|
112
|
+
for (const line of lines) if (isTableConstraint(line)) {
|
|
113
|
+
const constraint = parseTableConstraint(line);
|
|
114
|
+
if (constraint) constraints.push(constraint);
|
|
115
|
+
} else {
|
|
116
|
+
const column = parseColumnDef(line);
|
|
117
|
+
if (column) columns.push(column);
|
|
118
|
+
}
|
|
119
|
+
const pkConstraint = constraints.find((c) => c.type === "primary_key");
|
|
120
|
+
if (pkConstraint) {
|
|
121
|
+
for (const col of columns) if (pkConstraint.columns.includes(col.name)) {
|
|
122
|
+
col.primaryKey = true;
|
|
123
|
+
col.nullable = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return (0, neverthrow.ok)({
|
|
127
|
+
name: tableName,
|
|
128
|
+
columns,
|
|
129
|
+
constraints
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function parseSchemaFiles(files) {
|
|
133
|
+
const snapshot = {};
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const matches = stripComments(file.content).match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?\w+"?\s*\([\s\S]*?\);/gi);
|
|
136
|
+
if (!matches) continue;
|
|
137
|
+
for (const statement of matches) {
|
|
138
|
+
const result = parseCreateTable(statement);
|
|
139
|
+
if (result.isErr()) return (0, neverthrow.err)(new _tsqx_core.SchemaError(`Error in ${file.filename}: ${result.error.message}`));
|
|
140
|
+
const table = result.value;
|
|
141
|
+
if (snapshot[table.name]) return (0, neverthrow.err)(new _tsqx_core.SchemaError(`Duplicate table "${table.name}" found in ${file.filename}`));
|
|
142
|
+
snapshot[table.name] = table;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return (0, neverthrow.ok)(snapshot);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/postgres/pg/generator.ts
|
|
149
|
+
function quoteIdent(name) {
|
|
150
|
+
if (name !== name.toLowerCase()) return `"${name}"`;
|
|
151
|
+
return name;
|
|
152
|
+
}
|
|
153
|
+
function columnToSQL(col) {
|
|
154
|
+
const parts = [quoteIdent(col.name), col.type];
|
|
155
|
+
if (col.primaryKey) parts.push("PRIMARY KEY");
|
|
156
|
+
if (!col.nullable && !col.primaryKey) parts.push("NOT NULL");
|
|
157
|
+
if (col.unique) parts.push("UNIQUE");
|
|
158
|
+
if (col.default !== void 0) parts.push(`DEFAULT ${col.default}`);
|
|
159
|
+
if (col.references) parts.push(`REFERENCES ${quoteIdent(col.references.table)}(${quoteIdent(col.references.column)})`);
|
|
160
|
+
return parts.join(" ");
|
|
161
|
+
}
|
|
162
|
+
function createTableSQL(table) {
|
|
163
|
+
const cols = table.columns.map((c) => ` ${columnToSQL(c)}`);
|
|
164
|
+
for (const constraint of table.constraints) {
|
|
165
|
+
const name = constraint.name ? `CONSTRAINT ${constraint.name} ` : "";
|
|
166
|
+
if (constraint.type === "primary_key") {
|
|
167
|
+
if (constraint.columns.length > 1) cols.push(` ${name}PRIMARY KEY (${constraint.columns.map(quoteIdent).join(", ")})`);
|
|
168
|
+
} else if (constraint.type === "unique") {
|
|
169
|
+
if (constraint.columns.length > 1) cols.push(` ${name}UNIQUE (${constraint.columns.map(quoteIdent).join(", ")})`);
|
|
170
|
+
} else if (constraint.type === "foreign_key" && constraint.references) cols.push(` ${name}FOREIGN KEY (${constraint.columns.map(quoteIdent).join(", ")}) REFERENCES ${quoteIdent(constraint.references.table)}(${constraint.references.columns.map(quoteIdent).join(", ")})`);
|
|
171
|
+
}
|
|
172
|
+
return `CREATE TABLE ${quoteIdent(table.name)} (\n${cols.join(",\n")}\n);`;
|
|
173
|
+
}
|
|
174
|
+
function operationToSQL(op) {
|
|
175
|
+
switch (op.type) {
|
|
176
|
+
case "create_table": return createTableSQL(op.table);
|
|
177
|
+
case "drop_table": return `DROP TABLE IF EXISTS ${quoteIdent(op.tableName)};`;
|
|
178
|
+
case "add_column": return `ALTER TABLE ${quoteIdent(op.tableName)} ADD COLUMN ${columnToSQL(op.column)};`;
|
|
179
|
+
case "drop_column": return `ALTER TABLE ${quoteIdent(op.tableName)} DROP COLUMN ${quoteIdent(op.columnName)};`;
|
|
180
|
+
case "alter_column": {
|
|
181
|
+
const stmts = [];
|
|
182
|
+
const { tableName, columnName, from, to } = op;
|
|
183
|
+
const qt = quoteIdent(tableName);
|
|
184
|
+
const qc = quoteIdent(columnName);
|
|
185
|
+
if (from.type !== to.type) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET DATA TYPE ${to.type};`);
|
|
186
|
+
if (from.nullable && !to.nullable) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET NOT NULL;`);
|
|
187
|
+
else if (!from.nullable && to.nullable) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} DROP NOT NULL;`);
|
|
188
|
+
if (from.default !== to.default) if (to.default !== void 0) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET DEFAULT ${to.default};`);
|
|
189
|
+
else stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} DROP DEFAULT;`);
|
|
190
|
+
if (!from.unique && to.unique) stmts.push(`ALTER TABLE ${qt} ADD CONSTRAINT ${tableName}_${columnName}_unique UNIQUE (${qc});`);
|
|
191
|
+
else if (from.unique && !to.unique) stmts.push(`ALTER TABLE ${qt} DROP CONSTRAINT ${tableName}_${columnName}_unique;`);
|
|
192
|
+
return stmts.join("\n");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function generateSQL(operations) {
|
|
197
|
+
if (operations.length === 0) return "";
|
|
198
|
+
return `BEGIN;\n\n${operations.map(operationToSQL).filter(Boolean).join("\n\n")}\n\nCOMMIT;\n`;
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/postgres/pg/types.ts
|
|
202
|
+
function sqlTypeToJsonSchema(sqlType) {
|
|
203
|
+
switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
|
|
204
|
+
case "SERIAL":
|
|
205
|
+
case "BIGSERIAL":
|
|
206
|
+
case "INTEGER":
|
|
207
|
+
case "INT":
|
|
208
|
+
case "INT4":
|
|
209
|
+
case "SMALLINT":
|
|
210
|
+
case "INT2":
|
|
211
|
+
case "BIGINT":
|
|
212
|
+
case "INT8": return { type: "integer" };
|
|
213
|
+
case "REAL":
|
|
214
|
+
case "FLOAT4":
|
|
215
|
+
case "DOUBLE PRECISION":
|
|
216
|
+
case "FLOAT8":
|
|
217
|
+
case "NUMERIC":
|
|
218
|
+
case "DECIMAL": return { type: "number" };
|
|
219
|
+
case "BOOLEAN":
|
|
220
|
+
case "BOOL": return { type: "boolean" };
|
|
221
|
+
case "TEXT": return { type: "string" };
|
|
222
|
+
case "VARCHAR":
|
|
223
|
+
case "CHARACTER VARYING": {
|
|
224
|
+
const lenMatch = sqlType.match(/\((\d+)\)/);
|
|
225
|
+
return lenMatch ? {
|
|
226
|
+
type: "string",
|
|
227
|
+
maxLength: parseInt(lenMatch[1], 10)
|
|
228
|
+
} : { type: "string" };
|
|
229
|
+
}
|
|
230
|
+
case "CHAR":
|
|
231
|
+
case "CHARACTER": {
|
|
232
|
+
const lenMatch = sqlType.match(/\((\d+)\)/);
|
|
233
|
+
return lenMatch ? {
|
|
234
|
+
type: "string",
|
|
235
|
+
maxLength: parseInt(lenMatch[1], 10)
|
|
236
|
+
} : { type: "string" };
|
|
237
|
+
}
|
|
238
|
+
case "UUID": return {
|
|
239
|
+
type: "string",
|
|
240
|
+
format: "uuid"
|
|
241
|
+
};
|
|
242
|
+
case "DATE": return {
|
|
243
|
+
type: "string",
|
|
244
|
+
format: "date"
|
|
245
|
+
};
|
|
246
|
+
case "TIMESTAMP":
|
|
247
|
+
case "TIMESTAMP WITHOUT TIME ZONE":
|
|
248
|
+
case "TIMESTAMP WITH TIME ZONE":
|
|
249
|
+
case "TIMESTAMPTZ": return {
|
|
250
|
+
type: "string",
|
|
251
|
+
format: "date-time"
|
|
252
|
+
};
|
|
253
|
+
case "TIME":
|
|
254
|
+
case "TIME WITHOUT TIME ZONE":
|
|
255
|
+
case "TIME WITH TIME ZONE":
|
|
256
|
+
case "TIMETZ": return {
|
|
257
|
+
type: "string",
|
|
258
|
+
format: "time"
|
|
259
|
+
};
|
|
260
|
+
case "JSON":
|
|
261
|
+
case "JSONB": return { type: "object" };
|
|
262
|
+
case "BYTEA": return {
|
|
263
|
+
type: "string",
|
|
264
|
+
format: "byte"
|
|
265
|
+
};
|
|
266
|
+
default: return { type: "string" };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function sqlTypeToTsType(sqlType) {
|
|
270
|
+
switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
|
|
271
|
+
case "SERIAL":
|
|
272
|
+
case "BIGSERIAL":
|
|
273
|
+
case "INTEGER":
|
|
274
|
+
case "INT":
|
|
275
|
+
case "INT4":
|
|
276
|
+
case "SMALLINT":
|
|
277
|
+
case "INT2":
|
|
278
|
+
case "BIGINT":
|
|
279
|
+
case "INT8":
|
|
280
|
+
case "REAL":
|
|
281
|
+
case "FLOAT4":
|
|
282
|
+
case "DOUBLE PRECISION":
|
|
283
|
+
case "FLOAT8":
|
|
284
|
+
case "NUMERIC":
|
|
285
|
+
case "DECIMAL": return "number";
|
|
286
|
+
case "BOOLEAN":
|
|
287
|
+
case "BOOL": return "boolean";
|
|
288
|
+
case "JSON":
|
|
289
|
+
case "JSONB": return "unknown";
|
|
290
|
+
case "BYTEA": return "Buffer";
|
|
291
|
+
default: return "string";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/postgres/pg/query-codegen.ts
|
|
296
|
+
function camelCase(str) {
|
|
297
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
298
|
+
}
|
|
299
|
+
function generateParamInterface(query) {
|
|
300
|
+
if (query.params.length === 0) return null;
|
|
301
|
+
return `export interface ${`${(0, _tsqx_core.pascalCase)(query.name)}Params`} {\n${query.params.map((p) => {
|
|
302
|
+
const base = sqlTypeToTsType(p.sqlType);
|
|
303
|
+
const type = p.nullable ? `${base} | null` : base;
|
|
304
|
+
return ` ${p.name}: ${type};`;
|
|
305
|
+
}).join("\n")}\n}`;
|
|
306
|
+
}
|
|
307
|
+
function generateParamSchema(query) {
|
|
308
|
+
if (query.params.length === 0) return null;
|
|
309
|
+
const name = `${(0, _tsqx_core.pascalCase)(query.name)}ParamsSchema`;
|
|
310
|
+
const properties = {};
|
|
311
|
+
const required = [];
|
|
312
|
+
for (const param of query.params) {
|
|
313
|
+
const jsonType = sqlTypeToJsonSchema(param.sqlType);
|
|
314
|
+
if (param.nullable) properties[param.name] = {
|
|
315
|
+
...jsonType,
|
|
316
|
+
type: Array.isArray(jsonType.type) ? [...jsonType.type, "null"] : [jsonType.type, "null"]
|
|
317
|
+
};
|
|
318
|
+
else {
|
|
319
|
+
properties[param.name] = jsonType;
|
|
320
|
+
required.push(param.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const schema = {
|
|
324
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
325
|
+
title: `${(0, _tsqx_core.pascalCase)(query.name)}Params`,
|
|
326
|
+
type: "object",
|
|
327
|
+
properties,
|
|
328
|
+
required,
|
|
329
|
+
additionalProperties: false
|
|
330
|
+
};
|
|
331
|
+
return `export const ${name} = ${JSON.stringify(schema, null, 2)} as const;`;
|
|
332
|
+
}
|
|
333
|
+
function generateResultType(query, snapshot) {
|
|
334
|
+
if (query.command === "exec" || query.command === "execrows" || query.command === "execresult") return null;
|
|
335
|
+
if (!query.returnsTable || !snapshot[query.returnsTable]) return null;
|
|
336
|
+
const table = snapshot[query.returnsTable];
|
|
337
|
+
const typeName = (0, _tsqx_core.pascalCase)(query.returnsTable);
|
|
338
|
+
if (query.returnsColumns.length === table.columns.length && query.returnsColumns.every((c) => table.columns.some((tc) => tc.name === c))) return typeName;
|
|
339
|
+
return `Pick<${typeName}, ${query.returnsColumns.map((c) => `"${c}"`).join(" | ")}>`;
|
|
340
|
+
}
|
|
341
|
+
function generateReturnType(query, resultType) {
|
|
342
|
+
switch (query.command) {
|
|
343
|
+
case "one": return `Promise<${resultType ?? "unknown"} | null>`;
|
|
344
|
+
case "many": return `Promise<${resultType ?? "unknown"}[]>`;
|
|
345
|
+
case "exec": return "Promise<void>";
|
|
346
|
+
case "execrows": return "Promise<number>";
|
|
347
|
+
case "execresult": return "Promise<QueryResult>";
|
|
348
|
+
default: return "Promise<void>";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function generateFunctionBody(query) {
|
|
352
|
+
const paramArgs = query.params.map((p) => `params.${p.name}`).join(", ");
|
|
353
|
+
const queryArgs = query.params.length > 0 ? `sql, [${paramArgs}]` : "sql";
|
|
354
|
+
switch (query.command) {
|
|
355
|
+
case "one": return ` const result = await client.query(${queryArgs});\n return result.rows[0] ?? null;`;
|
|
356
|
+
case "many": return ` const result = await client.query(${queryArgs});\n return result.rows;`;
|
|
357
|
+
case "exec": return ` await client.query(${queryArgs});`;
|
|
358
|
+
case "execrows": return ` const result = await client.query(${queryArgs});\n return result.rowCount ?? 0;`;
|
|
359
|
+
case "execresult": return ` return await client.query(${queryArgs});`;
|
|
360
|
+
default: return ` await client.query(${queryArgs});`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function generateFunction(query, snapshot) {
|
|
364
|
+
const fnName = camelCase(query.name);
|
|
365
|
+
const returnType = generateReturnType(query, generateResultType(query, snapshot));
|
|
366
|
+
const hasParams = query.params.length > 0;
|
|
367
|
+
const paramsType = hasParams ? `${(0, _tsqx_core.pascalCase)(query.name)}Params` : null;
|
|
368
|
+
const args = hasParams ? `client: Client | Pool, params: ${paramsType}` : "client: Client | Pool";
|
|
369
|
+
const sql = query.expandedSql.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
370
|
+
const lines = [];
|
|
371
|
+
lines.push(`export async function ${fnName}(${args}): ${returnType} {`);
|
|
372
|
+
lines.push(` const sql = '${sql}';`);
|
|
373
|
+
lines.push(generateFunctionBody(query));
|
|
374
|
+
lines.push("}");
|
|
375
|
+
return lines.join("\n");
|
|
376
|
+
}
|
|
377
|
+
function generateQueryFiles(queries, snapshot) {
|
|
378
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const query of queries) {
|
|
380
|
+
const key = query.sourceFile.replace(/\.sql$/, "");
|
|
381
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
382
|
+
grouped.get(key).push(query);
|
|
383
|
+
}
|
|
384
|
+
const files = {};
|
|
385
|
+
for (const [basename, fileQueries] of grouped) {
|
|
386
|
+
const lines = ["// This file is auto-generated by tsqx. Do not edit manually.", "import type { Client, Pool, QueryResult } from \"pg\";"];
|
|
387
|
+
const tableImports = /* @__PURE__ */ new Set();
|
|
388
|
+
for (const query of fileQueries) if (query.returnsTable && snapshot[query.returnsTable]) tableImports.add(query.returnsTable);
|
|
389
|
+
if (tableImports.size > 0) {
|
|
390
|
+
const imports = [...tableImports].map((t) => `type ${(0, _tsqx_core.pascalCase)(t)}`).join(", ");
|
|
391
|
+
lines.push(`import { ${imports} } from "../../generated";`);
|
|
392
|
+
}
|
|
393
|
+
lines.push("");
|
|
394
|
+
for (const query of fileQueries) {
|
|
395
|
+
const paramInterface = generateParamInterface(query);
|
|
396
|
+
if (paramInterface) {
|
|
397
|
+
lines.push(paramInterface);
|
|
398
|
+
lines.push("");
|
|
399
|
+
}
|
|
400
|
+
const paramSchema = generateParamSchema(query);
|
|
401
|
+
if (paramSchema) {
|
|
402
|
+
lines.push(paramSchema);
|
|
403
|
+
lines.push("");
|
|
404
|
+
}
|
|
405
|
+
lines.push(generateFunction(query, snapshot));
|
|
406
|
+
lines.push("");
|
|
407
|
+
}
|
|
408
|
+
files[`${basename}.ts`] = lines.join("\n");
|
|
409
|
+
}
|
|
410
|
+
return files;
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/postgres/pg/index.ts
|
|
414
|
+
function pgDialect() {
|
|
415
|
+
return {
|
|
416
|
+
name: "pg",
|
|
417
|
+
parseSchema: parseSchemaFiles,
|
|
418
|
+
generateSQL,
|
|
419
|
+
sqlTypeToJsonSchema,
|
|
420
|
+
sqlTypeToTsType,
|
|
421
|
+
generateQueryCode: generateQueryFiles
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
exports.pgDialect = pgDialect;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { SchemaError, pascalCase } from "@tsqx/core";
|
|
2
|
+
import { err, ok } from "neverthrow";
|
|
3
|
+
//#region src/postgres/pg/parser.ts
|
|
4
|
+
function stripComments(sql) {
|
|
5
|
+
sql = sql.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6
|
+
sql = sql.replace(/--.*$/gm, "");
|
|
7
|
+
return sql;
|
|
8
|
+
}
|
|
9
|
+
function parseColumnDef(raw) {
|
|
10
|
+
const trimmed = raw.trim();
|
|
11
|
+
if (!trimmed) return null;
|
|
12
|
+
const match = trimmed.match(/^("(\w+)"|(\w+))\s+(.+)$/is);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const name = match[2] ?? match[3].toLowerCase();
|
|
15
|
+
const rest = match[4].trim();
|
|
16
|
+
const typeMatch = rest.match(/^(\w+(?:\s+(?:PRECISION|VARYING|ZONE|WITHOUT|WITH(?:\s+TIME\s+ZONE)?))?(?:\(\s*\d+(?:\s*,\s*\d+)?\s*\))?(?:\[\])?)/i);
|
|
17
|
+
if (!typeMatch) return null;
|
|
18
|
+
const type = typeMatch[1].toUpperCase();
|
|
19
|
+
const modifiers = rest.slice(typeMatch[0].length).trim().toUpperCase();
|
|
20
|
+
const nullable = !modifiers.includes("NOT NULL");
|
|
21
|
+
const primaryKey = modifiers.includes("PRIMARY KEY");
|
|
22
|
+
const unique = modifiers.includes("UNIQUE");
|
|
23
|
+
let defaultValue;
|
|
24
|
+
const defaultMatch = rest.slice(typeMatch[0].length).match(/DEFAULT\s+(.+?)(?:\s+(?:NOT\s+NULL|NULL|PRIMARY\s+KEY|UNIQUE|REFERENCES|CHECK|CONSTRAINT)|\s*$)/i);
|
|
25
|
+
if (defaultMatch) defaultValue = defaultMatch[1].trim();
|
|
26
|
+
let references;
|
|
27
|
+
const restModifiers = rest.slice(typeMatch[0].length).trim();
|
|
28
|
+
const refMatch = restModifiers.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*"?(\w+)"?\s*\)/i);
|
|
29
|
+
if (refMatch) references = {
|
|
30
|
+
table: restModifiers.includes(`"${refMatch[1]}"`) ? refMatch[1] : refMatch[1].toLowerCase(),
|
|
31
|
+
column: restModifiers.includes(`"${refMatch[2]}"`) ? refMatch[2] : refMatch[2].toLowerCase()
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
type,
|
|
36
|
+
nullable: primaryKey ? false : nullable,
|
|
37
|
+
...defaultValue !== void 0 && { default: defaultValue },
|
|
38
|
+
primaryKey,
|
|
39
|
+
unique,
|
|
40
|
+
...references && { references }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function isTableConstraint(line) {
|
|
44
|
+
const upper = line.trim().toUpperCase();
|
|
45
|
+
return upper.startsWith("PRIMARY KEY") || upper.startsWith("UNIQUE") || upper.startsWith("FOREIGN KEY") || upper.startsWith("CONSTRAINT") || upper.startsWith("CHECK");
|
|
46
|
+
}
|
|
47
|
+
function parseTableConstraint(raw) {
|
|
48
|
+
let working = raw.trim().toUpperCase();
|
|
49
|
+
let name;
|
|
50
|
+
const constraintMatch = raw.trim().match(/^CONSTRAINT\s+"?(\w+)"?\s+(.+)$/is);
|
|
51
|
+
if (constraintMatch) {
|
|
52
|
+
name = constraintMatch[1].toLowerCase();
|
|
53
|
+
working = constraintMatch[2].trim().toUpperCase();
|
|
54
|
+
}
|
|
55
|
+
const colsMatch = working.match(/\(\s*(.+?)\s*\)/);
|
|
56
|
+
if (!colsMatch) return null;
|
|
57
|
+
const columns = colsMatch[1].split(",").map((c) => {
|
|
58
|
+
const trimmed = c.trim();
|
|
59
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return trimmed.slice(1, -1);
|
|
60
|
+
return trimmed.replace(/"/g, "").toLowerCase();
|
|
61
|
+
});
|
|
62
|
+
if (working.startsWith("PRIMARY KEY")) return {
|
|
63
|
+
type: "primary_key",
|
|
64
|
+
...name && { name },
|
|
65
|
+
columns
|
|
66
|
+
};
|
|
67
|
+
if (working.startsWith("UNIQUE")) return {
|
|
68
|
+
type: "unique",
|
|
69
|
+
...name && { name },
|
|
70
|
+
columns
|
|
71
|
+
};
|
|
72
|
+
if (working.startsWith("FOREIGN KEY")) {
|
|
73
|
+
const refMatch = working.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*(.+?)\s*\)/i);
|
|
74
|
+
if (!refMatch) return null;
|
|
75
|
+
const refColumns = refMatch[2].split(",").map((c) => {
|
|
76
|
+
const trimmed = c.trim();
|
|
77
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return trimmed.slice(1, -1);
|
|
78
|
+
return trimmed.replace(/"/g, "").toLowerCase();
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
type: "foreign_key",
|
|
82
|
+
...name && { name },
|
|
83
|
+
columns,
|
|
84
|
+
references: {
|
|
85
|
+
table: refMatch[1].toLowerCase(),
|
|
86
|
+
columns: refColumns
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function parseCreateTable(sql) {
|
|
93
|
+
const tableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("(\w+)"|(\w+))\s*\(([\s\S]+)\)/i);
|
|
94
|
+
if (!tableMatch) return err(new SchemaError(`Failed to parse CREATE TABLE statement`));
|
|
95
|
+
const tableName = tableMatch[2] ?? tableMatch[3].toLowerCase();
|
|
96
|
+
const body = tableMatch[4];
|
|
97
|
+
const lines = [];
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let current = "";
|
|
100
|
+
for (const char of body) {
|
|
101
|
+
if (char === "(") depth++;
|
|
102
|
+
else if (char === ")") depth--;
|
|
103
|
+
if (char === "," && depth === 0) {
|
|
104
|
+
lines.push(current.trim());
|
|
105
|
+
current = "";
|
|
106
|
+
} else current += char;
|
|
107
|
+
}
|
|
108
|
+
if (current.trim()) lines.push(current.trim());
|
|
109
|
+
const columns = [];
|
|
110
|
+
const constraints = [];
|
|
111
|
+
for (const line of lines) if (isTableConstraint(line)) {
|
|
112
|
+
const constraint = parseTableConstraint(line);
|
|
113
|
+
if (constraint) constraints.push(constraint);
|
|
114
|
+
} else {
|
|
115
|
+
const column = parseColumnDef(line);
|
|
116
|
+
if (column) columns.push(column);
|
|
117
|
+
}
|
|
118
|
+
const pkConstraint = constraints.find((c) => c.type === "primary_key");
|
|
119
|
+
if (pkConstraint) {
|
|
120
|
+
for (const col of columns) if (pkConstraint.columns.includes(col.name)) {
|
|
121
|
+
col.primaryKey = true;
|
|
122
|
+
col.nullable = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return ok({
|
|
126
|
+
name: tableName,
|
|
127
|
+
columns,
|
|
128
|
+
constraints
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function parseSchemaFiles(files) {
|
|
132
|
+
const snapshot = {};
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
const matches = stripComments(file.content).match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?\w+"?\s*\([\s\S]*?\);/gi);
|
|
135
|
+
if (!matches) continue;
|
|
136
|
+
for (const statement of matches) {
|
|
137
|
+
const result = parseCreateTable(statement);
|
|
138
|
+
if (result.isErr()) return err(new SchemaError(`Error in ${file.filename}: ${result.error.message}`));
|
|
139
|
+
const table = result.value;
|
|
140
|
+
if (snapshot[table.name]) return err(new SchemaError(`Duplicate table "${table.name}" found in ${file.filename}`));
|
|
141
|
+
snapshot[table.name] = table;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return ok(snapshot);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/postgres/pg/generator.ts
|
|
148
|
+
function quoteIdent(name) {
|
|
149
|
+
if (name !== name.toLowerCase()) return `"${name}"`;
|
|
150
|
+
return name;
|
|
151
|
+
}
|
|
152
|
+
function columnToSQL(col) {
|
|
153
|
+
const parts = [quoteIdent(col.name), col.type];
|
|
154
|
+
if (col.primaryKey) parts.push("PRIMARY KEY");
|
|
155
|
+
if (!col.nullable && !col.primaryKey) parts.push("NOT NULL");
|
|
156
|
+
if (col.unique) parts.push("UNIQUE");
|
|
157
|
+
if (col.default !== void 0) parts.push(`DEFAULT ${col.default}`);
|
|
158
|
+
if (col.references) parts.push(`REFERENCES ${quoteIdent(col.references.table)}(${quoteIdent(col.references.column)})`);
|
|
159
|
+
return parts.join(" ");
|
|
160
|
+
}
|
|
161
|
+
function createTableSQL(table) {
|
|
162
|
+
const cols = table.columns.map((c) => ` ${columnToSQL(c)}`);
|
|
163
|
+
for (const constraint of table.constraints) {
|
|
164
|
+
const name = constraint.name ? `CONSTRAINT ${constraint.name} ` : "";
|
|
165
|
+
if (constraint.type === "primary_key") {
|
|
166
|
+
if (constraint.columns.length > 1) cols.push(` ${name}PRIMARY KEY (${constraint.columns.map(quoteIdent).join(", ")})`);
|
|
167
|
+
} else if (constraint.type === "unique") {
|
|
168
|
+
if (constraint.columns.length > 1) cols.push(` ${name}UNIQUE (${constraint.columns.map(quoteIdent).join(", ")})`);
|
|
169
|
+
} else if (constraint.type === "foreign_key" && constraint.references) cols.push(` ${name}FOREIGN KEY (${constraint.columns.map(quoteIdent).join(", ")}) REFERENCES ${quoteIdent(constraint.references.table)}(${constraint.references.columns.map(quoteIdent).join(", ")})`);
|
|
170
|
+
}
|
|
171
|
+
return `CREATE TABLE ${quoteIdent(table.name)} (\n${cols.join(",\n")}\n);`;
|
|
172
|
+
}
|
|
173
|
+
function operationToSQL(op) {
|
|
174
|
+
switch (op.type) {
|
|
175
|
+
case "create_table": return createTableSQL(op.table);
|
|
176
|
+
case "drop_table": return `DROP TABLE IF EXISTS ${quoteIdent(op.tableName)};`;
|
|
177
|
+
case "add_column": return `ALTER TABLE ${quoteIdent(op.tableName)} ADD COLUMN ${columnToSQL(op.column)};`;
|
|
178
|
+
case "drop_column": return `ALTER TABLE ${quoteIdent(op.tableName)} DROP COLUMN ${quoteIdent(op.columnName)};`;
|
|
179
|
+
case "alter_column": {
|
|
180
|
+
const stmts = [];
|
|
181
|
+
const { tableName, columnName, from, to } = op;
|
|
182
|
+
const qt = quoteIdent(tableName);
|
|
183
|
+
const qc = quoteIdent(columnName);
|
|
184
|
+
if (from.type !== to.type) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET DATA TYPE ${to.type};`);
|
|
185
|
+
if (from.nullable && !to.nullable) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET NOT NULL;`);
|
|
186
|
+
else if (!from.nullable && to.nullable) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} DROP NOT NULL;`);
|
|
187
|
+
if (from.default !== to.default) if (to.default !== void 0) stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} SET DEFAULT ${to.default};`);
|
|
188
|
+
else stmts.push(`ALTER TABLE ${qt} ALTER COLUMN ${qc} DROP DEFAULT;`);
|
|
189
|
+
if (!from.unique && to.unique) stmts.push(`ALTER TABLE ${qt} ADD CONSTRAINT ${tableName}_${columnName}_unique UNIQUE (${qc});`);
|
|
190
|
+
else if (from.unique && !to.unique) stmts.push(`ALTER TABLE ${qt} DROP CONSTRAINT ${tableName}_${columnName}_unique;`);
|
|
191
|
+
return stmts.join("\n");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function generateSQL(operations) {
|
|
196
|
+
if (operations.length === 0) return "";
|
|
197
|
+
return `BEGIN;\n\n${operations.map(operationToSQL).filter(Boolean).join("\n\n")}\n\nCOMMIT;\n`;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/postgres/pg/types.ts
|
|
201
|
+
function sqlTypeToJsonSchema(sqlType) {
|
|
202
|
+
switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
|
|
203
|
+
case "SERIAL":
|
|
204
|
+
case "BIGSERIAL":
|
|
205
|
+
case "INTEGER":
|
|
206
|
+
case "INT":
|
|
207
|
+
case "INT4":
|
|
208
|
+
case "SMALLINT":
|
|
209
|
+
case "INT2":
|
|
210
|
+
case "BIGINT":
|
|
211
|
+
case "INT8": return { type: "integer" };
|
|
212
|
+
case "REAL":
|
|
213
|
+
case "FLOAT4":
|
|
214
|
+
case "DOUBLE PRECISION":
|
|
215
|
+
case "FLOAT8":
|
|
216
|
+
case "NUMERIC":
|
|
217
|
+
case "DECIMAL": return { type: "number" };
|
|
218
|
+
case "BOOLEAN":
|
|
219
|
+
case "BOOL": return { type: "boolean" };
|
|
220
|
+
case "TEXT": return { type: "string" };
|
|
221
|
+
case "VARCHAR":
|
|
222
|
+
case "CHARACTER VARYING": {
|
|
223
|
+
const lenMatch = sqlType.match(/\((\d+)\)/);
|
|
224
|
+
return lenMatch ? {
|
|
225
|
+
type: "string",
|
|
226
|
+
maxLength: parseInt(lenMatch[1], 10)
|
|
227
|
+
} : { type: "string" };
|
|
228
|
+
}
|
|
229
|
+
case "CHAR":
|
|
230
|
+
case "CHARACTER": {
|
|
231
|
+
const lenMatch = sqlType.match(/\((\d+)\)/);
|
|
232
|
+
return lenMatch ? {
|
|
233
|
+
type: "string",
|
|
234
|
+
maxLength: parseInt(lenMatch[1], 10)
|
|
235
|
+
} : { type: "string" };
|
|
236
|
+
}
|
|
237
|
+
case "UUID": return {
|
|
238
|
+
type: "string",
|
|
239
|
+
format: "uuid"
|
|
240
|
+
};
|
|
241
|
+
case "DATE": return {
|
|
242
|
+
type: "string",
|
|
243
|
+
format: "date"
|
|
244
|
+
};
|
|
245
|
+
case "TIMESTAMP":
|
|
246
|
+
case "TIMESTAMP WITHOUT TIME ZONE":
|
|
247
|
+
case "TIMESTAMP WITH TIME ZONE":
|
|
248
|
+
case "TIMESTAMPTZ": return {
|
|
249
|
+
type: "string",
|
|
250
|
+
format: "date-time"
|
|
251
|
+
};
|
|
252
|
+
case "TIME":
|
|
253
|
+
case "TIME WITHOUT TIME ZONE":
|
|
254
|
+
case "TIME WITH TIME ZONE":
|
|
255
|
+
case "TIMETZ": return {
|
|
256
|
+
type: "string",
|
|
257
|
+
format: "time"
|
|
258
|
+
};
|
|
259
|
+
case "JSON":
|
|
260
|
+
case "JSONB": return { type: "object" };
|
|
261
|
+
case "BYTEA": return {
|
|
262
|
+
type: "string",
|
|
263
|
+
format: "byte"
|
|
264
|
+
};
|
|
265
|
+
default: return { type: "string" };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function sqlTypeToTsType(sqlType) {
|
|
269
|
+
switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
|
|
270
|
+
case "SERIAL":
|
|
271
|
+
case "BIGSERIAL":
|
|
272
|
+
case "INTEGER":
|
|
273
|
+
case "INT":
|
|
274
|
+
case "INT4":
|
|
275
|
+
case "SMALLINT":
|
|
276
|
+
case "INT2":
|
|
277
|
+
case "BIGINT":
|
|
278
|
+
case "INT8":
|
|
279
|
+
case "REAL":
|
|
280
|
+
case "FLOAT4":
|
|
281
|
+
case "DOUBLE PRECISION":
|
|
282
|
+
case "FLOAT8":
|
|
283
|
+
case "NUMERIC":
|
|
284
|
+
case "DECIMAL": return "number";
|
|
285
|
+
case "BOOLEAN":
|
|
286
|
+
case "BOOL": return "boolean";
|
|
287
|
+
case "JSON":
|
|
288
|
+
case "JSONB": return "unknown";
|
|
289
|
+
case "BYTEA": return "Buffer";
|
|
290
|
+
default: return "string";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/postgres/pg/query-codegen.ts
|
|
295
|
+
function camelCase(str) {
|
|
296
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
297
|
+
}
|
|
298
|
+
function generateParamInterface(query) {
|
|
299
|
+
if (query.params.length === 0) return null;
|
|
300
|
+
return `export interface ${`${pascalCase(query.name)}Params`} {\n${query.params.map((p) => {
|
|
301
|
+
const base = sqlTypeToTsType(p.sqlType);
|
|
302
|
+
const type = p.nullable ? `${base} | null` : base;
|
|
303
|
+
return ` ${p.name}: ${type};`;
|
|
304
|
+
}).join("\n")}\n}`;
|
|
305
|
+
}
|
|
306
|
+
function generateParamSchema(query) {
|
|
307
|
+
if (query.params.length === 0) return null;
|
|
308
|
+
const name = `${pascalCase(query.name)}ParamsSchema`;
|
|
309
|
+
const properties = {};
|
|
310
|
+
const required = [];
|
|
311
|
+
for (const param of query.params) {
|
|
312
|
+
const jsonType = sqlTypeToJsonSchema(param.sqlType);
|
|
313
|
+
if (param.nullable) properties[param.name] = {
|
|
314
|
+
...jsonType,
|
|
315
|
+
type: Array.isArray(jsonType.type) ? [...jsonType.type, "null"] : [jsonType.type, "null"]
|
|
316
|
+
};
|
|
317
|
+
else {
|
|
318
|
+
properties[param.name] = jsonType;
|
|
319
|
+
required.push(param.name);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const schema = {
|
|
323
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
324
|
+
title: `${pascalCase(query.name)}Params`,
|
|
325
|
+
type: "object",
|
|
326
|
+
properties,
|
|
327
|
+
required,
|
|
328
|
+
additionalProperties: false
|
|
329
|
+
};
|
|
330
|
+
return `export const ${name} = ${JSON.stringify(schema, null, 2)} as const;`;
|
|
331
|
+
}
|
|
332
|
+
function generateResultType(query, snapshot) {
|
|
333
|
+
if (query.command === "exec" || query.command === "execrows" || query.command === "execresult") return null;
|
|
334
|
+
if (!query.returnsTable || !snapshot[query.returnsTable]) return null;
|
|
335
|
+
const table = snapshot[query.returnsTable];
|
|
336
|
+
const typeName = pascalCase(query.returnsTable);
|
|
337
|
+
if (query.returnsColumns.length === table.columns.length && query.returnsColumns.every((c) => table.columns.some((tc) => tc.name === c))) return typeName;
|
|
338
|
+
return `Pick<${typeName}, ${query.returnsColumns.map((c) => `"${c}"`).join(" | ")}>`;
|
|
339
|
+
}
|
|
340
|
+
function generateReturnType(query, resultType) {
|
|
341
|
+
switch (query.command) {
|
|
342
|
+
case "one": return `Promise<${resultType ?? "unknown"} | null>`;
|
|
343
|
+
case "many": return `Promise<${resultType ?? "unknown"}[]>`;
|
|
344
|
+
case "exec": return "Promise<void>";
|
|
345
|
+
case "execrows": return "Promise<number>";
|
|
346
|
+
case "execresult": return "Promise<QueryResult>";
|
|
347
|
+
default: return "Promise<void>";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function generateFunctionBody(query) {
|
|
351
|
+
const paramArgs = query.params.map((p) => `params.${p.name}`).join(", ");
|
|
352
|
+
const queryArgs = query.params.length > 0 ? `sql, [${paramArgs}]` : "sql";
|
|
353
|
+
switch (query.command) {
|
|
354
|
+
case "one": return ` const result = await client.query(${queryArgs});\n return result.rows[0] ?? null;`;
|
|
355
|
+
case "many": return ` const result = await client.query(${queryArgs});\n return result.rows;`;
|
|
356
|
+
case "exec": return ` await client.query(${queryArgs});`;
|
|
357
|
+
case "execrows": return ` const result = await client.query(${queryArgs});\n return result.rowCount ?? 0;`;
|
|
358
|
+
case "execresult": return ` return await client.query(${queryArgs});`;
|
|
359
|
+
default: return ` await client.query(${queryArgs});`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function generateFunction(query, snapshot) {
|
|
363
|
+
const fnName = camelCase(query.name);
|
|
364
|
+
const returnType = generateReturnType(query, generateResultType(query, snapshot));
|
|
365
|
+
const hasParams = query.params.length > 0;
|
|
366
|
+
const paramsType = hasParams ? `${pascalCase(query.name)}Params` : null;
|
|
367
|
+
const args = hasParams ? `client: Client | Pool, params: ${paramsType}` : "client: Client | Pool";
|
|
368
|
+
const sql = query.expandedSql.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
369
|
+
const lines = [];
|
|
370
|
+
lines.push(`export async function ${fnName}(${args}): ${returnType} {`);
|
|
371
|
+
lines.push(` const sql = '${sql}';`);
|
|
372
|
+
lines.push(generateFunctionBody(query));
|
|
373
|
+
lines.push("}");
|
|
374
|
+
return lines.join("\n");
|
|
375
|
+
}
|
|
376
|
+
function generateQueryFiles(queries, snapshot) {
|
|
377
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const query of queries) {
|
|
379
|
+
const key = query.sourceFile.replace(/\.sql$/, "");
|
|
380
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
381
|
+
grouped.get(key).push(query);
|
|
382
|
+
}
|
|
383
|
+
const files = {};
|
|
384
|
+
for (const [basename, fileQueries] of grouped) {
|
|
385
|
+
const lines = ["// This file is auto-generated by tsqx. Do not edit manually.", "import type { Client, Pool, QueryResult } from \"pg\";"];
|
|
386
|
+
const tableImports = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const query of fileQueries) if (query.returnsTable && snapshot[query.returnsTable]) tableImports.add(query.returnsTable);
|
|
388
|
+
if (tableImports.size > 0) {
|
|
389
|
+
const imports = [...tableImports].map((t) => `type ${pascalCase(t)}`).join(", ");
|
|
390
|
+
lines.push(`import { ${imports} } from "../../generated";`);
|
|
391
|
+
}
|
|
392
|
+
lines.push("");
|
|
393
|
+
for (const query of fileQueries) {
|
|
394
|
+
const paramInterface = generateParamInterface(query);
|
|
395
|
+
if (paramInterface) {
|
|
396
|
+
lines.push(paramInterface);
|
|
397
|
+
lines.push("");
|
|
398
|
+
}
|
|
399
|
+
const paramSchema = generateParamSchema(query);
|
|
400
|
+
if (paramSchema) {
|
|
401
|
+
lines.push(paramSchema);
|
|
402
|
+
lines.push("");
|
|
403
|
+
}
|
|
404
|
+
lines.push(generateFunction(query, snapshot));
|
|
405
|
+
lines.push("");
|
|
406
|
+
}
|
|
407
|
+
files[`${basename}.ts`] = lines.join("\n");
|
|
408
|
+
}
|
|
409
|
+
return files;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/postgres/pg/index.ts
|
|
413
|
+
function pgDialect() {
|
|
414
|
+
return {
|
|
415
|
+
name: "pg",
|
|
416
|
+
parseSchema: parseSchemaFiles,
|
|
417
|
+
generateSQL,
|
|
418
|
+
sqlTypeToJsonSchema,
|
|
419
|
+
sqlTypeToTsType,
|
|
420
|
+
generateQueryCode: generateQueryFiles
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
export { pgDialect };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsqx/kit",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"require": "./dist/index.cjs",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./postgres/pg": {
|
|
13
|
+
"import": "./dist/postgres/pg/index.mjs",
|
|
14
|
+
"require": "./dist/postgres/pg/index.cjs",
|
|
15
|
+
"types": "./dist/postgres/pg/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"neverthrow": "^8.2.0",
|
|
26
|
+
"@tsqx/core": "^0.0.4"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/chtushar/tsqx",
|
|
31
|
+
"directory": "packages/kit"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [],
|
|
34
|
+
"license": "ISC",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"dev": "tsdown --watch"
|
|
38
|
+
}
|
|
39
|
+
}
|