@technicity/data-service-generator 0.23.0-next.1 → 0.23.0-next.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.
|
@@ -44,6 +44,7 @@ const isNotNullOrUndefined_1 = require("../lib/isNotNullOrUndefined");
|
|
|
44
44
|
const pg_1 = require("pg");
|
|
45
45
|
const MySQL_1 = require("../runtime/lib/MySQL");
|
|
46
46
|
const capitalizeFirstLetter_1 = require("../lib/capitalizeFirstLetter");
|
|
47
|
+
const pg2sqliteSchema_1 = require("./pg2sqliteSchema");
|
|
47
48
|
const ctxStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
48
49
|
function getCtx() {
|
|
49
50
|
const c = ctxStorage.getStore();
|
|
@@ -68,7 +69,8 @@ async function generate(input) {
|
|
|
68
69
|
const ctx = {
|
|
69
70
|
runId: node_crypto_1.default.randomUUID(),
|
|
70
71
|
dialect: input.dialect,
|
|
71
|
-
query: undefined
|
|
72
|
+
query: undefined,
|
|
73
|
+
pool: undefined
|
|
72
74
|
};
|
|
73
75
|
return ctxStorage.run(ctx, async () => {
|
|
74
76
|
init(input);
|
|
@@ -174,36 +176,46 @@ async function generate(input) {
|
|
|
174
176
|
fs.writeFileSync(path.join(tmpBuildOutputPath, "IRuntime.d.ts"), fs.existsSync(path.join(__dirname, "../runtime", "IRuntime.d.ts"))
|
|
175
177
|
? fs.readFileSync(path.join(__dirname, "../runtime", "IRuntime.d.ts"), "utf-8")
|
|
176
178
|
: fs.readFileSync(sourceIRuntimeFilePath, "utf-8"));
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
if (input.outputSqliteSchema) {
|
|
180
|
+
let schemaSqlite = null;
|
|
181
|
+
if (getCtx().dialect === "mysql") {
|
|
182
|
+
// Since mysql2sqlite outputs a malformed string if a column
|
|
183
|
+
// has the name `enum`, temporarily change the name to something else,
|
|
184
|
+
// then change it back.
|
|
185
|
+
const enumMarker = "`" + node_crypto_1.default.randomUUID() + "`";
|
|
186
|
+
const schemaMySql = Object.values(artifacts)
|
|
187
|
+
.reduce((acc, x) => {
|
|
188
|
+
let d = x.dump?.schema;
|
|
189
|
+
if (!d) {
|
|
190
|
+
return acc;
|
|
191
|
+
}
|
|
192
|
+
d = d.replace(/`enum`/g, enumMarker);
|
|
193
|
+
d += ";";
|
|
194
|
+
acc.push(d);
|
|
186
195
|
return acc;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
196
|
+
}, [])
|
|
197
|
+
.join("\n\n");
|
|
198
|
+
const mysql2SqliteSrc = getMysql2sqliteSrc();
|
|
199
|
+
const mysql2SqlitePath = path.join(tmpDirPath, "mysql2sqlite");
|
|
200
|
+
fs.writeFileSync(mysql2SqlitePath, mysql2SqliteSrc);
|
|
201
|
+
fs.chmodSync(mysql2SqlitePath, 0o755);
|
|
202
|
+
const tmpMySqlSchemaFilename = "tmp.sql";
|
|
203
|
+
const tmpMySqlSchemaPath = path.join(tmpDirPath, tmpMySqlSchemaFilename);
|
|
204
|
+
fs.writeFileSync(tmpMySqlSchemaPath, schemaMySql);
|
|
205
|
+
schemaSqlite = child_process
|
|
206
|
+
.execFileSync(mysql2SqlitePath, [tmpMySqlSchemaFilename], {
|
|
207
|
+
cwd: tmpDirPath
|
|
208
|
+
})
|
|
209
|
+
.toString();
|
|
210
|
+
schemaSqlite = schemaSqlite.replace(new RegExp(enumMarker, "g"), "`enum`");
|
|
211
|
+
}
|
|
212
|
+
else if (getCtx().dialect === "postgresql") {
|
|
213
|
+
schemaSqlite = await (0, pg2sqliteSchema_1.convertPgSchemaToSqlite)(getCtx().pool);
|
|
214
|
+
}
|
|
215
|
+
if (schemaSqlite) {
|
|
216
|
+
const src = prettier.format(`module.exports = { schema: \`${schemaSqlite.replace(/`/g, "\\`")}\` }`, { parser: "babel" });
|
|
217
|
+
fs.writeFileSync(path.join(tmpBuildOutputPath, "artifacts.sqlite.js"), src);
|
|
218
|
+
}
|
|
207
219
|
}
|
|
208
220
|
if (!fs.existsSync(outdir)) {
|
|
209
221
|
fse.mkdirpSync(outdir);
|
|
@@ -217,24 +229,28 @@ function init(input) {
|
|
|
217
229
|
const ctx = getCtx();
|
|
218
230
|
const { database, user, password, host, port, server } = input;
|
|
219
231
|
if (ctx.dialect === "mysql") {
|
|
220
|
-
const
|
|
232
|
+
const connectionOpts = {
|
|
221
233
|
user,
|
|
222
234
|
password,
|
|
223
|
-
host,
|
|
224
|
-
port,
|
|
235
|
+
host: host ?? "localhost",
|
|
236
|
+
port: port ?? 3306,
|
|
225
237
|
database
|
|
226
|
-
}
|
|
238
|
+
};
|
|
239
|
+
const mysql = new MySQL_1.MySQL(connectionOpts);
|
|
227
240
|
ctx.query = mysql.query.bind(mysql);
|
|
241
|
+
ctx.pool = mysql;
|
|
228
242
|
}
|
|
229
243
|
if (ctx.dialect === "postgresql") {
|
|
230
|
-
const
|
|
244
|
+
const connectionOpts = {
|
|
231
245
|
host: host ?? "localhost",
|
|
232
246
|
port: port ?? 5432,
|
|
233
247
|
user,
|
|
234
248
|
password,
|
|
235
249
|
database
|
|
236
|
-
}
|
|
250
|
+
};
|
|
251
|
+
const pool = new pg_1.Pool(connectionOpts);
|
|
237
252
|
ctx.query = (q, values) => pool.query(q, values ?? []).then((r) => r.rows);
|
|
253
|
+
ctx.pool = pool;
|
|
238
254
|
}
|
|
239
255
|
}
|
|
240
256
|
// It's a bit awkward to put __whereNeedsProcessing, __prepareWhere on the class,
|
|
@@ -278,7 +294,7 @@ async function getSDKSource(input, specialCaseUuidColumn, supplementClientOpts,
|
|
|
278
294
|
runtime: any;
|
|
279
295
|
clientOpts: { [k: string]: any; },
|
|
280
296
|
otherOpts?: { [k: string]: any; },
|
|
281
|
-
passBeforeValueToAfterCallback
|
|
297
|
+
passBeforeValueToAfterCallback?: boolean,
|
|
282
298
|
}) {
|
|
283
299
|
let otherOpts = opts.otherOpts ?? {};
|
|
284
300
|
if (opts.clientOpts.filename === ":memory:") {
|
|
@@ -291,7 +307,7 @@ async function getSDKSource(input, specialCaseUuidColumn, supplementClientOpts,
|
|
|
291
307
|
: "otherOpts"}, artifacts);
|
|
292
308
|
this.onHandlerMap = new Map();
|
|
293
309
|
this.eventTarget = new EventTarget();
|
|
294
|
-
this.passBeforeValueToAfterCallback = opts.passBeforeValueToAfterCallback;
|
|
310
|
+
this.passBeforeValueToAfterCallback = opts.passBeforeValueToAfterCallback ?? false;
|
|
295
311
|
}
|
|
296
312
|
|
|
297
313
|
$use(middleware: TMiddleware) {
|
|
@@ -1622,7 +1638,7 @@ const getRelationInfo = (0, memoize_1.default)(async function getRelationInfo(ta
|
|
|
1622
1638
|
return acc;
|
|
1623
1639
|
}, []);
|
|
1624
1640
|
out = out.concat(relationsManyToMany);
|
|
1625
|
-
out = _.sortBy((x) => x.table, out);
|
|
1641
|
+
out = _.sortBy([(x) => x.table, (x) => x.name], out);
|
|
1626
1642
|
return out;
|
|
1627
1643
|
}, (table) => getCtx().runId + ":" + table);
|
|
1628
1644
|
function getRelationManyToOneFieldName(x) {
|
|
@@ -1784,6 +1800,32 @@ const getPgEnumDefinition = (0, memoize_1.default)(async function getPgEnumDefin
|
|
|
1784
1800
|
const labels = rows.map((r) => String(r.enumlabel).replace(/'/g, "''"));
|
|
1785
1801
|
return "enum('" + labels.join("', '") + "')";
|
|
1786
1802
|
}, (udtName) => getCtx().runId + ":" + udtName);
|
|
1803
|
+
/** List enum type names in the public schema (for building CREATE TYPE DDL). */
|
|
1804
|
+
async function getPublicEnumTypeNames() {
|
|
1805
|
+
const { dialect, query } = getCtx();
|
|
1806
|
+
if (dialect !== "postgresql")
|
|
1807
|
+
return [];
|
|
1808
|
+
const rows = await query(`SELECT t.typname FROM pg_type t
|
|
1809
|
+
JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
|
|
1810
|
+
WHERE n.nspname = 'public' AND t.typtype = 'e'
|
|
1811
|
+
ORDER BY t.typname`);
|
|
1812
|
+
return rows.map((r) => String(r.typname));
|
|
1813
|
+
}
|
|
1814
|
+
const getPgCreateTypeEnumStatement = (0, memoize_1.default)(async function getPgCreateTypeEnumStatement(typname) {
|
|
1815
|
+
const { dialect, query } = getCtx();
|
|
1816
|
+
if (dialect !== "postgresql")
|
|
1817
|
+
return null;
|
|
1818
|
+
const rows = await query(`SELECT e.enumlabel FROM pg_enum e
|
|
1819
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
1820
|
+
JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
|
|
1821
|
+
WHERE t.typname = $1 AND n.nspname = 'public'
|
|
1822
|
+
ORDER BY e.enumsortorder`, [typname]);
|
|
1823
|
+
if (rows.length === 0)
|
|
1824
|
+
return null;
|
|
1825
|
+
const quoted = typname.replace(/"/g, '""');
|
|
1826
|
+
const labels = rows.map((r) => "'" + String(r.enumlabel).replace(/'/g, "''") + "'");
|
|
1827
|
+
return `CREATE TYPE "${quoted}" AS ENUM (${labels.join(", ")});`;
|
|
1828
|
+
}, (typname) => getCtx().runId + ":" + typname);
|
|
1787
1829
|
const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
1788
1830
|
const { dialect, query } = getCtx();
|
|
1789
1831
|
if (dialect === "mysql") {
|
|
@@ -1839,7 +1881,10 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1839
1881
|
Type: type,
|
|
1840
1882
|
Null: c.is_nullable === "YES" ? "YES" : "NO",
|
|
1841
1883
|
Key: keyMap.get(c.Field) ?? "",
|
|
1842
|
-
Default: c.Default ?? ""
|
|
1884
|
+
Default: c.Default ?? "",
|
|
1885
|
+
...(c.data_type === "USER-DEFINED" && c.udt_name != null
|
|
1886
|
+
? { PgType: c.udt_name }
|
|
1887
|
+
: {})
|
|
1843
1888
|
};
|
|
1844
1889
|
});
|
|
1845
1890
|
}
|
|
@@ -1859,7 +1904,21 @@ async function getShowCreateTable(table) {
|
|
|
1859
1904
|
]);
|
|
1860
1905
|
const refByFk = new Map(relations.map((r) => [r.foreignKey, r]));
|
|
1861
1906
|
const columnDefs = tableMeta.map((c) => {
|
|
1862
|
-
|
|
1907
|
+
const isSerialPk = c.Key === "PRI" &&
|
|
1908
|
+
c.Default != null &&
|
|
1909
|
+
c.Default !== "" &&
|
|
1910
|
+
/nextval\s*\(/i.test(c.Default);
|
|
1911
|
+
if (isSerialPk) {
|
|
1912
|
+
const baseType = (c.PgType ?? c.Type).toLowerCase();
|
|
1913
|
+
const serialType = baseType === "bigint" || baseType === "int8"
|
|
1914
|
+
? "BIGSERIAL"
|
|
1915
|
+
: baseType === "smallint" || baseType === "int2"
|
|
1916
|
+
? "SMALLSERIAL"
|
|
1917
|
+
: "SERIAL";
|
|
1918
|
+
return `"${c.Field.replace(/"/g, '""')}" ${serialType} PRIMARY KEY`;
|
|
1919
|
+
}
|
|
1920
|
+
const pgType = c.PgType ?? c.Type;
|
|
1921
|
+
let def = `"${c.Field.replace(/"/g, '""')}" ${pgType} ${c.Null === "YES" ? "NULL" : "NOT NULL"}`;
|
|
1863
1922
|
if (c.Default != null && c.Default !== "") {
|
|
1864
1923
|
def += ` DEFAULT ${c.Default}`;
|
|
1865
1924
|
}
|
|
@@ -1873,7 +1932,7 @@ async function getShowCreateTable(table) {
|
|
|
1873
1932
|
}
|
|
1874
1933
|
return def;
|
|
1875
1934
|
});
|
|
1876
|
-
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n)
|
|
1935
|
+
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n);`;
|
|
1877
1936
|
}
|
|
1878
1937
|
return Promise.resolve(null);
|
|
1879
1938
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertPgSchemaToSqlite = convertPgSchemaToSqlite;
|
|
4
|
+
/**
|
|
5
|
+
* Reflects the PostgreSQL schema from the given connection and returns
|
|
6
|
+
* SQLite-compatible CREATE TABLE statements as a string.
|
|
7
|
+
*/
|
|
8
|
+
async function convertPgSchemaToSqlite(pool) {
|
|
9
|
+
const columnsResult = await pool.query(`
|
|
10
|
+
SELECT table_schema, table_name, column_name, ordinal_position,
|
|
11
|
+
data_type, character_maximum_length, is_nullable, column_default
|
|
12
|
+
FROM information_schema.columns
|
|
13
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
14
|
+
ORDER BY table_schema, table_name, ordinal_position
|
|
15
|
+
`);
|
|
16
|
+
const pkResult = await pool.query(`
|
|
17
|
+
SELECT tc.table_schema, tc.table_name, kcu.column_name
|
|
18
|
+
FROM information_schema.table_constraints tc
|
|
19
|
+
JOIN information_schema.key_column_usage kcu
|
|
20
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
21
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
22
|
+
`);
|
|
23
|
+
const primaryKeys = new Set(pkResult.rows.map((r) => `${r.table_schema}.${r.table_name}.${r.column_name}`));
|
|
24
|
+
const byTable = {};
|
|
25
|
+
for (const c of columnsResult.rows) {
|
|
26
|
+
const key = `${c.table_schema}.${c.table_name}`;
|
|
27
|
+
if (!byTable[key])
|
|
28
|
+
byTable[key] = [];
|
|
29
|
+
byTable[key].push(c);
|
|
30
|
+
}
|
|
31
|
+
const statements = [];
|
|
32
|
+
for (const [key, cols] of Object.entries(byTable)) {
|
|
33
|
+
const tableName = key.split(".").slice(-1)[0];
|
|
34
|
+
const parts = cols.map((c) => {
|
|
35
|
+
const sqliteType = toSqliteType(c.data_type);
|
|
36
|
+
const pk = primaryKeys.has(`${c.table_schema}.${c.table_name}.${c.column_name}`);
|
|
37
|
+
const notNull = c.is_nullable === "NO" && !(c.column_default?.includes("nextval") ?? false);
|
|
38
|
+
const defaultVal = pgDefaultToSqlite(c.column_default);
|
|
39
|
+
const pkClause = pk ? " PRIMARY KEY" : "";
|
|
40
|
+
const nullClause = notNull && !pk ? " NOT NULL" : "";
|
|
41
|
+
const defaultClause = defaultVal != null ? ` DEFAULT ${defaultVal}` : "";
|
|
42
|
+
return ` "${c.column_name}" ${sqliteType}${pkClause}${nullClause}${defaultClause}`;
|
|
43
|
+
});
|
|
44
|
+
statements.push(`CREATE TABLE "${tableName}" (\n${parts.join(",\n")}\n);`);
|
|
45
|
+
}
|
|
46
|
+
return statements.join("\n\n") + "\n";
|
|
47
|
+
}
|
|
48
|
+
const PG_TO_SQLITE_TYPE = {
|
|
49
|
+
smallint: "INTEGER",
|
|
50
|
+
integer: "INTEGER",
|
|
51
|
+
bigint: "INTEGER",
|
|
52
|
+
serial: "INTEGER",
|
|
53
|
+
bigserial: "INTEGER",
|
|
54
|
+
smallserial: "INTEGER",
|
|
55
|
+
real: "REAL",
|
|
56
|
+
"double precision": "REAL",
|
|
57
|
+
numeric: "REAL",
|
|
58
|
+
decimal: "REAL",
|
|
59
|
+
boolean: "INTEGER",
|
|
60
|
+
character: "TEXT",
|
|
61
|
+
"character varying": "TEXT",
|
|
62
|
+
varchar: "TEXT",
|
|
63
|
+
char: "TEXT",
|
|
64
|
+
text: "TEXT",
|
|
65
|
+
timestamp: "TEXT",
|
|
66
|
+
"timestamp with time zone": "TEXT",
|
|
67
|
+
"timestamp without time zone": "TEXT",
|
|
68
|
+
date: "TEXT",
|
|
69
|
+
time: "TEXT",
|
|
70
|
+
json: "TEXT",
|
|
71
|
+
jsonb: "TEXT",
|
|
72
|
+
uuid: "TEXT",
|
|
73
|
+
bytea: "BLOB"
|
|
74
|
+
};
|
|
75
|
+
function toSqliteType(pgType) {
|
|
76
|
+
const base = (pgType ?? "")
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replace(/^(.*?)(\s+with.*|\s+without.*)$/, "$1")
|
|
79
|
+
.trim();
|
|
80
|
+
return PG_TO_SQLITE_TYPE[base] ?? "TEXT";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Converts a PostgreSQL column_default expression to a SQLite DEFAULT clause
|
|
84
|
+
* value (without the "DEFAULT" keyword). Returns null if we should not emit a default.
|
|
85
|
+
*/
|
|
86
|
+
function pgDefaultToSqlite(columnDefault) {
|
|
87
|
+
if (columnDefault == null || columnDefault.trim() === "")
|
|
88
|
+
return null;
|
|
89
|
+
const raw = columnDefault.trim();
|
|
90
|
+
// Skip sequence-based defaults (serial/identity); SQLite uses INTEGER PRIMARY KEY
|
|
91
|
+
if (raw.toLowerCase().includes("nextval"))
|
|
92
|
+
return null;
|
|
93
|
+
// Timestamp / time
|
|
94
|
+
if (/^(now\s*\(\s*\)|current_timestamp)$/i.test(raw))
|
|
95
|
+
return "CURRENT_TIMESTAMP";
|
|
96
|
+
if (/^current_date$/i.test(raw))
|
|
97
|
+
return "CURRENT_DATE";
|
|
98
|
+
if (/^current_time$/i.test(raw))
|
|
99
|
+
return "CURRENT_TIME";
|
|
100
|
+
// Booleans
|
|
101
|
+
if (/^\s*true\s*$/i.test(raw))
|
|
102
|
+
return "1";
|
|
103
|
+
if (/^\s*false\s*$/i.test(raw))
|
|
104
|
+
return "0";
|
|
105
|
+
// Numeric literal (optionally with ::type)
|
|
106
|
+
const numericMatch = raw.match(/^(-?\d+(?:\.\d+)?)\s*(?:::\s*\w+(?:\s+\w+)*)?\s*$/);
|
|
107
|
+
if (numericMatch)
|
|
108
|
+
return numericMatch[1];
|
|
109
|
+
// String literal: '...' with optional ::type
|
|
110
|
+
const stringMatch = raw.match(/^'(.*)'\s*(?:::\s*\w+(?:\s+\w+)*)?\s*$/s);
|
|
111
|
+
if (stringMatch) {
|
|
112
|
+
const inner = stringMatch[1].replace(/'/g, "''");
|
|
113
|
+
return `'${inner}'`;
|
|
114
|
+
}
|
|
115
|
+
// Unknown expression: strip ::type and emit in parens if it looks safe
|
|
116
|
+
const withoutCast = raw.replace(/\s*::\s*[\w\s]+$/, "").trim();
|
|
117
|
+
if (withoutCast && /^[\w().\s+-]+$/i.test(withoutCast)) {
|
|
118
|
+
return `(${withoutCast})`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|