@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 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;
@@ -0,0 +1,6 @@
1
+ import { Config, ConfigInput } from "@tsqx/core";
2
+
3
+ //#region src/config.d.ts
4
+ declare function defineConfig(config: ConfigInput): Config;
5
+ //#endregion
6
+ export { type Config, type ConfigInput, defineConfig };
@@ -0,0 +1,6 @@
1
+ import { Config, ConfigInput } from "@tsqx/core";
2
+
3
+ //#region src/config.d.ts
4
+ declare function defineConfig(config: ConfigInput): Config;
5
+ //#endregion
6
+ export { type Config, type ConfigInput, defineConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import { parseConfig } from "@tsqx/core";
2
+ //#region src/config.ts
3
+ function defineConfig(config) {
4
+ const result = parseConfig(config);
5
+ if (result.isErr()) throw result.error;
6
+ return result.value;
7
+ }
8
+ //#endregion
9
+ export { defineConfig };
@@ -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,6 @@
1
+ import { Dialect } from "@tsqx/core";
2
+
3
+ //#region src/postgres/pg/index.d.ts
4
+ declare function pgDialect(): Dialect;
5
+ //#endregion
6
+ export { pgDialect };
@@ -0,0 +1,6 @@
1
+ import { Dialect } from "@tsqx/core";
2
+
3
+ //#region src/postgres/pg/index.d.ts
4
+ declare function pgDialect(): Dialect;
5
+ //#endregion
6
+ export { 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
+ }