draw2sql 1.0.0-beta.1

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.

Potentially problematic release.


This version of draw2sql might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/README.md +131 -0
  2. package/dist/draw2sql.js +985 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # draw2sql
2
+
3
+ [![npm](https://img.shields.io/npm/v/draw2sql)](https://www.npmjs.com/package/draw2sql)
4
+
5
+ Generate SQL DDL from a draw.io XML ER diagram.
6
+
7
+ ## Dependencies
8
+
9
+ **Runtime:** Node.js 18 or later. No other dependencies — the published package is a single compiled JavaScript file.
10
+
11
+ **To build from source:** Node.js 18+, TypeScript 5+, ts-node 10+.
12
+
13
+ ## Installation
14
+
15
+ ### Run without installing (recommended)
16
+
17
+ No install needed. `npx` downloads and runs draw2sql on the fly:
18
+
19
+ ```powershell
20
+ npx draw2sql --input schema.drawio --dialect postgres
21
+ ```
22
+
23
+ ### Install as a project dev dependency
24
+
25
+ Add draw2sql to a project's dev dependencies so it's available via `npx` without downloading each time:
26
+
27
+ ```powershell
28
+ npm install --save-dev draw2sql
29
+ npx draw2sql --input schema.drawio --dialect postgres
30
+ ```
31
+
32
+ Or add a script to your project's `package.json` and skip `npx` entirely:
33
+
34
+ ```json
35
+ "scripts": {
36
+ "generate-sql": "draw2sql --input schema.drawio --dialect postgres"
37
+ }
38
+ ```
39
+
40
+ ```powershell
41
+ npm run generate-sql
42
+ ```
43
+
44
+ `npm run` automatically looks in `node_modules/.bin`, so no `npx` is needed in scripts.
45
+
46
+ ### Install globally
47
+
48
+ Install once and run as a plain command from anywhere:
49
+
50
+ ```powershell
51
+ npm install -g draw2sql
52
+ draw2sql --input schema.drawio --dialect postgres
53
+ ```
54
+
55
+ To uninstall: `npm uninstall -g draw2sql`
56
+
57
+ ## Usage
58
+
59
+ If `--output` is omitted, the output file is derived from the input filename with a `.<dialect>.sql` extension:
60
+
61
+ ```powershell
62
+ npx draw2sql --input schema.drawio --dialect postgres
63
+ # writes schema.postgres.sql
64
+ ```
65
+
66
+ If `--output` already exists, draw2sql fails by default to prevent accidental overwrite. Use `--overwrite` (or `-f`) to replace it:
67
+
68
+ ```powershell
69
+ npx draw2sql --input schema.drawio --dialect postgres --overwrite
70
+ ```
71
+
72
+ ### Naming cases
73
+
74
+ By default, draw2sql uses `db-default` naming:
75
+
76
+ | SQL Dialect | Convention | Case | Traditional Identifier Quoting | If Unquoted |
77
+ | --- | --- | --- | --- | --- |
78
+ | `postgres` | `snake_case` | `snake` | `"name"` | Folded to lowercase (`MyTable` -> `mytable`); names with spaces fail unless quoted |
79
+ | `mariadb` / `mysql` | `snake_case` | `snake` | `` `name` `` | Works for simple names; reserved words/special chars fail; spaces require quoting |
80
+ | `sqlite` | `snake_case` | `snake` | `"name"` | Usually case-insensitive matching; names with spaces require quoting |
81
+ | `sqlserver` | `PascalCase` | `pascal` | `[name]` | Works for regular names; reserved words/special chars fail; spaces require quoting |
82
+ | `oracle` | `SCREAMING_SNAKE_CASE` | `screaming_snake` | `"NAME"` | Folded to uppercase (`mytable` -> `MYTABLE`); names with spaces fail unless quoted |
83
+
84
+ Override with:
85
+
86
+ ```powershell
87
+ npx draw2sql -i schema.drawio -d postgres --table-case snake --field-case snake
88
+ ```
89
+
90
+ Supported cases:
91
+ - `as-drawn` (no transformation)
92
+ - `db-default` (default; depends on `--dialect`)
93
+ - `pascal`
94
+ - `camel`
95
+ - `snake`
96
+ - `screaming_snake`
97
+ - `kebab`
98
+
99
+ Supported dialects:
100
+ - `postgres`
101
+ - `mariadb` (also accepts `mysql`)
102
+ - `sqlserver`
103
+ - `sqlite`
104
+ - `oracle`
105
+
106
+ ## Diagram parameter block
107
+
108
+ You can add a text block in draw.io that includes parameters. Example:
109
+
110
+ ```text
111
+ draw2sql
112
+ dialect = oracle
113
+ schema = FILEOMATIC
114
+ ```
115
+
116
+ When present, recognized keys are captured and included in SQL output comments.
117
+ - `dialect` (also `sqldialect`, `flavor`, `sqlFlavor`) overrides the CLI dialect.
118
+ - `schema` qualifies generated table names (e.g. `"myschema"."users"`). If omitted, table names are unqualified and the database will use its session default (`public` for postgres, `dbo` for sqlserver, the connected user's schema for oracle, etc.). Ignored for `sqlite`, which has no schema concept.
119
+
120
+ ## Output strategy
121
+
122
+ Generated SQL is idempotent-oriented and includes:
123
+ 1. create-table pass
124
+ 2. add-missing-columns pass
125
+ 3. inferred foreign-key pass
126
+
127
+ This generator infers column types/keys from draw.io table shapes and labels (for example `PK`, `FK`, `Name: varchar(120)`, `ProviderId (FK)`).
128
+
129
+ ## Notes on Oracle
130
+
131
+ Oracle output uses PL/SQL blocks for create/alter operations with existence checks so scripts can be rerun safely.
@@ -0,0 +1,985 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SqlTypeMapper = exports.DrawIoDiagramParser = exports.CliParser = exports.NameStyler = exports.NameStyleResolver = exports.DialectResolver = void 0;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const DEFAULT_REF_COLUMN = "Id";
11
+ const MARKER_WORDS = new Set(["PK", "FK", "NN", "NOT NULL", "UNIQUE", "UQ"]);
12
+ class DialectResolver {
13
+ static normalize(input) {
14
+ const normalized = input.trim().toLowerCase();
15
+ if (normalized === "postgres" || normalized === "postgresql")
16
+ return "postgres";
17
+ if (normalized === "mariadb" || normalized === "mysql")
18
+ return "mariadb";
19
+ if (normalized === "sqlserver" || normalized === "mssql")
20
+ return "sqlserver";
21
+ if (normalized === "sqlite" || normalized === "sqlite3")
22
+ return "sqlite";
23
+ if (normalized === "oracle" || normalized === "oracledb")
24
+ return "oracle";
25
+ return null;
26
+ }
27
+ }
28
+ exports.DialectResolver = DialectResolver;
29
+ class NameStyleResolver {
30
+ static normalize(input) {
31
+ const normalized = input.trim().toLowerCase();
32
+ if (normalized === "as-drawn" || normalized === "asdrawn" || normalized === "as_drawn")
33
+ return "as-drawn";
34
+ if (normalized === "db-default" || normalized === "dbdefault" || normalized === "db_default" || normalized === "default")
35
+ return "db-default";
36
+ if (normalized === "pascal" || normalized === "pascalcase")
37
+ return "pascal";
38
+ if (normalized === "camel" || normalized === "camelcase")
39
+ return "camel";
40
+ if (normalized === "snake" || normalized === "snake_case" || normalized === "snakecase")
41
+ return "snake";
42
+ if (normalized === "screaming_snake" || normalized === "screaming-snake" || normalized === "screamingsnake")
43
+ return "screaming_snake";
44
+ if (normalized === "kebab" || normalized === "kebab-case" || normalized === "kebabcase")
45
+ return "kebab";
46
+ return null;
47
+ }
48
+ }
49
+ exports.NameStyleResolver = NameStyleResolver;
50
+ class NameStyler {
51
+ static resolveDbDefault(dialect) {
52
+ if (dialect === "sqlserver")
53
+ return "pascal";
54
+ if (dialect === "oracle")
55
+ return "screaming_snake";
56
+ return "snake";
57
+ }
58
+ static resolve(style, dialect) {
59
+ return style === "db-default" ? NameStyler.resolveDbDefault(dialect) : style;
60
+ }
61
+ static apply(tables, dialect, naming) {
62
+ const tableStyle = NameStyler.resolve(naming.tableCase, dialect);
63
+ const fieldStyle = NameStyler.resolve(naming.fieldCase, dialect);
64
+ const usedTableNames = new Set();
65
+ const tableOldToNew = new Map();
66
+ const colOldToNewByTable = new Map();
67
+ for (const table of tables) {
68
+ const oldTableName = table.name;
69
+ const styledTableName = NameStyler.uniqueName(NameStyler.transform(oldTableName, tableStyle), usedTableNames, tableStyle);
70
+ tableOldToNew.set(oldTableName, styledTableName);
71
+ const usedColNames = new Set();
72
+ const colOldToNew = new Map();
73
+ for (const col of table.columns) {
74
+ const oldColName = col.name;
75
+ const styledColName = NameStyler.uniqueName(NameStyler.transform(oldColName, fieldStyle), usedColNames, fieldStyle);
76
+ colOldToNew.set(oldColName, styledColName);
77
+ }
78
+ colOldToNewByTable.set(oldTableName, colOldToNew);
79
+ }
80
+ for (const table of tables) {
81
+ const oldTableName = table.name;
82
+ const colOldToNew = colOldToNewByTable.get(oldTableName);
83
+ table.name = tableOldToNew.get(oldTableName) ?? table.name;
84
+ for (const col of table.columns) {
85
+ const oldColName = col.name;
86
+ col.name = colOldToNew?.get(oldColName) ?? col.name;
87
+ }
88
+ }
89
+ for (const table of tables) {
90
+ for (const col of table.columns) {
91
+ const refTableOld = col.referencesTable;
92
+ if (!refTableOld)
93
+ continue;
94
+ col.referencesTable = tableOldToNew.get(refTableOld) ?? refTableOld;
95
+ const refColOld = col.referencesColumn;
96
+ if (!refColOld)
97
+ continue;
98
+ const refColMap = colOldToNewByTable.get(refTableOld);
99
+ col.referencesColumn = refColMap?.get(refColOld) ?? refColOld;
100
+ }
101
+ }
102
+ return { tableStyle, fieldStyle };
103
+ }
104
+ static transform(name, style) {
105
+ const trimmed = name.trim();
106
+ if (!trimmed)
107
+ return trimmed;
108
+ if (style === "as-drawn")
109
+ return trimmed;
110
+ const words = NameStyler.words(trimmed);
111
+ if (words.length === 0)
112
+ return trimmed;
113
+ if (style === "snake")
114
+ return words.map((w) => w.toLowerCase()).join("_");
115
+ if (style === "screaming_snake")
116
+ return words.map((w) => w.toUpperCase()).join("_");
117
+ if (style === "kebab")
118
+ return words.map((w) => w.toLowerCase()).join("-");
119
+ if (style === "pascal")
120
+ return words.map((w) => NameStyler.capitalize(w)).join("");
121
+ if (style === "camel") {
122
+ const [first, ...rest] = words;
123
+ return [first.toLowerCase(), ...rest.map((w) => NameStyler.capitalize(w))].join("");
124
+ }
125
+ return trimmed;
126
+ }
127
+ static words(input) {
128
+ const withCamelBoundaries = input
129
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
130
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
131
+ const normalized = withCamelBoundaries.replace(/[^A-Za-z0-9]+/g, " ");
132
+ return normalized.split(/\s+/).map((w) => w.trim()).filter(Boolean);
133
+ }
134
+ static capitalize(word) {
135
+ if (!word)
136
+ return word;
137
+ const lower = word.toLowerCase();
138
+ return lower[0].toUpperCase() + lower.slice(1);
139
+ }
140
+ static uniqueName(base, used, style) {
141
+ const trimmed = base.trim();
142
+ if (!trimmed)
143
+ return trimmed;
144
+ if (!used.has(trimmed)) {
145
+ used.add(trimmed);
146
+ return trimmed;
147
+ }
148
+ let i = 2;
149
+ while (true) {
150
+ const candidate = `${trimmed}${NameStyler.suffix(style, i)}`;
151
+ if (!used.has(candidate)) {
152
+ used.add(candidate);
153
+ return candidate;
154
+ }
155
+ i++;
156
+ }
157
+ }
158
+ static suffix(style, i) {
159
+ if (style === "kebab")
160
+ return `-${i}`;
161
+ if (style === "snake" || style === "screaming_snake" || style === "as-drawn")
162
+ return `_${i}`;
163
+ return `${i}`;
164
+ }
165
+ }
166
+ exports.NameStyler = NameStyler;
167
+ class CliParser {
168
+ static HELP = [
169
+ "",
170
+ "+--------------------------------------------------------------+",
171
+ "| draw2sql: Convert draw.io ER diagram into SQL DDL |",
172
+ "+--------------------------------------------------------------+",
173
+ "",
174
+ "Usage:",
175
+ " draw2sql --input <file> --dialect <dialect> [options]",
176
+ " draw2sql <input.drawio> <dialect> [output.sql] [options]",
177
+ "",
178
+ "Arguments:",
179
+ " -i, --input <file> Input .drawio diagram file (or 1st positional arg)",
180
+ " -d, --dialect <dialect> SQL dialect to generate (or 2nd positional arg; or set in diagram)",
181
+ " -o, --output <file> Output SQL file (default: <input>.<dialect>.sql)",
182
+ " -f, --overwrite Overwrite output file if it exists",
183
+ " -h, --help Show this help",
184
+ " --schema <name> Qualify table names with a schema prefix; omit to use the database default (ignored for sqlite)",
185
+ " --table-case <case> Table naming case (default: db-default)",
186
+ " --field-case <case> Field/column naming case (default: db-default)",
187
+ "",
188
+ "Examples:",
189
+ " draw2sql -i schema.drawio -d postgres",
190
+ " draw2sql -i schema.drawio -d mariadb -o out.sql -f",
191
+ " draw2sql -i schema.drawio -d oracle --schema MYAPP",
192
+ " draw2sql -i schema.drawio -d sqlserver --table-case pascal --field-case camel",
193
+ " draw2sql schema.drawio oracle",
194
+ "",
195
+ "Diagram options:",
196
+ " Add a text block like this anywhere in your diagram:",
197
+ " +--------------------+",
198
+ " | draw2sql |",
199
+ " | dialect = oracle |",
200
+ " | schema = myschema |",
201
+ " +--------------------+",
202
+ " Recognized keys: dialect (overrides -d), schema",
203
+ "",
204
+ "Dialects:",
205
+ " postgres",
206
+ " mariadb (mysql is accepted as an alias)",
207
+ " sqlserver",
208
+ " sqlite",
209
+ " oracle",
210
+ "",
211
+ "Cases:",
212
+ " as-drawn No transformation — use names exactly as drawn",
213
+ " db-default Dialect's conventional case (default)",
214
+ " pascal MyTableName (sqlserver default)",
215
+ " camel myTableName",
216
+ " snake my_table_name (postgres, mariadb, sqlite default)",
217
+ " screaming_snake MY_TABLE_NAME (oracle default)",
218
+ " kebab my-table-name",
219
+ ].join("\n");
220
+ static parse(argv) {
221
+ if (argv.some((a) => a === "--help" || a === "-h")) {
222
+ console.log(CliParser.HELP);
223
+ process.exit(0);
224
+ }
225
+ const positional = argv.filter((x) => !x.startsWith("-"));
226
+ let inputFile = "";
227
+ let outputFile = "";
228
+ let dialect = "";
229
+ let schema = "";
230
+ let tableCase = "db-default";
231
+ let fieldCase = "db-default";
232
+ let overwrite = false;
233
+ if (positional.length >= 3) {
234
+ [inputFile, dialect, outputFile] = positional;
235
+ }
236
+ else if (positional.length === 2) {
237
+ [inputFile, dialect] = positional;
238
+ }
239
+ for (let i = 0; i < argv.length; i++) {
240
+ const token = argv[i];
241
+ const next = argv[i + 1];
242
+ if ((token === "--input" || token === "-i") && next)
243
+ inputFile = next;
244
+ if ((token === "--output" || token === "-o") && next)
245
+ outputFile = next;
246
+ if ((token === "--dialect" || token === "-d") && next)
247
+ dialect = next;
248
+ if (token === "--schema" && next)
249
+ schema = next;
250
+ if ((token === "--table-case" || token === "--table-style") && next) {
251
+ const normalized = NameStyleResolver.normalize(next);
252
+ if (!normalized)
253
+ throw new Error(`Unsupported table case: ${next}`);
254
+ tableCase = normalized;
255
+ }
256
+ if ((token === "--field-case" || token === "--column-case" || token === "--field-style" || token === "--column-style") && next) {
257
+ const normalized = NameStyleResolver.normalize(next);
258
+ if (!normalized)
259
+ throw new Error(`Unsupported field case: ${next}`);
260
+ fieldCase = normalized;
261
+ }
262
+ if (token === "--overwrite" || token === "-f")
263
+ overwrite = true;
264
+ }
265
+ if (!inputFile || !dialect) {
266
+ throw new Error(CliParser.HELP);
267
+ }
268
+ const normalizedDialect = DialectResolver.normalize(dialect);
269
+ if (!normalizedDialect) {
270
+ throw new Error(`Unsupported SQL dialect: ${dialect}`);
271
+ }
272
+ return {
273
+ inputFile,
274
+ outputFile: outputFile || undefined,
275
+ dialect: normalizedDialect,
276
+ schema: schema || undefined,
277
+ tableCase,
278
+ fieldCase,
279
+ overwrite,
280
+ };
281
+ }
282
+ }
283
+ exports.CliParser = CliParser;
284
+ class XmlText {
285
+ static decode(input) {
286
+ return input
287
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
288
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)))
289
+ .replace(/&quot;/g, '"')
290
+ .replace(/&apos;/g, "'")
291
+ .replace(/&lt;/g, "<")
292
+ .replace(/&gt;/g, ">")
293
+ .replace(/&amp;/g, "&");
294
+ }
295
+ static stripHtml(input) {
296
+ return XmlText.decode(input)
297
+ .replace(/<br\s*\/?\s*>/gi, "\n")
298
+ .replace(/\\n/g, "\n")
299
+ .replace(/<[^>]+>/g, "")
300
+ .replace(/\u00A0/g, " ")
301
+ .trim();
302
+ }
303
+ static normalizeColumnName(name) {
304
+ return name.replace(/^`|`$/g, "").replace(/^\[|\]$/g, "").replace(/^"|"$/g, "").trim();
305
+ }
306
+ }
307
+ class DrawIoDiagramParser {
308
+ parse(xml) {
309
+ const cells = this.parseCells(xml);
310
+ const cellsById = new Map(cells.map((c) => [c.id, c]));
311
+ const cellsByParent = this.indexByParent(cells);
312
+ const tableCells = cells.filter((c) => c.vertex && /(?:^|;)shape=table(?:;|$)/.test(c.style));
313
+ const tables = tableCells
314
+ .map((tableCell) => {
315
+ const name = XmlText.stripHtml(tableCell.value).replace(/\s+/g, " ").trim();
316
+ const columns = this.extractTableColumns(cellsByParent, tableCell);
317
+ const table = { id: tableCell.id, name, columns };
318
+ this.assignImplicitKeys(table);
319
+ return table;
320
+ })
321
+ .filter((t) => t.name.length > 0);
322
+ this.bindEdgeBasedRelationships(cells, cellsById, cellsByParent, tables);
323
+ this.deduceForeignKeyTargets(tables);
324
+ const parameters = {};
325
+ for (const cell of cells) {
326
+ if (!cell.vertex || cell.style.includes("shape=table") || !cell.value)
327
+ continue;
328
+ const parsed = this.parseParameterBlock(XmlText.stripHtml(cell.value));
329
+ for (const [k, v] of Object.entries(parsed)) {
330
+ parameters[k] = v;
331
+ }
332
+ }
333
+ return { tables, parameters };
334
+ }
335
+ parseCells(xml) {
336
+ const cells = [];
337
+ const cellRegex = /<mxCell\b([^>]*?)(?:\/>|>)/g;
338
+ for (const match of xml.matchAll(cellRegex)) {
339
+ const attrs = this.parseAttributes(match[1] ?? "");
340
+ if (!attrs.id)
341
+ continue;
342
+ cells.push({
343
+ id: attrs.id,
344
+ parent: attrs.parent,
345
+ style: attrs.style ?? "",
346
+ value: attrs.value ?? "",
347
+ vertex: attrs.vertex === "1",
348
+ edge: attrs.edge === "1",
349
+ source: attrs.source,
350
+ target: attrs.target,
351
+ });
352
+ }
353
+ return cells;
354
+ }
355
+ parseAttributes(rawAttrs) {
356
+ const out = {};
357
+ const attrRegex = /([a-zA-Z_][\w:-]*)\s*=\s*"([^"]*)"/g;
358
+ for (const match of rawAttrs.matchAll(attrRegex)) {
359
+ out[match[1]] = XmlText.decode(match[2]);
360
+ }
361
+ return out;
362
+ }
363
+ indexByParent(cells) {
364
+ const map = new Map();
365
+ for (const cell of cells) {
366
+ if (!cell.parent)
367
+ continue;
368
+ if (!map.has(cell.parent))
369
+ map.set(cell.parent, []);
370
+ map.get(cell.parent)?.push(cell);
371
+ }
372
+ return map;
373
+ }
374
+ parseParameterBlock(text) {
375
+ const params = {};
376
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
377
+ const hasMarker = lines.some((l) => /draw2sql/i.test(l));
378
+ for (const line of lines) {
379
+ const cleaned = line.replace(/^draw2sql\s*[:\-]?\s*/i, "");
380
+ const m = cleaned.match(/^([A-Za-z0-9_ .-]+)\s*[:=]\s*(.+)$/);
381
+ if (!m)
382
+ continue;
383
+ const key = m[1].toLowerCase().replace(/[\s_.-]+/g, "");
384
+ const value = m[2].trim();
385
+ if (!value)
386
+ continue;
387
+ if (!hasMarker && !(key.includes("sql") || key === "schema" || key === "flavor")) {
388
+ continue;
389
+ }
390
+ params[key] = value;
391
+ }
392
+ return params;
393
+ }
394
+ parseColumnLabel(raw) {
395
+ let text = XmlText.stripHtml(raw).replace(/\s+/g, " ").trim();
396
+ if (!text)
397
+ return null;
398
+ const markers = new Set();
399
+ const trailingMeta = text.match(/\(([^)]+)\)\s*$/);
400
+ if (trailingMeta) {
401
+ for (const part of trailingMeta[1].split(/[;,/|]/)) {
402
+ const word = part.trim().toUpperCase();
403
+ if (MARKER_WORDS.has(word))
404
+ markers.add(word);
405
+ }
406
+ text = text.slice(0, trailingMeta.index).trim();
407
+ }
408
+ const bracketMeta = text.match(/\[([^\]]+)\]$/);
409
+ if (bracketMeta) {
410
+ for (const part of bracketMeta[1].split(/[;,/|]/)) {
411
+ const word = part.trim().toUpperCase();
412
+ if (MARKER_WORDS.has(word))
413
+ markers.add(word);
414
+ }
415
+ text = text.slice(0, bracketMeta.index).trim();
416
+ }
417
+ let name = text;
418
+ let rawType;
419
+ if (text.includes(":")) {
420
+ const [left, ...right] = text.split(":");
421
+ name = left.trim();
422
+ rawType = right.join(":").trim() || undefined;
423
+ }
424
+ else {
425
+ const tokens = text.split(" ").filter(Boolean);
426
+ if (tokens.length >= 2 && /[a-z]/i.test(tokens[1])) {
427
+ const possibleType = tokens.slice(1).join(" ");
428
+ if (/^(varchar|char|text|uuid|int|bigint|decimal|numeric|date|datetime|timestamp|bool|boolean|json|jsonb|number|clob|blob)/i.test(possibleType)) {
429
+ name = tokens[0];
430
+ rawType = possibleType;
431
+ }
432
+ }
433
+ }
434
+ name = XmlText.normalizeColumnName(name);
435
+ if (!name)
436
+ return null;
437
+ const upper = text.toUpperCase();
438
+ if (upper.startsWith("PK "))
439
+ markers.add("PK");
440
+ if (upper.startsWith("FK "))
441
+ markers.add("FK");
442
+ if (upper.includes(" NOT NULL"))
443
+ markers.add("NOT NULL");
444
+ if (upper.startsWith("PK "))
445
+ name = name.replace(/^PK\s+/i, "").trim();
446
+ if (upper.startsWith("FK "))
447
+ name = name.replace(/^FK\s+/i, "").trim();
448
+ return {
449
+ name,
450
+ rawType,
451
+ inferredType: undefined,
452
+ nullable: !(markers.has("NN") || markers.has("NOT NULL") || markers.has("PK")),
453
+ primaryKey: markers.has("PK"),
454
+ foreignKey: markers.has("FK"),
455
+ unique: markers.has("UNIQUE") || markers.has("UQ"),
456
+ };
457
+ }
458
+ extractTableColumns(cellsByParent, tableCell) {
459
+ const columns = [];
460
+ const seen = new Set();
461
+ const tableChildren = cellsByParent.get(tableCell.id) ?? [];
462
+ for (const row of tableChildren) {
463
+ const isRow = row.style.includes("shape=tableRow");
464
+ const isText = row.style.includes("text;") || row.style.startsWith("text");
465
+ if (isRow) {
466
+ const childParts = (cellsByParent.get(row.id) ?? []).map((c) => XmlText.stripHtml(c.value)).filter(Boolean);
467
+ if (childParts.length === 0 && row.value) {
468
+ const parsed = this.parseColumnLabel(row.value);
469
+ if (parsed && !seen.has(parsed.name.toLowerCase())) {
470
+ columns.push(parsed);
471
+ seen.add(parsed.name.toLowerCase());
472
+ }
473
+ continue;
474
+ }
475
+ const markerWords = childParts.map((p) => p.toUpperCase()).filter((p) => MARKER_WORDS.has(p));
476
+ const candidateName = childParts.filter((p) => !MARKER_WORDS.has(p.toUpperCase())).pop();
477
+ if (candidateName) {
478
+ const parsed = this.parseColumnLabel(candidateName);
479
+ if (parsed && !seen.has(parsed.name.toLowerCase())) {
480
+ for (const marker of markerWords) {
481
+ if (marker === "PK")
482
+ parsed.primaryKey = true;
483
+ if (marker === "FK")
484
+ parsed.foreignKey = true;
485
+ if (marker === "NN" || marker === "NOT NULL")
486
+ parsed.nullable = false;
487
+ if (marker === "UNIQUE" || marker === "UQ")
488
+ parsed.unique = true;
489
+ }
490
+ if (parsed.primaryKey)
491
+ parsed.nullable = false;
492
+ columns.push(parsed);
493
+ seen.add(parsed.name.toLowerCase());
494
+ }
495
+ }
496
+ continue;
497
+ }
498
+ if (isText) {
499
+ const lines = XmlText.stripHtml(row.value).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
500
+ for (const line of lines) {
501
+ const parsed = this.parseColumnLabel(line);
502
+ if (parsed && !seen.has(parsed.name.toLowerCase())) {
503
+ columns.push(parsed);
504
+ seen.add(parsed.name.toLowerCase());
505
+ }
506
+ }
507
+ }
508
+ }
509
+ return columns;
510
+ }
511
+ assignImplicitKeys(table) {
512
+ if (table.columns.length === 0)
513
+ return;
514
+ const hasPk = table.columns.some((c) => c.primaryKey);
515
+ if (!hasPk) {
516
+ const idCol = table.columns.find((c) => c.name.toLowerCase() === "id")
517
+ ?? table.columns.find((c) => c.name.toLowerCase() === `${table.name.toLowerCase()}id`);
518
+ if (idCol) {
519
+ idCol.primaryKey = true;
520
+ idCol.nullable = false;
521
+ }
522
+ }
523
+ for (const col of table.columns) {
524
+ if (!col.foreignKey && /id$/i.test(col.name) && !col.primaryKey && col.name.toLowerCase() !== "id") {
525
+ col.foreignKey = true;
526
+ }
527
+ }
528
+ }
529
+ deduceForeignKeyTargets(tables) {
530
+ const tableByName = new Map();
531
+ for (const table of tables) {
532
+ tableByName.set(table.name.toLowerCase(), table);
533
+ }
534
+ for (const table of tables) {
535
+ for (const col of table.columns) {
536
+ if (!col.foreignKey || col.referencesTable)
537
+ continue;
538
+ const base = col.name.replace(/id$/i, "");
539
+ if (!base)
540
+ continue;
541
+ const candidates = [base.toLowerCase(), `${base.toLowerCase()}s`, `${base.toLowerCase()}es`];
542
+ const target = candidates.map((key) => tableByName.get(key)).find(Boolean);
543
+ if (!target)
544
+ continue;
545
+ col.referencesTable = target.name;
546
+ col.referencesColumn = target.columns.find((c) => c.primaryKey)?.name ?? DEFAULT_REF_COLUMN;
547
+ }
548
+ }
549
+ }
550
+ bindEdgeBasedRelationships(cells, cellsById, cellsByParent, tables) {
551
+ const rowToTableName = new Map();
552
+ for (const t of tables) {
553
+ const children = cellsByParent.get(t.id) ?? [];
554
+ for (const row of children) {
555
+ if (row.style.includes("shape=tableRow")) {
556
+ rowToTableName.set(row.id, t.name);
557
+ for (const part of cellsByParent.get(row.id) ?? []) {
558
+ rowToTableName.set(part.id, t.name);
559
+ }
560
+ }
561
+ }
562
+ }
563
+ const findColumnByCellId = (cellId) => {
564
+ let current = cellsById.get(cellId);
565
+ while (current) {
566
+ const tableName = rowToTableName.get(current.id);
567
+ if (tableName) {
568
+ const table = tables.find((t) => t.name === tableName);
569
+ if (!table)
570
+ return null;
571
+ const textParts = [
572
+ XmlText.stripHtml(current.value),
573
+ ...(cellsByParent.get(current.id) ?? []).map((p) => XmlText.stripHtml(p.value)),
574
+ ].filter(Boolean);
575
+ for (const text of textParts) {
576
+ const colName = XmlText.normalizeColumnName(text.replace(/^PK\s+|^FK\s+/i, ""));
577
+ const col = table.columns.find((c) => c.name.toLowerCase() === colName.toLowerCase());
578
+ if (col)
579
+ return { table, column: col };
580
+ }
581
+ const likely = table.columns.find((c) => c.foreignKey) ?? table.columns.find((c) => c.primaryKey);
582
+ if (likely)
583
+ return { table, column: likely };
584
+ }
585
+ current = current.parent ? cellsById.get(current.parent) : undefined;
586
+ }
587
+ return null;
588
+ };
589
+ for (const edge of cells.filter((c) => c.edge && c.source && c.target)) {
590
+ const a = findColumnByCellId(edge.source);
591
+ const b = findColumnByCellId(edge.target);
592
+ if (!a || !b || a.table.name === b.table.name)
593
+ continue;
594
+ if (a.column.foreignKey && !a.column.referencesTable) {
595
+ a.column.referencesTable = b.table.name;
596
+ a.column.referencesColumn = b.column.primaryKey ? b.column.name : (b.table.columns.find((c) => c.primaryKey)?.name ?? DEFAULT_REF_COLUMN);
597
+ }
598
+ else if (b.column.foreignKey && !b.column.referencesTable) {
599
+ b.column.referencesTable = a.table.name;
600
+ b.column.referencesColumn = a.column.primaryKey ? a.column.name : (a.table.columns.find((c) => c.primaryKey)?.name ?? DEFAULT_REF_COLUMN);
601
+ }
602
+ }
603
+ }
604
+ }
605
+ exports.DrawIoDiagramParser = DrawIoDiagramParser;
606
+ class SqlTypeMapper {
607
+ dialect;
608
+ constructor(dialect) {
609
+ this.dialect = dialect;
610
+ }
611
+ map(rawType) {
612
+ const t = rawType.trim().toLowerCase();
613
+ if (t === "string") {
614
+ if (this.dialect === "sqlserver")
615
+ return "nvarchar(255)";
616
+ if (this.dialect === "oracle")
617
+ return "varchar2(255)";
618
+ return "text";
619
+ }
620
+ if (t === "bool") {
621
+ if (this.dialect === "sqlserver")
622
+ return "bit";
623
+ if (this.dialect === "oracle")
624
+ return "number(1)";
625
+ return "boolean";
626
+ }
627
+ if (t === "datetime") {
628
+ if (this.dialect === "postgres")
629
+ return "timestamptz";
630
+ if (this.dialect === "sqlserver")
631
+ return "datetime2";
632
+ if (this.dialect === "oracle")
633
+ return "timestamp";
634
+ }
635
+ if (t === "uuid" && this.dialect === "mariadb")
636
+ return "char(36)";
637
+ if (t === "uuid" && this.dialect === "sqlserver")
638
+ return "uniqueidentifier";
639
+ if (t === "uuid" && this.dialect === "oracle")
640
+ return "raw(16)";
641
+ if ((t === "json" || t === "jsonb") && this.dialect === "oracle")
642
+ return "clob";
643
+ if (t === "text" && this.dialect === "oracle")
644
+ return "clob";
645
+ return rawType;
646
+ }
647
+ infer(columnName) {
648
+ const lower = columnName.toLowerCase();
649
+ if (lower.endsWith("key")) {
650
+ if (this.dialect === "sqlserver")
651
+ return "nvarchar(255)";
652
+ if (this.dialect === "oracle")
653
+ return "varchar2(255)";
654
+ return "text";
655
+ }
656
+ if (lower.includes("external") && lower.endsWith("id")) {
657
+ if (this.dialect === "sqlserver")
658
+ return "nvarchar(255)";
659
+ if (this.dialect === "oracle")
660
+ return "varchar2(255)";
661
+ return "text";
662
+ }
663
+ if (lower === "id" || lower.endsWith("id")) {
664
+ if (this.dialect === "postgres")
665
+ return "uuid";
666
+ if (this.dialect === "mariadb")
667
+ return "char(36)";
668
+ if (this.dialect === "sqlserver")
669
+ return "uniqueidentifier";
670
+ if (this.dialect === "oracle")
671
+ return "raw(16)";
672
+ return "text";
673
+ }
674
+ if (lower.startsWith("is") || lower.startsWith("has")) {
675
+ if (this.dialect === "sqlserver")
676
+ return "bit";
677
+ if (this.dialect === "sqlite")
678
+ return "integer";
679
+ if (this.dialect === "oracle")
680
+ return "number(1)";
681
+ return "boolean";
682
+ }
683
+ if (lower.includes("createdat") || lower.includes("updatedat") || lower.includes("deletedat")) {
684
+ if (this.dialect === "postgres")
685
+ return "timestamptz";
686
+ if (this.dialect === "mariadb")
687
+ return "datetime";
688
+ if (this.dialect === "sqlserver")
689
+ return "datetime2";
690
+ if (this.dialect === "oracle")
691
+ return "timestamp";
692
+ return "text";
693
+ }
694
+ if (lower.includes("date")) {
695
+ if (this.dialect === "sqlite")
696
+ return "text";
697
+ if (this.dialect === "oracle")
698
+ return "date";
699
+ return "date";
700
+ }
701
+ if (lower.includes("amount") || lower.includes("price") || lower.includes("total") || lower.includes("cost")) {
702
+ return this.dialect === "oracle" ? "number(12,2)" : "decimal(12,2)";
703
+ }
704
+ if (lower.includes("count") || lower.includes("qty") || lower.includes("quantity")) {
705
+ return this.dialect === "oracle" ? "number(10)" : "integer";
706
+ }
707
+ if (lower.includes("json") || lower.includes("metadata")) {
708
+ if (this.dialect === "postgres")
709
+ return "jsonb";
710
+ if (this.dialect === "mariadb")
711
+ return "json";
712
+ if (this.dialect === "sqlserver")
713
+ return "nvarchar(max)";
714
+ if (this.dialect === "oracle")
715
+ return "clob";
716
+ return "text";
717
+ }
718
+ if (this.dialect === "sqlserver")
719
+ return "nvarchar(255)";
720
+ if (this.dialect === "oracle")
721
+ return "varchar2(255)";
722
+ return "text";
723
+ }
724
+ }
725
+ exports.SqlTypeMapper = SqlTypeMapper;
726
+ class SqlSyntax {
727
+ settings;
728
+ constructor(settings) {
729
+ this.settings = settings;
730
+ }
731
+ quoteIdent(name) {
732
+ if (this.settings.dialect === "mariadb")
733
+ return `\`${name}\``;
734
+ if (this.settings.dialect === "sqlserver")
735
+ return `[${name}]`;
736
+ return `"${name}"`;
737
+ }
738
+ qualifyTable(name) {
739
+ if (!this.settings.schema || this.settings.dialect === "sqlite")
740
+ return this.quoteIdent(name);
741
+ if (this.settings.dialect === "sqlserver") {
742
+ return `${this.quoteIdent(this.settings.schema)}.${this.quoteIdent(name)}`;
743
+ }
744
+ return `${this.quoteIdent(this.settings.schema)}.${this.quoteIdent(name)}`;
745
+ }
746
+ createTable(table) {
747
+ const lines = this.getCreateColumns(table);
748
+ if (this.settings.dialect === "sqlserver") {
749
+ return [
750
+ `IF OBJECT_ID(N'${table.name}', N'U') IS NULL`,
751
+ "BEGIN",
752
+ `CREATE TABLE ${this.qualifyTable(table.name)} (`,
753
+ lines.join(",\n"),
754
+ ");",
755
+ "END;",
756
+ ].join("\n");
757
+ }
758
+ if (this.settings.dialect === "oracle") {
759
+ const sql = [
760
+ `CREATE TABLE ${this.qualifyTable(table.name)} (`,
761
+ lines.join(",\n"),
762
+ ")",
763
+ ].join("\n");
764
+ return [
765
+ "BEGIN",
766
+ ` EXECUTE IMMEDIATE q'[${sql}]';`,
767
+ "EXCEPTION",
768
+ " WHEN OTHERS THEN",
769
+ " IF SQLCODE != -955 THEN RAISE; END IF;",
770
+ "END;",
771
+ "/",
772
+ ].join("\n");
773
+ }
774
+ return [
775
+ `CREATE TABLE IF NOT EXISTS ${this.qualifyTable(table.name)} (`,
776
+ lines.join(",\n"),
777
+ ");",
778
+ ].join("\n");
779
+ }
780
+ addColumn(tableName, col) {
781
+ const colType = col.inferredType ?? col.rawType ?? "text";
782
+ const nullSql = col.nullable ? "" : " NOT NULL";
783
+ if (this.settings.dialect === "sqlserver") {
784
+ return [
785
+ `IF COL_LENGTH(N'${tableName}', N'${col.name}') IS NULL`,
786
+ ` ALTER TABLE ${this.qualifyTable(tableName)} ADD ${this.quoteIdent(col.name)} ${colType}${nullSql};`,
787
+ ].join("\n");
788
+ }
789
+ if (this.settings.dialect === "oracle") {
790
+ const checkSchema = (this.settings.schema ?? "").toUpperCase();
791
+ const schemaFilter = checkSchema ? ` AND owner = '${checkSchema}'` : "";
792
+ return [
793
+ "DECLARE",
794
+ " v_count NUMBER;",
795
+ "BEGIN",
796
+ " SELECT COUNT(1) INTO v_count",
797
+ " FROM all_tab_cols",
798
+ ` WHERE table_name = '${tableName.toUpperCase()}'${schemaFilter}`,
799
+ ` AND column_name = '${col.name.toUpperCase()}';`,
800
+ " IF v_count = 0 THEN",
801
+ ` EXECUTE IMMEDIATE q'[ALTER TABLE ${this.qualifyTable(tableName)} ADD (${this.quoteIdent(col.name)} ${colType}${nullSql})]';`,
802
+ " END IF;",
803
+ "END;",
804
+ "/",
805
+ ].join("\n");
806
+ }
807
+ return `ALTER TABLE ${this.qualifyTable(tableName)} ADD COLUMN IF NOT EXISTS ${this.quoteIdent(col.name)} ${colType}${nullSql};`;
808
+ }
809
+ addForeignKey(table, col) {
810
+ if (!col.referencesTable || !col.referencesColumn)
811
+ return "";
812
+ const constraintName = `fk_${table.name}_${col.name}_${col.referencesTable}_${col.referencesColumn}`.replace(/[^A-Za-z0-9_]/g, "_");
813
+ const tableQ = this.qualifyTable(table.name);
814
+ const colQ = this.quoteIdent(col.name);
815
+ const refTableQ = this.qualifyTable(col.referencesTable);
816
+ const refColQ = this.quoteIdent(col.referencesColumn);
817
+ if (this.settings.dialect === "postgres") {
818
+ return [
819
+ "DO $$",
820
+ "BEGIN",
821
+ ` IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '${constraintName}') THEN`,
822
+ ` ALTER TABLE ${tableQ} ADD CONSTRAINT ${this.quoteIdent(constraintName)} FOREIGN KEY (${colQ}) REFERENCES ${refTableQ} (${refColQ});`,
823
+ " END IF;",
824
+ "END $$;",
825
+ ].join("\n");
826
+ }
827
+ if (this.settings.dialect === "mariadb") {
828
+ return [
829
+ "SET @fk_exists := (",
830
+ " SELECT COUNT(1)",
831
+ " FROM information_schema.table_constraints",
832
+ " WHERE constraint_schema = DATABASE()",
833
+ ` AND table_name = '${table.name}'`,
834
+ ` AND constraint_name = '${constraintName}'`,
835
+ ");",
836
+ `SET @fk_sql := IF(@fk_exists = 0, 'ALTER TABLE ${table.name} ADD CONSTRAINT ${constraintName} FOREIGN KEY (${col.name}) REFERENCES ${col.referencesTable} (${col.referencesColumn});', 'SELECT 1;');`,
837
+ "PREPARE stmt FROM @fk_sql;",
838
+ "EXECUTE stmt;",
839
+ "DEALLOCATE PREPARE stmt;",
840
+ ].join("\n");
841
+ }
842
+ if (this.settings.dialect === "sqlserver") {
843
+ return [
844
+ `IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'${constraintName}')`,
845
+ ` ALTER TABLE ${tableQ} ADD CONSTRAINT ${this.quoteIdent(constraintName)} FOREIGN KEY (${colQ}) REFERENCES ${refTableQ} (${refColQ});`,
846
+ ].join("\n");
847
+ }
848
+ if (this.settings.dialect === "oracle") {
849
+ const owner = (this.settings.schema ?? "").toUpperCase();
850
+ const ownerFilter = owner ? ` AND owner = '${owner}'` : "";
851
+ return [
852
+ "DECLARE",
853
+ " v_count NUMBER;",
854
+ "BEGIN",
855
+ " SELECT COUNT(1) INTO v_count",
856
+ " FROM all_constraints",
857
+ ` WHERE constraint_name = '${constraintName.toUpperCase()}'${ownerFilter};`,
858
+ " IF v_count = 0 THEN",
859
+ ` EXECUTE IMMEDIATE q'[ALTER TABLE ${tableQ} ADD CONSTRAINT ${this.quoteIdent(constraintName)} FOREIGN KEY (${colQ}) REFERENCES ${refTableQ} (${refColQ})]';`,
860
+ " END IF;",
861
+ "END;",
862
+ "/",
863
+ ].join("\n");
864
+ }
865
+ return `-- SQLite note: FK ${constraintName} (${table.name}.${col.name} -> ${col.referencesTable}.${col.referencesColumn}) requires table rebuild if not created initially.`;
866
+ }
867
+ getCreateColumns(table) {
868
+ const lines = [];
869
+ const pkColumns = [];
870
+ const uniqueConstraints = [];
871
+ for (const col of table.columns) {
872
+ const parts = [`${this.quoteIdent(col.name)} ${col.inferredType ?? col.rawType ?? "text"}`, col.nullable ? "" : "NOT NULL"].filter(Boolean);
873
+ lines.push(` ${parts.join(" ")}`);
874
+ if (col.primaryKey)
875
+ pkColumns.push(this.quoteIdent(col.name));
876
+ if (col.unique)
877
+ uniqueConstraints.push(`UNIQUE (${this.quoteIdent(col.name)})`);
878
+ }
879
+ if (pkColumns.length > 0)
880
+ lines.push(` PRIMARY KEY (${pkColumns.join(", ")})`);
881
+ for (const uq of uniqueConstraints)
882
+ lines.push(` ${uq}`);
883
+ return lines;
884
+ }
885
+ }
886
+ class SqlGenerator {
887
+ generate(parsed, cliDialect, naming, cliSchema) {
888
+ const dialectOverride = parsed.parameters.dialect ?? parsed.parameters.sqldialect ?? parsed.parameters.flavor ?? parsed.parameters.sqlflavor ?? parsed.parameters.sqldatabase;
889
+ const effectiveDialect = dialectOverride ? (DialectResolver.normalize(dialectOverride) ?? cliDialect) : cliDialect;
890
+ const schema = cliSchema ?? parsed.parameters.schema ?? parsed.parameters.defaultschema;
891
+ const settings = { dialect: effectiveDialect, schema };
892
+ const mapper = new SqlTypeMapper(settings.dialect);
893
+ const dialect = new SqlSyntax(settings);
894
+ for (const table of parsed.tables) {
895
+ for (const col of table.columns) {
896
+ col.inferredType = col.rawType ? mapper.map(col.rawType) : mapper.infer(col.name);
897
+ }
898
+ }
899
+ const resolvedNaming = NameStyler.apply(parsed.tables, settings.dialect, naming);
900
+ const header = [
901
+ "-- Generated by draw2sql",
902
+ `-- Dialect: ${settings.dialect}`,
903
+ `-- Table Case: ${naming.tableCase} (${resolvedNaming.tableStyle})`,
904
+ `-- Field Case: ${naming.fieldCase} (${resolvedNaming.fieldStyle})`,
905
+ `-- Generated UTC: ${new Date().toISOString()}`,
906
+ ];
907
+ if (settings.schema) {
908
+ header.push(`-- Schema: ${settings.schema}`);
909
+ }
910
+ if (Object.keys(parsed.parameters).length > 0) {
911
+ header.push("-- Diagram Parameters:");
912
+ for (const [k, v] of Object.entries(parsed.parameters)) {
913
+ header.push(`-- ${k} = ${v}`);
914
+ }
915
+ }
916
+ if (parsed.tables.length === 0) {
917
+ header.push("-- No tables detected in draw.io file.");
918
+ return { sql: `${header.join("\n")}\n`, dialect: settings.dialect };
919
+ }
920
+ const sections = [header.join("\n")];
921
+ sections.push("\n-- 1) Create missing tables");
922
+ for (const table of parsed.tables) {
923
+ sections.push(dialect.createTable(table));
924
+ }
925
+ sections.push("\n-- 2) Add missing columns");
926
+ for (const table of parsed.tables) {
927
+ for (const col of table.columns) {
928
+ sections.push(dialect.addColumn(table.name, col));
929
+ }
930
+ }
931
+ sections.push("\n-- 3) Add foreign keys where inferred");
932
+ for (const table of parsed.tables) {
933
+ for (const col of table.columns.filter((c) => c.foreignKey)) {
934
+ const fkSql = dialect.addForeignKey(table, col);
935
+ if (fkSql)
936
+ sections.push(fkSql);
937
+ }
938
+ }
939
+ return {
940
+ sql: `${sections.join("\n\n")}\n`,
941
+ dialect: settings.dialect,
942
+ };
943
+ }
944
+ }
945
+ class Draw2SqlApp {
946
+ parser = new DrawIoDiagramParser();
947
+ generator = new SqlGenerator();
948
+ run(argv) {
949
+ const args = CliParser.parse(argv);
950
+ const xml = node_fs_1.default.readFileSync(args.inputFile, "utf8");
951
+ const parsed = this.parser.parse(xml);
952
+ const generated = this.generator.generate(parsed, args.dialect, { tableCase: args.tableCase, fieldCase: args.fieldCase }, args.schema);
953
+ const outputFile = args.outputFile ?? this.deriveOutputFile(args.inputFile, generated.dialect);
954
+ if (!args.overwrite && node_fs_1.default.existsSync(outputFile)) {
955
+ console.error(`Output file already exists: ${outputFile}`);
956
+ console.error(`Pass --overwrite (or -f) to replace it.`);
957
+ process.exit(1);
958
+ }
959
+ this.ensureDirForFile(outputFile);
960
+ node_fs_1.default.writeFileSync(outputFile, generated.sql, "utf8");
961
+ console.log("draw2sql complete.");
962
+ console.log(`Input: ${args.inputFile}`);
963
+ console.log(`Dialect: ${generated.dialect}`);
964
+ console.log(`Tables: ${parsed.tables.length}`);
965
+ console.log(`Output: ${outputFile}`);
966
+ }
967
+ deriveOutputFile(inputFile, dialect) {
968
+ const ext = node_path_1.default.extname(inputFile);
969
+ const base = inputFile.slice(0, inputFile.length - ext.length);
970
+ return `${base}.${dialect}.sql`;
971
+ }
972
+ ensureDirForFile(filePath) {
973
+ const dir = node_path_1.default.dirname(node_path_1.default.resolve(filePath));
974
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
975
+ }
976
+ }
977
+ if (require.main === module) {
978
+ try {
979
+ new Draw2SqlApp().run(process.argv.slice(2));
980
+ }
981
+ catch (err) {
982
+ console.error(err instanceof Error ? err.message : String(err));
983
+ process.exit(1);
984
+ }
985
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "draw2sql",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Generate SQL DDL from a draw.io XML ER diagram",
5
+ "author": "Kyle White",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kylemwhite/draw2sql.git"
9
+ },
10
+ "main": "dist/draw2sql.js",
11
+ "bin": {
12
+ "draw2sql": "dist/draw2sql.js"
13
+ },
14
+ "files": [
15
+ "dist/draw2sql.js"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "ts-node draw2sql.test.ts",
20
+ "start": "ts-node draw2sql.ts",
21
+ "prepublishOnly": "tsc",
22
+ "repack": "npm run build && npm pack && node -e \"const v=require('./package.json').version;require('child_process').execSync('npm install -g draw2sql-'+v+'.tgz',{stdio:'inherit'})\""
23
+ },
24
+ "keywords": [
25
+ "draw.io",
26
+ "sql",
27
+ "ddl",
28
+ "erd",
29
+ "diagram"
30
+ ],
31
+ "license": "MIT",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.0.0",
37
+ "ts-node": "^10.9.0",
38
+ "@types/node": "^22.0.0"
39
+ }
40
+ }