@technicity/data-service-generator 0.23.0-next.2 → 0.23.0-next.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.
|
@@ -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) {
|
|
@@ -1824,7 +1838,7 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1824
1838
|
];
|
|
1825
1839
|
const enumDefs = await Promise.all(udtNames.map((udt) => getPgEnumDefinition(udt)));
|
|
1826
1840
|
const enumMap = new Map(udtNames.map((udt, i) => [udt, enumDefs[i] ?? "varchar(255)"]));
|
|
1827
|
-
|
|
1841
|
+
const cols = columns.map((c) => {
|
|
1828
1842
|
let type;
|
|
1829
1843
|
if (c.data_type === "USER-DEFINED" && c.udt_name != null) {
|
|
1830
1844
|
type = enumMap.get(c.udt_name) ?? "character varying(255)";
|
|
@@ -1841,9 +1855,13 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1841
1855
|
Type: type,
|
|
1842
1856
|
Null: c.is_nullable === "YES" ? "YES" : "NO",
|
|
1843
1857
|
Key: keyMap.get(c.Field) ?? "",
|
|
1844
|
-
Default: c.Default ?? ""
|
|
1858
|
+
Default: c.Default ?? "",
|
|
1859
|
+
...(c.data_type === "USER-DEFINED" && c.udt_name != null
|
|
1860
|
+
? { PgType: c.udt_name }
|
|
1861
|
+
: {})
|
|
1845
1862
|
};
|
|
1846
1863
|
});
|
|
1864
|
+
return _.sortBy((c) => c.Field, cols);
|
|
1847
1865
|
}
|
|
1848
1866
|
throw new Error("Unsupported dialect: " + dialect);
|
|
1849
1867
|
}, (table) => getCtx().runId + ":" + table);
|
|
@@ -1861,7 +1879,21 @@ async function getShowCreateTable(table) {
|
|
|
1861
1879
|
]);
|
|
1862
1880
|
const refByFk = new Map(relations.map((r) => [r.foreignKey, r]));
|
|
1863
1881
|
const columnDefs = tableMeta.map((c) => {
|
|
1864
|
-
|
|
1882
|
+
const isSerialPk = c.Key === "PRI" &&
|
|
1883
|
+
c.Default != null &&
|
|
1884
|
+
c.Default !== "" &&
|
|
1885
|
+
/nextval\s*\(/i.test(c.Default);
|
|
1886
|
+
if (isSerialPk) {
|
|
1887
|
+
const baseType = (c.PgType ?? c.Type).toLowerCase();
|
|
1888
|
+
const serialType = baseType === "bigint" || baseType === "int8"
|
|
1889
|
+
? "BIGSERIAL"
|
|
1890
|
+
: baseType === "smallint" || baseType === "int2"
|
|
1891
|
+
? "SMALLSERIAL"
|
|
1892
|
+
: "SERIAL";
|
|
1893
|
+
return `"${c.Field.replace(/"/g, '""')}" ${serialType} PRIMARY KEY`;
|
|
1894
|
+
}
|
|
1895
|
+
const pgType = c.PgType ?? c.Type;
|
|
1896
|
+
let def = `"${c.Field.replace(/"/g, '""')}" ${pgType} ${c.Null === "YES" ? "NULL" : "NOT NULL"}`;
|
|
1865
1897
|
if (c.Default != null && c.Default !== "") {
|
|
1866
1898
|
def += ` DEFAULT ${c.Default}`;
|
|
1867
1899
|
}
|
|
@@ -1875,7 +1907,7 @@ async function getShowCreateTable(table) {
|
|
|
1875
1907
|
}
|
|
1876
1908
|
return def;
|
|
1877
1909
|
});
|
|
1878
|
-
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n)
|
|
1910
|
+
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n);`;
|
|
1879
1911
|
}
|
|
1880
1912
|
return Promise.resolve(null);
|
|
1881
1913
|
}
|
|
@@ -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
|
+
}
|