@technicity/data-service-generator 0.23.0-next.1 → 0.23.0-next.10
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.
|
@@ -9,6 +9,9 @@ type IGenerateInput = {
|
|
|
9
9
|
host?: string;
|
|
10
10
|
port?: number;
|
|
11
11
|
server?: string;
|
|
12
|
+
clientOpts?: {
|
|
13
|
+
[k: string]: any;
|
|
14
|
+
};
|
|
12
15
|
outdir: string;
|
|
13
16
|
tables?: Array<string>;
|
|
14
17
|
excludeTables?: Array<string>;
|
|
@@ -16,6 +19,7 @@ type IGenerateInput = {
|
|
|
16
19
|
includeMappedFields?: IIncludeMappedFields;
|
|
17
20
|
supplementClientOpts?: ISupplementClientOpts;
|
|
18
21
|
outputSqliteSchema?: boolean;
|
|
22
|
+
logLevel?: string;
|
|
19
23
|
};
|
|
20
24
|
export declare function generate(input: IGenerateInput): Promise<void>;
|
|
21
25
|
export {};
|
|
@@ -33,6 +33,7 @@ const os = __importStar(require("node:os"));
|
|
|
33
33
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
34
34
|
const child_process = __importStar(require("node:child_process"));
|
|
35
35
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
36
|
+
const pino_1 = require("pino");
|
|
36
37
|
const prettier = __importStar(require("prettier"));
|
|
37
38
|
const changeCase = __importStar(require("change-case"));
|
|
38
39
|
const fse = __importStar(require("fs-extra"));
|
|
@@ -44,6 +45,7 @@ const isNotNullOrUndefined_1 = require("../lib/isNotNullOrUndefined");
|
|
|
44
45
|
const pg_1 = require("pg");
|
|
45
46
|
const MySQL_1 = require("../runtime/lib/MySQL");
|
|
46
47
|
const capitalizeFirstLetter_1 = require("../lib/capitalizeFirstLetter");
|
|
48
|
+
const pg2sqliteSchema_1 = require("./pg2sqliteSchema");
|
|
47
49
|
const ctxStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
48
50
|
function getCtx() {
|
|
49
51
|
const c = ctxStorage.getStore();
|
|
@@ -62,17 +64,38 @@ async function generate(input) {
|
|
|
62
64
|
if (input.dialect == null) {
|
|
63
65
|
throw new Error("Must specify `dialect`");
|
|
64
66
|
}
|
|
67
|
+
const log = (0, pino_1.pino)({
|
|
68
|
+
name: "generate",
|
|
69
|
+
level: input.logLevel ?? process.env.LOG_LEVEL ?? "info",
|
|
70
|
+
transport: {
|
|
71
|
+
target: "pino-pretty",
|
|
72
|
+
options: {
|
|
73
|
+
colorize: true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
65
77
|
const specialCaseUuidColumn = input.specialCaseUuidColumn ?? true;
|
|
66
78
|
const includeMappedFields = input.includeMappedFields ?? true;
|
|
67
79
|
const supplementClientOpts = input.supplementClientOpts ?? true;
|
|
80
|
+
const runId = node_crypto_1.default.randomUUID();
|
|
68
81
|
const ctx = {
|
|
69
|
-
runId
|
|
82
|
+
runId,
|
|
83
|
+
log: log.child({}),
|
|
70
84
|
dialect: input.dialect,
|
|
71
|
-
query: undefined
|
|
85
|
+
query: undefined,
|
|
86
|
+
pool: undefined
|
|
72
87
|
};
|
|
88
|
+
log.debug({
|
|
89
|
+
runId: ctx.runId,
|
|
90
|
+
dialect: input.dialect,
|
|
91
|
+
database: input.database,
|
|
92
|
+
outdir: input.outdir
|
|
93
|
+
}, "generate() started");
|
|
73
94
|
return ctxStorage.run(ctx, async () => {
|
|
74
95
|
init(input);
|
|
96
|
+
ctx.log.debug("init() completed");
|
|
75
97
|
let tables = await getTableNames();
|
|
98
|
+
ctx.log.debug({ tableCount: tables.length, tables }, "getTableNames() completed");
|
|
76
99
|
if (tables.length === 0) {
|
|
77
100
|
throw new Error("No tables found");
|
|
78
101
|
}
|
|
@@ -82,6 +105,7 @@ async function generate(input) {
|
|
|
82
105
|
if (input.excludeTables != null) {
|
|
83
106
|
tables = tables.filter((x) => !input.excludeTables?.includes(x));
|
|
84
107
|
}
|
|
108
|
+
ctx.log.debug({ tableCount: tables.length, tables }, "tables after filter");
|
|
85
109
|
const data = await Promise.all(tables.flatMap((x) => [
|
|
86
110
|
getGetOneData(x, includeMappedFields),
|
|
87
111
|
getGetListData(x),
|
|
@@ -92,9 +116,12 @@ async function generate(input) {
|
|
|
92
116
|
getDeleteOneData(x),
|
|
93
117
|
getDeleteListData(x)
|
|
94
118
|
]));
|
|
119
|
+
ctx.log.debug({ inputLength: data.length }, "SDK input data collected");
|
|
95
120
|
const artifacts = await getArtifacts(tables, includeMappedFields, specialCaseUuidColumn);
|
|
121
|
+
ctx.log.debug("getArtifacts() completed");
|
|
96
122
|
const artifactsSource = getArtifactsSource(artifacts);
|
|
97
123
|
const sdkSource = await getSDKSource(data, specialCaseUuidColumn, supplementClientOpts, artifacts, input.outputSqliteSchema);
|
|
124
|
+
ctx.log.debug("getSDKSource() completed");
|
|
98
125
|
const sdkFilename = "index.ts";
|
|
99
126
|
const sourceIRuntimeFilePath = fs.existsSync(path.join(__dirname, "../runtime", "IRuntime.ts"))
|
|
100
127
|
? path.join(__dirname, "../runtime", "IRuntime.ts")
|
|
@@ -129,6 +156,7 @@ async function generate(input) {
|
|
|
129
156
|
const tmpDirPath = path.join(os.tmpdir(),
|
|
130
157
|
// _ because - in filename is not supported by mysql2sqlite
|
|
131
158
|
`dsg_${node_crypto_1.default.randomUUID()}`.replace(/-/g, "_"));
|
|
159
|
+
ctx.log.debug({ tmpDirPath }, "writing SDK and artifacts to tmp dir");
|
|
132
160
|
fse.mkdirpSync(tmpDirPath);
|
|
133
161
|
fs.writeFileSync(path.join(tmpDirPath, sdkFilename), sdkSource);
|
|
134
162
|
fs.writeFileSync(path.join(tmpDirPath, artifactsFilename), artifactsSource);
|
|
@@ -174,67 +202,99 @@ async function generate(input) {
|
|
|
174
202
|
fs.writeFileSync(path.join(tmpBuildOutputPath, "IRuntime.d.ts"), fs.existsSync(path.join(__dirname, "../runtime", "IRuntime.d.ts"))
|
|
175
203
|
? fs.readFileSync(path.join(__dirname, "../runtime", "IRuntime.d.ts"), "utf-8")
|
|
176
204
|
: fs.readFileSync(sourceIRuntimeFilePath, "utf-8"));
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
if (input.outputSqliteSchema) {
|
|
206
|
+
let schemaSqlite = null;
|
|
207
|
+
if (ctx.dialect === "mysql") {
|
|
208
|
+
// Since mysql2sqlite outputs a malformed string if a column
|
|
209
|
+
// has the name `enum`, temporarily change the name to something else,
|
|
210
|
+
// then change it back.
|
|
211
|
+
const enumMarker = "`" + node_crypto_1.default.randomUUID() + "`";
|
|
212
|
+
const schemaMySql = Object.values(artifacts)
|
|
213
|
+
.reduce((acc, x) => {
|
|
214
|
+
let d = x.dump?.schema;
|
|
215
|
+
if (!d) {
|
|
216
|
+
return acc;
|
|
217
|
+
}
|
|
218
|
+
d = d.replace(/`enum`/g, enumMarker);
|
|
219
|
+
d += ";";
|
|
220
|
+
acc.push(d);
|
|
186
221
|
return acc;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
}, [])
|
|
223
|
+
.join("\n\n");
|
|
224
|
+
const mysql2SqliteSrc = getMysql2sqliteSrc();
|
|
225
|
+
const mysql2SqlitePath = path.join(tmpDirPath, "mysql2sqlite");
|
|
226
|
+
fs.writeFileSync(mysql2SqlitePath, mysql2SqliteSrc);
|
|
227
|
+
fs.chmodSync(mysql2SqlitePath, 0o755);
|
|
228
|
+
const tmpMySqlSchemaFilename = "tmp.sql";
|
|
229
|
+
const tmpMySqlSchemaPath = path.join(tmpDirPath, tmpMySqlSchemaFilename);
|
|
230
|
+
fs.writeFileSync(tmpMySqlSchemaPath, schemaMySql);
|
|
231
|
+
schemaSqlite = child_process
|
|
232
|
+
.execFileSync(mysql2SqlitePath, [tmpMySqlSchemaFilename], {
|
|
233
|
+
cwd: tmpDirPath
|
|
234
|
+
})
|
|
235
|
+
.toString();
|
|
236
|
+
schemaSqlite = schemaSqlite.replace(new RegExp(enumMarker, "g"), "`enum`");
|
|
237
|
+
}
|
|
238
|
+
else if (ctx.dialect === "postgresql") {
|
|
239
|
+
schemaSqlite = await (0, pg2sqliteSchema_1.convertPgSchemaToSqlite)(ctx.pool);
|
|
240
|
+
}
|
|
241
|
+
if (schemaSqlite) {
|
|
242
|
+
const src = prettier.format(`module.exports = { schema: \`${schemaSqlite.replace(/`/g, "\\`")}\` }`, { parser: "babel" });
|
|
243
|
+
fs.writeFileSync(path.join(tmpBuildOutputPath, "artifacts.sqlite.js"), src);
|
|
244
|
+
}
|
|
207
245
|
}
|
|
208
246
|
if (!fs.existsSync(outdir)) {
|
|
209
247
|
fse.mkdirpSync(outdir);
|
|
210
248
|
}
|
|
211
249
|
fse.emptyDirSync(sdkOutputPath);
|
|
212
250
|
fse.copySync(tmpBuildOutputPath, sdkOutputPath);
|
|
251
|
+
ctx.log.debug({ outdir, sdkOutputPath }, "copy to output dir completed");
|
|
213
252
|
fse.removeSync(tmpDirPath);
|
|
253
|
+
ctx.log.debug("generate() completed");
|
|
214
254
|
});
|
|
215
255
|
}
|
|
216
256
|
function init(input) {
|
|
217
257
|
const ctx = getCtx();
|
|
218
258
|
const { database, user, password, host, port, server } = input;
|
|
219
259
|
if (ctx.dialect === "mysql") {
|
|
220
|
-
const
|
|
260
|
+
const connectionOpts = {
|
|
221
261
|
user,
|
|
222
262
|
password,
|
|
223
|
-
host,
|
|
224
|
-
port,
|
|
225
|
-
database
|
|
226
|
-
|
|
263
|
+
host: host ?? "localhost",
|
|
264
|
+
port: port ?? 3306,
|
|
265
|
+
database,
|
|
266
|
+
...input.clientOpts
|
|
267
|
+
};
|
|
268
|
+
ctx.log.debug({
|
|
269
|
+
dialect: "mysql",
|
|
270
|
+
host: connectionOpts.host,
|
|
271
|
+
port: connectionOpts.port,
|
|
272
|
+
database,
|
|
273
|
+
user
|
|
274
|
+
}, "connecting to MySQL");
|
|
275
|
+
const mysql = new MySQL_1.MySQL(connectionOpts);
|
|
227
276
|
ctx.query = mysql.query.bind(mysql);
|
|
277
|
+
ctx.pool = mysql;
|
|
228
278
|
}
|
|
229
279
|
if (ctx.dialect === "postgresql") {
|
|
230
|
-
const
|
|
280
|
+
const connectionOpts = {
|
|
231
281
|
host: host ?? "localhost",
|
|
232
282
|
port: port ?? 5432,
|
|
233
283
|
user,
|
|
234
284
|
password,
|
|
235
|
-
database
|
|
236
|
-
|
|
285
|
+
database,
|
|
286
|
+
...input.clientOpts
|
|
287
|
+
};
|
|
288
|
+
ctx.log.debug({
|
|
289
|
+
dialect: "postgresql",
|
|
290
|
+
host: connectionOpts.host,
|
|
291
|
+
port: connectionOpts.port,
|
|
292
|
+
database,
|
|
293
|
+
user
|
|
294
|
+
}, "connecting to PostgreSQL");
|
|
295
|
+
const pool = new pg_1.Pool(connectionOpts);
|
|
237
296
|
ctx.query = (q, values) => pool.query(q, values ?? []).then((r) => r.rows);
|
|
297
|
+
ctx.pool = pool;
|
|
238
298
|
}
|
|
239
299
|
}
|
|
240
300
|
// It's a bit awkward to put __whereNeedsProcessing, __prepareWhere on the class,
|
|
@@ -278,7 +338,7 @@ async function getSDKSource(input, specialCaseUuidColumn, supplementClientOpts,
|
|
|
278
338
|
runtime: any;
|
|
279
339
|
clientOpts: { [k: string]: any; },
|
|
280
340
|
otherOpts?: { [k: string]: any; },
|
|
281
|
-
passBeforeValueToAfterCallback
|
|
341
|
+
passBeforeValueToAfterCallback?: boolean,
|
|
282
342
|
}) {
|
|
283
343
|
let otherOpts = opts.otherOpts ?? {};
|
|
284
344
|
if (opts.clientOpts.filename === ":memory:") {
|
|
@@ -291,7 +351,7 @@ async function getSDKSource(input, specialCaseUuidColumn, supplementClientOpts,
|
|
|
291
351
|
: "otherOpts"}, artifacts);
|
|
292
352
|
this.onHandlerMap = new Map();
|
|
293
353
|
this.eventTarget = new EventTarget();
|
|
294
|
-
this.passBeforeValueToAfterCallback = opts.passBeforeValueToAfterCallback;
|
|
354
|
+
this.passBeforeValueToAfterCallback = opts.passBeforeValueToAfterCallback ?? false;
|
|
295
355
|
}
|
|
296
356
|
|
|
297
357
|
$use(middleware: TMiddleware) {
|
|
@@ -621,6 +681,7 @@ function getMethodSourceOnHandlerPostOne(x) {
|
|
|
621
681
|
return `on${(0, capitalizeFirstLetter_1.capitalizeFirstLetter)(x.methodName)}(handler:
|
|
622
682
|
(sdk: InstanceType<typeof SDK>, input: { data: ${x.typeDataName} },
|
|
623
683
|
output: Partial<${getTypeReturnName(x.table)}>,
|
|
684
|
+
before: null,
|
|
624
685
|
context: TContext,
|
|
625
686
|
) => Promise<void>
|
|
626
687
|
): void {
|
|
@@ -735,6 +796,7 @@ function getMethodSourceOnHandlerDeleteOne(x, findOnes) {
|
|
|
735
796
|
.map((findOne) => `{ ${findOne.name}: ${findOne.type}${findOne.nullable ? " | null" : ""} }`)
|
|
736
797
|
.join(" | ")}, },
|
|
737
798
|
output: void,
|
|
799
|
+
before: ${getTypeReturnName(x.table)} | null,
|
|
738
800
|
context: TContext,
|
|
739
801
|
) => Promise<void>
|
|
740
802
|
): void {
|
|
@@ -1444,6 +1506,7 @@ function getArtifactsSource(artifacts) {
|
|
|
1444
1506
|
return prettier.format(src, { parser: "typescript" });
|
|
1445
1507
|
}
|
|
1446
1508
|
async function getArtifacts(tables, includeMappedFields, specialCaseUuidColumn) {
|
|
1509
|
+
const ctx = getCtx();
|
|
1447
1510
|
const tableMetaList = await Promise.all(tables.map(async (table) => {
|
|
1448
1511
|
const [tableMeta, primaryKey, dumpSchema] = await Promise.all([
|
|
1449
1512
|
getTableMeta(table),
|
|
@@ -1509,7 +1572,7 @@ async function getArtifacts(tables, includeMappedFields, specialCaseUuidColumn)
|
|
|
1509
1572
|
}
|
|
1510
1573
|
return {
|
|
1511
1574
|
kind: "scalar",
|
|
1512
|
-
type: getBaseJSONType(t.Type,
|
|
1575
|
+
type: getBaseJSONType(t.Type, ctx.dialect),
|
|
1513
1576
|
name: t.Field,
|
|
1514
1577
|
nullable,
|
|
1515
1578
|
hasDefaultValue: !!t.Default
|
|
@@ -1622,14 +1685,14 @@ const getRelationInfo = (0, memoize_1.default)(async function getRelationInfo(ta
|
|
|
1622
1685
|
return acc;
|
|
1623
1686
|
}, []);
|
|
1624
1687
|
out = out.concat(relationsManyToMany);
|
|
1625
|
-
out = _.sortBy((x) => x.table, out);
|
|
1688
|
+
out = _.sortBy([(x) => x.table, (x) => x.name], out);
|
|
1626
1689
|
return out;
|
|
1627
1690
|
}, (table) => getCtx().runId + ":" + table);
|
|
1628
1691
|
function getRelationManyToOneFieldName(x) {
|
|
1629
1692
|
return changeCase.camelCase(x.foreignKey.replace(new RegExp(x.referencedKey + "$", "i"), ""));
|
|
1630
1693
|
}
|
|
1631
1694
|
// TODO: not sure if this logic is correct
|
|
1632
|
-
async function getJunctionTables() {
|
|
1695
|
+
const getJunctionTables = (0, memoize_1.default)(async function getJunctionTables() {
|
|
1633
1696
|
const tables = await getTableNames();
|
|
1634
1697
|
return (await Promise.all(tables.map(async (table) => {
|
|
1635
1698
|
const relations = await getRelationsManyToOne(table);
|
|
@@ -1644,7 +1707,7 @@ async function getJunctionTables() {
|
|
|
1644
1707
|
}
|
|
1645
1708
|
return null;
|
|
1646
1709
|
}))).filter(isNotNullOrUndefined_1.isNotNullOrUndefined);
|
|
1647
|
-
}
|
|
1710
|
+
}, () => getCtx().runId);
|
|
1648
1711
|
// `from` relations
|
|
1649
1712
|
// https://stackoverflow.com/a/54732547
|
|
1650
1713
|
const getRelationsManyToOne = (0, memoize_1.default)(async function getRelationsManyToOne(table) {
|
|
@@ -1674,7 +1737,8 @@ const getRelationsManyToOne = (0, memoize_1.default)(async function getRelations
|
|
|
1674
1737
|
FROM information_schema.key_column_usage kcu
|
|
1675
1738
|
JOIN information_schema.referential_constraints rc ON kcu.constraint_name = rc.constraint_name AND kcu.table_schema = rc.constraint_schema
|
|
1676
1739
|
JOIN information_schema.constraint_column_usage ccu ON rc.unique_constraint_name = ccu.constraint_name AND rc.unique_constraint_schema = ccu.table_schema
|
|
1677
|
-
WHERE kcu.table_schema = 'public' AND kcu.table_name = $1
|
|
1740
|
+
WHERE kcu.table_schema = 'public' AND kcu.table_name = $1
|
|
1741
|
+
ORDER BY ccu.table_name, ccu.column_name`, [table]);
|
|
1678
1742
|
}
|
|
1679
1743
|
else {
|
|
1680
1744
|
throw new Error("Unsupported dialect: " + dialect);
|
|
@@ -1688,7 +1752,7 @@ const getRelationsManyToOne = (0, memoize_1.default)(async function getRelations
|
|
|
1688
1752
|
nullable: tableMeta.find((m) => m.Field === v.t1Field)?.Null === "YES"
|
|
1689
1753
|
};
|
|
1690
1754
|
})));
|
|
1691
|
-
return _.sortBy((x) => x.referencedTable, xs);
|
|
1755
|
+
return _.sortBy([(x) => x.referencedTable, (x) => x.referencedKey, (x) => x.foreignKey], xs);
|
|
1692
1756
|
}, (table) => getCtx().runId + ":" + table);
|
|
1693
1757
|
// `to` relations
|
|
1694
1758
|
const getRelationsOneToMany = (0, memoize_1.default)(async function getRelationsOneToMany(table) {
|
|
@@ -1717,7 +1781,8 @@ const getRelationsOneToMany = (0, memoize_1.default)(async function getRelations
|
|
|
1717
1781
|
FROM information_schema.key_column_usage kcu
|
|
1718
1782
|
JOIN information_schema.referential_constraints rc ON kcu.constraint_name = rc.constraint_name AND kcu.table_schema = rc.constraint_schema
|
|
1719
1783
|
JOIN information_schema.constraint_column_usage ccu ON rc.unique_constraint_name = ccu.constraint_name AND rc.unique_constraint_schema = ccu.table_schema
|
|
1720
|
-
WHERE kcu.table_schema = 'public' AND ccu.table_name = $1
|
|
1784
|
+
WHERE kcu.table_schema = 'public' AND ccu.table_name = $1
|
|
1785
|
+
ORDER BY kcu.table_name, kcu.column_name`, [table]);
|
|
1721
1786
|
}
|
|
1722
1787
|
else {
|
|
1723
1788
|
throw new Error("Unsupported dialect: " + dialect);
|
|
@@ -1731,7 +1796,7 @@ const getRelationsOneToMany = (0, memoize_1.default)(async function getRelations
|
|
|
1731
1796
|
nullable: false
|
|
1732
1797
|
};
|
|
1733
1798
|
})));
|
|
1734
|
-
return _.sortBy((x) => x.referencedKey,
|
|
1799
|
+
return _.sortBy([(x) => x.referencedTable, (x) => x.referencedKey, (x) => x.foreignKey], xs);
|
|
1735
1800
|
}, (table) => getCtx().runId + ":" + table);
|
|
1736
1801
|
async function getPrimaryColumn(table) {
|
|
1737
1802
|
const tableMeta = await getTableMeta(table);
|
|
@@ -1770,27 +1835,29 @@ async function getUuidColumn(table) {
|
|
|
1770
1835
|
nullable: column.Null === "YES"
|
|
1771
1836
|
};
|
|
1772
1837
|
}
|
|
1773
|
-
const getPgEnumDefinition = (0, memoize_1.default)(async function getPgEnumDefinition(udtName) {
|
|
1838
|
+
const getPgEnumDefinition = (0, memoize_1.default)(async function getPgEnumDefinition(udtSchema, udtName) {
|
|
1774
1839
|
const { dialect, query } = getCtx();
|
|
1775
1840
|
if (dialect !== "postgresql")
|
|
1776
1841
|
return null;
|
|
1777
1842
|
const rows = await query(`SELECT e.enumlabel FROM pg_enum e
|
|
1778
1843
|
JOIN pg_type t ON e.enumtypid = t.oid
|
|
1779
1844
|
JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
|
|
1780
|
-
WHERE t.typname = $
|
|
1781
|
-
ORDER BY e.enumsortorder`, [udtName]);
|
|
1845
|
+
WHERE t.typname = $2 AND n.nspname = $1
|
|
1846
|
+
ORDER BY e.enumsortorder`, [udtSchema, udtName]);
|
|
1782
1847
|
if (rows.length === 0)
|
|
1783
1848
|
return null;
|
|
1784
1849
|
const labels = rows.map((r) => String(r.enumlabel).replace(/'/g, "''"));
|
|
1785
1850
|
return "enum('" + labels.join("', '") + "')";
|
|
1786
|
-
}, (udtName) => getCtx().runId + ":" + udtName);
|
|
1851
|
+
}, (udtSchema, udtName) => getCtx().runId + ":" + udtSchema + ":" + udtName);
|
|
1787
1852
|
const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
1788
|
-
const
|
|
1853
|
+
const ctx = getCtx();
|
|
1854
|
+
const { dialect, query } = ctx;
|
|
1855
|
+
ctx.log.debug({ table }, "getTableMeta() fetching");
|
|
1789
1856
|
if (dialect === "mysql") {
|
|
1790
1857
|
return query("DESCRIBE ??", [table]).then((xs) => _.sortBy((x) => x.Field, xs));
|
|
1791
1858
|
}
|
|
1792
1859
|
if (dialect === "postgresql") {
|
|
1793
|
-
const columns = await query(`SELECT column_name AS "Field", data_type, udt_name, character_maximum_length AS char_max, is_nullable, column_default AS "Default"
|
|
1860
|
+
const columns = await query(`SELECT column_name AS "Field", data_type, udt_schema, udt_name, character_maximum_length AS char_max, is_nullable, column_default AS "Default"
|
|
1794
1861
|
FROM information_schema.columns
|
|
1795
1862
|
WHERE table_schema = 'public' AND table_name = $1
|
|
1796
1863
|
ORDER BY ordinal_position`, [table]);
|
|
@@ -1815,17 +1882,26 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1815
1882
|
if (!keyMap.has(k.col) || k.key_type === "PRI")
|
|
1816
1883
|
keyMap.set(k.col, k.key_type);
|
|
1817
1884
|
}
|
|
1818
|
-
const
|
|
1819
|
-
...new
|
|
1820
|
-
.filter((c) => c.data_type === "USER-DEFINED" &&
|
|
1821
|
-
.
|
|
1885
|
+
const udtKeys = [
|
|
1886
|
+
...new Map(columns
|
|
1887
|
+
.filter((c) => c.data_type === "USER-DEFINED" &&
|
|
1888
|
+
c.udt_schema != null &&
|
|
1889
|
+
c.udt_name != null)
|
|
1890
|
+
.map((c) => [`${c.udt_schema}.${c.udt_name}`, c])).keys()
|
|
1822
1891
|
];
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1892
|
+
const udtPairs = udtKeys.map((k) => {
|
|
1893
|
+
const [s, n] = k.split(".", 2);
|
|
1894
|
+
return [s, n];
|
|
1895
|
+
});
|
|
1896
|
+
const enumDefs = await Promise.all(udtPairs.map(([schema, name]) => getPgEnumDefinition(schema, name)));
|
|
1897
|
+
const enumMap = new Map(udtKeys.map((k, i) => [k, enumDefs[i] ?? "varchar(255)"]));
|
|
1898
|
+
const cols = columns.map((c) => {
|
|
1826
1899
|
let type;
|
|
1827
|
-
if (c.data_type === "USER-DEFINED" &&
|
|
1828
|
-
|
|
1900
|
+
if (c.data_type === "USER-DEFINED" &&
|
|
1901
|
+
c.udt_schema != null &&
|
|
1902
|
+
c.udt_name != null) {
|
|
1903
|
+
const enumKey = `${c.udt_schema}.${c.udt_name}`;
|
|
1904
|
+
type = enumMap.get(enumKey) ?? "character varying(255)";
|
|
1829
1905
|
}
|
|
1830
1906
|
else {
|
|
1831
1907
|
type = c.data_type;
|
|
@@ -1839,9 +1915,18 @@ const getTableMeta = (0, memoize_1.default)(async function getTableMeta(table) {
|
|
|
1839
1915
|
Type: type,
|
|
1840
1916
|
Null: c.is_nullable === "YES" ? "YES" : "NO",
|
|
1841
1917
|
Key: keyMap.get(c.Field) ?? "",
|
|
1842
|
-
|
|
1918
|
+
// Preserve `null` when there is no default so that
|
|
1919
|
+
// required-field detection (via `hasDefault`) works
|
|
1920
|
+
// consistently with the MySQL `DESCRIBE` output.
|
|
1921
|
+
Default: c.Default,
|
|
1922
|
+
...(c.data_type === "USER-DEFINED" &&
|
|
1923
|
+
c.udt_schema != null &&
|
|
1924
|
+
c.udt_name != null
|
|
1925
|
+
? { PgType: c.udt_name }
|
|
1926
|
+
: {})
|
|
1843
1927
|
};
|
|
1844
1928
|
});
|
|
1929
|
+
return _.sortBy((c) => c.Field, cols);
|
|
1845
1930
|
}
|
|
1846
1931
|
throw new Error("Unsupported dialect: " + dialect);
|
|
1847
1932
|
}, (table) => getCtx().runId + ":" + table);
|
|
@@ -1859,7 +1944,21 @@ async function getShowCreateTable(table) {
|
|
|
1859
1944
|
]);
|
|
1860
1945
|
const refByFk = new Map(relations.map((r) => [r.foreignKey, r]));
|
|
1861
1946
|
const columnDefs = tableMeta.map((c) => {
|
|
1862
|
-
|
|
1947
|
+
const isSerialPk = c.Key === "PRI" &&
|
|
1948
|
+
c.Default != null &&
|
|
1949
|
+
c.Default !== "" &&
|
|
1950
|
+
/nextval\s*\(/i.test(c.Default);
|
|
1951
|
+
if (isSerialPk) {
|
|
1952
|
+
const baseType = (c.PgType ?? c.Type).toLowerCase();
|
|
1953
|
+
const serialType = baseType === "bigint" || baseType === "int8"
|
|
1954
|
+
? "BIGSERIAL"
|
|
1955
|
+
: baseType === "smallint" || baseType === "int2"
|
|
1956
|
+
? "SMALLSERIAL"
|
|
1957
|
+
: "SERIAL";
|
|
1958
|
+
return `"${c.Field.replace(/"/g, '""')}" ${serialType} PRIMARY KEY`;
|
|
1959
|
+
}
|
|
1960
|
+
const pgType = c.PgType ?? c.Type;
|
|
1961
|
+
let def = `"${c.Field.replace(/"/g, '""')}" ${pgType} ${c.Null === "YES" ? "NULL" : "NOT NULL"}`;
|
|
1863
1962
|
if (c.Default != null && c.Default !== "") {
|
|
1864
1963
|
def += ` DEFAULT ${c.Default}`;
|
|
1865
1964
|
}
|
|
@@ -1873,7 +1972,7 @@ async function getShowCreateTable(table) {
|
|
|
1873
1972
|
}
|
|
1874
1973
|
return def;
|
|
1875
1974
|
});
|
|
1876
|
-
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n)
|
|
1975
|
+
return `CREATE TABLE "${table.replace(/"/g, '""')}" (\n ${columnDefs.join(",\n ")}\n);`;
|
|
1877
1976
|
}
|
|
1878
1977
|
return Promise.resolve(null);
|
|
1879
1978
|
}
|
|
@@ -2043,18 +2142,18 @@ function getPropertyFormat(sqlType) {
|
|
|
2043
2142
|
}
|
|
2044
2143
|
return undefined;
|
|
2045
2144
|
}
|
|
2046
|
-
async function getTableNames() {
|
|
2145
|
+
const getTableNames = (0, memoize_1.default)(async function getTableNames() {
|
|
2047
2146
|
const { dialect, query } = getCtx();
|
|
2048
2147
|
if (dialect === "mysql") {
|
|
2049
2148
|
return query("SHOW TABLES").then((xs) => xs.flatMap((x) => Object.values(x)).sort());
|
|
2050
2149
|
}
|
|
2051
2150
|
if (dialect === "postgresql") {
|
|
2052
2151
|
return query(`SELECT table_name FROM information_schema.tables
|
|
2053
|
-
|
|
2054
|
-
|
|
2152
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
2153
|
+
ORDER BY table_name`).then((rows) => rows.map((r) => r.table_name));
|
|
2055
2154
|
}
|
|
2056
2155
|
throw new Error("Unsupported dialect: " + dialect);
|
|
2057
|
-
}
|
|
2156
|
+
}, () => getCtx().runId);
|
|
2058
2157
|
function getMysql2sqliteSrc() {
|
|
2059
2158
|
return `#!/usr/bin/awk -f
|
|
2060
2159
|
|
|
@@ -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
|
+
}
|
|
@@ -70,7 +70,9 @@ async function resolve(input, dbCall, formatQuery, beginTransaction, dialect, mi
|
|
|
70
70
|
const shouldRunOnHandler = typeof onHandler === "function" &&
|
|
71
71
|
asyncLocalStorage != null &&
|
|
72
72
|
asyncLocalStorage?.getStore()?.isInOnHandler !== true;
|
|
73
|
-
const beforeValue = shouldRunOnHandler &&
|
|
73
|
+
const beforeValue = shouldRunOnHandler &&
|
|
74
|
+
input.passBeforeValueToAfterCallback &&
|
|
75
|
+
input.action !== "create"
|
|
74
76
|
? await _resolve({
|
|
75
77
|
...paramsChanged,
|
|
76
78
|
action: paramsChanged.action === "updateMany" ||
|
|
@@ -103,7 +105,9 @@ async function resolve(input, dbCall, formatQuery, beginTransaction, dialect, mi
|
|
|
103
105
|
const shouldRunOnHandler = typeof onHandler === "function" &&
|
|
104
106
|
asyncLocalStorage != null &&
|
|
105
107
|
asyncLocalStorage?.getStore()?.isInOnHandler !== true;
|
|
106
|
-
const beforeValue = shouldRunOnHandler &&
|
|
108
|
+
const beforeValue = shouldRunOnHandler &&
|
|
109
|
+
input.passBeforeValueToAfterCallback &&
|
|
110
|
+
input.action !== "create"
|
|
107
111
|
? await _resolve({
|
|
108
112
|
...input,
|
|
109
113
|
action: input.action === "updateMany" || input.action === "deleteMany"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technicity/data-service-generator",
|
|
3
|
-
"version": "0.23.0-next.
|
|
3
|
+
"version": "0.23.0-next.10",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"mysql2": "^3.10.1",
|
|
28
28
|
"pg": "^8.13.1",
|
|
29
29
|
"pg-format": "^1.0.4",
|
|
30
|
+
"pino": "^9.5.0",
|
|
31
|
+
"pino-pretty": "^13.1.3",
|
|
30
32
|
"prettier": "^2.1.2",
|
|
31
33
|
"sqlstring": "^2.3.2",
|
|
32
34
|
"sqlstring-sqlite": "^0.1.1",
|