@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 (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) {
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
- 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], {
203
- cwd: tmpDirPath
204
- })
205
- .toString();
206
- schemaSqlite = schemaSqlite.replace(new RegExp(enumMarker, "g"), "`enum`");
207
- const src = prettier.format(`module.exports = { schema: \`${schemaSqlite.replace(/`/g, "\\`")}\` }`, { parser: "babel" });
208
- fs.writeFileSync(path.join(tmpBuildOutputPath, "artifacts.sqlite.js"), src);
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 mysql = new MySQL_1.MySQL({
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 pool = new pg_1.Pool({
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
- let def = `"${c.Field.replace(/"/g, '""')}" ${c.Type} ${c.Null === "YES" ? "NULL" : "NOT NULL"}`;
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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technicity/data-service-generator",
3
- "version": "0.23.0-next.2",
3
+ "version": "0.23.0-next.3",
4
4
  "main": "./dist/index.js",
5
5
  "files": [
6
6
  "dist"