@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: node_crypto_1.default.randomUUID(),
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 (getCtx().dialect === "mysql" && input.outputSqliteSchema) {
178
- // Since mysql2sqlite outputs a malformed string if a column
179
- // has the name `enum`, temporarily change the name to something else,
180
- // then change it back.
181
- const enumMarker = "`" + node_crypto_1.default.randomUUID() + "`";
182
- const schemaMySql = Object.values(artifacts)
183
- .reduce((acc, x) => {
184
- let d = x.dump?.schema;
185
- if (!d) {
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
- d = d.replace(/`enum`/g, enumMarker);
189
- d += ";";
190
- acc.push(d);
191
- return acc;
192
- }, [])
193
- .join("\n\n");
194
- const mysql2SqliteSrc = getMysql2sqliteSrc();
195
- const mysql2SqlitePath = path.join(tmpDirPath, "mysql2sqlite");
196
- fs.writeFileSync(mysql2SqlitePath, mysql2SqliteSrc);
197
- fs.chmodSync(mysql2SqlitePath, 0o755);
198
- const tmpMySqlSchemaFilename = "tmp.sql";
199
- const tmpMySqlSchemaPath = path.join(tmpDirPath, tmpMySqlSchemaFilename);
200
- fs.writeFileSync(tmpMySqlSchemaPath, schemaMySql);
201
- let schemaSqlite = child_process
202
- .execFileSync(mysql2SqlitePath, [tmpMySqlSchemaFilename], { cwd: tmpDirPath })
203
- .toString();
204
- schemaSqlite = schemaSqlite.replace(new RegExp(enumMarker, "g"), "`enum`");
205
- const src = prettier.format(`module.exports = { schema: \`${schemaSqlite.replace(/`/g, "\\`")}\` }`, { parser: "babel" });
206
- fs.writeFileSync(path.join(tmpBuildOutputPath, "artifacts.sqlite.js"), src);
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 mysql = new MySQL_1.MySQL({
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 pool = new pg_1.Pool({
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: boolean,
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, getCtx().dialect),
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`, [table]);
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`, [table]);
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, _.sortBy((x) => x.referencedTable, xs));
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 = $1 AND n.nspname = 'public'
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 { dialect, query } = getCtx();
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 udtNames = [
1819
- ...new Set(columns
1820
- .filter((c) => c.data_type === "USER-DEFINED" && c.udt_name != null)
1821
- .map((c) => c.udt_name))
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 enumDefs = await Promise.all(udtNames.map((udt) => getPgEnumDefinition(udt)));
1824
- const enumMap = new Map(udtNames.map((udt, i) => [udt, enumDefs[i] ?? "varchar(255)"]));
1825
- return columns.map((c) => {
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" && c.udt_name != null) {
1828
- type = enumMap.get(c.udt_name) ?? "character varying(255)";
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
- Default: c.Default ?? ""
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
- let def = `"${c.Field.replace(/"/g, '""')}" ${c.Type} ${c.Null === "YES" ? "NULL" : "NOT NULL"}`;
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
- WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
2054
- ORDER BY table_name`).then((rows) => rows.map((r) => r.table_name));
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,6 @@
1
+ import { Pool } from "pg";
2
+ /**
3
+ * Reflects the PostgreSQL schema from the given connection and returns
4
+ * SQLite-compatible CREATE TABLE statements as a string.
5
+ */
6
+ export declare function convertPgSchemaToSqlite(pool: Pool): Promise<string>;
@@ -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 && input.passBeforeValueToAfterCallback
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 && input.passBeforeValueToAfterCallback
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.1",
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",