@technicity/data-service-generator 0.23.0-next.2 → 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,38 +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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
219
|
}
|
|
210
220
|
if (!fs.existsSync(outdir)) {
|
|
211
221
|
fse.mkdirpSync(outdir);
|
|
@@ -219,24 +229,28 @@ function init(input) {
|
|
|
219
229
|
const ctx = getCtx();
|
|
220
230
|
const { database, user, password, host, port, server } = input;
|
|
221
231
|
if (ctx.dialect === "mysql") {
|
|
222
|
-
const
|
|
232
|
+
const connectionOpts = {
|
|
223
233
|
user,
|
|
224
234
|
password,
|
|
225
|
-
host,
|
|
226
|
-
port,
|
|
235
|
+
host: host ?? "localhost",
|
|
236
|
+
port: port ?? 3306,
|
|
227
237
|
database
|
|
228
|
-
}
|
|
238
|
+
};
|
|
239
|
+
const mysql = new MySQL_1.MySQL(connectionOpts);
|
|
229
240
|
ctx.query = mysql.query.bind(mysql);
|
|
241
|
+
ctx.pool = mysql;
|
|
230
242
|
}
|
|
231
243
|
if (ctx.dialect === "postgresql") {
|
|
232
|
-
const
|
|
244
|
+
const connectionOpts = {
|
|
233
245
|
host: host ?? "localhost",
|
|
234
246
|
port: port ?? 5432,
|
|
235
247
|
user,
|
|
236
248
|
password,
|
|
237
249
|
database
|
|
238
|
-
}
|
|
250
|
+
};
|
|
251
|
+
const pool = new pg_1.Pool(connectionOpts);
|
|
239
252
|
ctx.query = (q, values) => pool.query(q, values ?? []).then((r) => r.rows);
|
|
253
|
+
ctx.pool = pool;
|
|
240
254
|
}
|
|
241
255
|
}
|
|
242
256
|
// It's a bit awkward to put __whereNeedsProcessing, __prepareWhere on the class,
|
|
@@ -1624,7 +1638,7 @@ const getRelationInfo = (0, memoize_1.default)(async function getRelationInfo(ta
|
|
|
1624
1638
|
return acc;
|
|
1625
1639
|
}, []);
|
|
1626
1640
|
out = out.concat(relationsManyToMany);
|
|
1627
|
-
out = _.sortBy((x) => x.table, out);
|
|
1641
|
+
out = _.sortBy([(x) => x.table, (x) => x.name], out);
|
|
1628
1642
|
return out;
|
|
1629
1643
|
}, (table) => getCtx().runId + ":" + table);
|
|
1630
1644
|
function getRelationManyToOneFieldName(x) {
|
|
@@ -1786,6 +1800,32 @@ const getPgEnumDefinition = (0, memoize_1.default)(async function getPgEnumDefin
|
|
|
1786
1800
|
const labels = rows.map((r) => String(r.enumlabel).replace(/'/g, "''"));
|
|
1787
1801
|
return "enum('" + labels.join("', '") + "')";
|
|
1788
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);
|
|
1789
1829
|
const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
1790
1830
|
const { dialect, query } = getCtx();
|
|
1791
1831
|
if (dialect === "mysql") {
|
|
@@ -1841,7 +1881,10 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1841
1881
|
Type: type,
|
|
1842
1882
|
Null: c.is_nullable === "YES" ? "YES" : "NO",
|
|
1843
1883
|
Key: keyMap.get(c.Field) ?? "",
|
|
1844
|
-
Default: c.Default ?? ""
|
|
1884
|
+
Default: c.Default ?? "",
|
|
1885
|
+
...(c.data_type === "USER-DEFINED" && c.udt_name != null
|
|
1886
|
+
? { PgType: c.udt_name }
|
|
1887
|
+
: {})
|
|
1845
1888
|
};
|
|
1846
1889
|
});
|
|
1847
1890
|
}
|
|
@@ -1861,7 +1904,21 @@ async function getShowCreateTable(table) {
|
|
|
1861
1904
|
]);
|
|
1862
1905
|
const refByFk = new Map(relations.map((r) => [r.foreignKey, r]));
|
|
1863
1906
|
const columnDefs = tableMeta.map((c) => {
|
|
1864
|
-
|
|
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"}`;
|
|
1865
1922
|
if (c.Default != null && c.Default !== "") {
|
|
1866
1923
|
def += ` DEFAULT ${c.Default}`;
|
|
1867
1924
|
}
|
|
@@ -1875,7 +1932,7 @@ async function getShowCreateTable(table) {
|
|
|
1875
1932
|
}
|
|
1876
1933
|
return def;
|
|
1877
1934
|
});
|
|
1878
|
-
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n)
|
|
1935
|
+
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n);`;
|
|
1879
1936
|
}
|
|
1880
1937
|
return Promise.resolve(null);
|
|
1881
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
|
+
}
|