outlet-orm 4.2.1 → 5.5.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.
package/bin/reverse.js ADDED
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Outlet ORM — Database Reverse Engineering Tool
6
+ *
7
+ * Introspects an existing database (or SQL dump file) and generates:
8
+ * • Migration files (up/down using Schema Blueprint methods)
9
+ * • Seeder files (INSERT rows fetched from each table)
10
+ *
11
+ * Usage (CLI): node bin/reverse.js
12
+ * Usage (API): const { parseCreateTable, generateMigration, generateSeeder } = require('./bin/reverse');
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // readline is only initialised when running as a CLI (not when required as a lib)
19
+ let rl;
20
+ function getRL() {
21
+ if (!rl) {
22
+ const readline = require('readline');
23
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
24
+ }
25
+ return rl;
26
+ }
27
+ function question(q) { return new Promise(resolve => getRL().question(q, resolve)); }
28
+
29
+ // ─── SQL Parser ───────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Split the body of a CREATE TABLE statement into individual definition lines,
33
+ * respecting nested parentheses (ENUM values, CHECK expressions, etc.).
34
+ *
35
+ * @param {string} body Text between the outermost ( … )
36
+ * @returns {string[]}
37
+ */
38
+ function splitDefinitions(body) {
39
+ const lines = [];
40
+ let depth = 0;
41
+ let current = '';
42
+
43
+ for (let i = 0; i < body.length; i++) {
44
+ const ch = body[i];
45
+ if (ch === '(') { depth++; current += ch; }
46
+ else if (ch === ')') { depth--; current += ch; }
47
+ else if (ch === ',' && depth === 0) { lines.push(current.trim()); current = ''; }
48
+ else { current += ch; }
49
+ }
50
+ if (current.trim()) lines.push(current.trim());
51
+ return lines;
52
+ }
53
+
54
+ /**
55
+ * Parse a CREATE TABLE statement into a structured object.
56
+ * Handles MySQL, PostgreSQL, and SQLite dialects.
57
+ *
58
+ * @param {string} sql Raw CREATE TABLE SQL (single statement)
59
+ * @returns {{ tableName: string, columns: object[], foreignKeys: object[] } | null}
60
+ */
61
+ function parseCreateTable(sql) {
62
+ if (!sql || typeof sql !== 'string') return null;
63
+
64
+ // Strip single-line and block comments
65
+ sql = sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
66
+
67
+ const tableMatch = sql.match(
68
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"[]?(\w+)[`"\]]?\s*\(/i
69
+ );
70
+ if (!tableMatch) return null;
71
+
72
+ const tableName = tableMatch[1];
73
+ const columns = [];
74
+ const foreignKeys = [];
75
+
76
+ // Extract body between outermost parens
77
+ const bodyStart = sql.indexOf('(', tableMatch.index) + 1;
78
+ const bodyEnd = sql.lastIndexOf(')');
79
+ if (bodyStart <= 0 || bodyEnd < 0) return null;
80
+ const body = sql.slice(bodyStart, bodyEnd);
81
+
82
+ for (const line of splitDefinitions(body)) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed) continue;
85
+ const upperTrimmed = trimmed.toUpperCase();
86
+
87
+ // ── Table-level FOREIGN KEY constraint ───────────────────────────────────
88
+ if (/^FOREIGN\s+KEY/i.test(trimmed) || /^CONSTRAINT\s+\S+\s+FOREIGN\s+KEY/i.test(trimmed)) {
89
+ const fkMatch = trimmed.match(
90
+ /FOREIGN\s+KEY\s*\(`?(\w+)`?\)\s*REFERENCES\s+`?(\w+)`?\s*\(`?(\w+)`?\)/i
91
+ );
92
+ if (fkMatch) {
93
+ foreignKeys.push({
94
+ column: fkMatch[1],
95
+ referencedTable: fkMatch[2],
96
+ referencedColumn: fkMatch[3]
97
+ });
98
+ }
99
+ continue;
100
+ }
101
+
102
+ // ── Skip other table-level constraints and index declarations ─────────────
103
+ if (/^(PRIMARY\s+KEY|UNIQUE\s+(KEY|INDEX)?|KEY\s|INDEX\s|CHECK\s|CONSTRAINT\s)/i.test(trimmed)) {
104
+ continue;
105
+ }
106
+
107
+ // ── Column definition ─────────────────────────────────────────────────────
108
+ // Pattern: [`"]?colname[`"]? TYPE(params) ...rest...
109
+ const colMatch = trimmed.match(/^[`"[]?(\w+)[`"\]]?\s+(\w+(?:\s*\([^)]+\))?)\s*([\s\S]*)/i);
110
+ if (!colMatch) continue;
111
+
112
+ const [, name, rawType, rest] = colMatch;
113
+ const restUpper = rest.toUpperCase();
114
+
115
+ // Safety: skip if "name" is a reserved SQL keyword used at table level
116
+ if (['PRIMARY', 'UNIQUE', 'KEY', 'INDEX', 'CONSTRAINT', 'FOREIGN',
117
+ 'CHECK', 'FULLTEXT', 'SPATIAL'].includes(name.toUpperCase())) {
118
+ continue;
119
+ }
120
+
121
+ const col = {
122
+ name,
123
+ type: rawType.trim(),
124
+ nullable: !restUpper.includes('NOT NULL'),
125
+ autoIncrement: restUpper.includes('AUTO_INCREMENT') || restUpper.includes('AUTOINCREMENT'),
126
+ primary: restUpper.includes('PRIMARY KEY'),
127
+ unique: /\bUNIQUE\b/.test(restUpper) && !/\bUNIQUE\s+KEY\b/.test(restUpper),
128
+ unsigned: restUpper.includes('UNSIGNED'),
129
+ default: null
130
+ };
131
+
132
+ // Extract DEFAULT value (handles quoted strings and bare values)
133
+ const defMatch = rest.match(/DEFAULT\s+('(?:[^'\\]|\\.)*'|\S+)/i);
134
+ if (defMatch) {
135
+ col.default = defMatch[1].replace(/^'|'$/g, '');
136
+ }
137
+
138
+ columns.push(col);
139
+ }
140
+
141
+ return { tableName, columns, foreignKeys };
142
+ }
143
+
144
+ // ─── Blueprint Mapper ─────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Map a parsed column object → a Schema Blueprint call descriptor.
148
+ *
149
+ * @param {object} col Column object from parseCreateTable
150
+ * @returns {{ method: string, args: any[], modifiers: string[] }}
151
+ */
152
+ function columnToBlueprint(col) {
153
+ const rawType = col.type || '';
154
+ const typeLower = rawType.toLowerCase();
155
+
156
+ // Parse base type and optional numeric parameters
157
+ const paramMatch = rawType.match(/\(([^)]+)\)/);
158
+ const params = paramMatch ? paramMatch[1].split(',').map(s => s.trim()) : [];
159
+ const baseType = typeLower.replace(/\s*\([^)]+\)/, '').replace(/\s+unsigned$/i, '').trim();
160
+
161
+ // Build modifier chain
162
+ const modifiers = [];
163
+ const isKey = col.primary || col.autoIncrement;
164
+ if (!isKey && col.nullable) modifiers.push('nullable()');
165
+ if (!isKey && col.unique) modifiers.push('unique()');
166
+ if (!isKey && col.default !== null && col.default !== undefined) {
167
+ const defVal = /^-?\d+(\.\d+)?$/.test(String(col.default))
168
+ ? col.default
169
+ : `'${col.default}'`;
170
+ modifiers.push(`default(${defVal})`);
171
+ }
172
+
173
+ // ── Auto-increment primary keys ───────────────────────────────────────────
174
+ if (col.autoIncrement || (col.primary && col.name === 'id')) {
175
+ if (baseType === 'bigint' || baseType === 'bigserial') {
176
+ return { method: 'bigIncrements', args: [`'${col.name}'`], modifiers: [] };
177
+ }
178
+ return { method: 'increments', args: [`'${col.name}'`], modifiers: [] };
179
+ }
180
+
181
+ // ── Numeric types ────────────────────────────────────────────────────────
182
+ if (baseType === 'tinyint') {
183
+ if (params[0] === '1') return { method: 'boolean', args: [`'${col.name}'`], modifiers };
184
+ return { method: 'tinyInteger', args: [`'${col.name}'`], modifiers };
185
+ }
186
+ if (baseType === 'smallint')
187
+ return { method: 'smallInteger', args: [`'${col.name}'`], modifiers };
188
+ if (baseType === 'mediumint')
189
+ return { method: 'integer', args: [`'${col.name}'`], modifiers };
190
+ if (baseType === 'int' || baseType === 'integer')
191
+ return { method: 'integer', args: [`'${col.name}'`], modifiers };
192
+ if (baseType === 'bigint')
193
+ return { method: 'bigInteger', args: [`'${col.name}'`], modifiers };
194
+ if (baseType === 'serial')
195
+ return { method: 'increments', args: [`'${col.name}'`], modifiers: [] };
196
+ if (baseType === 'bigserial')
197
+ return { method: 'bigIncrements',args: [`'${col.name}'`], modifiers: [] };
198
+ if (baseType === 'float' || baseType === 'double' || baseType === 'real' ||
199
+ baseType === 'double precision')
200
+ return { method: 'float', args: [`'${col.name}'`], modifiers };
201
+ if (baseType === 'decimal' || baseType === 'numeric') {
202
+ const bpArgs = [`'${col.name}'`];
203
+ if (params.length >= 1) bpArgs.push(parseInt(params[0], 10));
204
+ if (params.length >= 2) bpArgs.push(parseInt(params[1], 10));
205
+ return { method: 'decimal', args: bpArgs, modifiers };
206
+ }
207
+ if (baseType === 'boolean' || baseType === 'bool')
208
+ return { method: 'boolean', args: [`'${col.name}'`], modifiers };
209
+
210
+ // ── String types ──────────────────────────────────────────────────────────
211
+ if (baseType === 'varchar' || baseType === 'character varying' || baseType === 'nvarchar') {
212
+ const len = params[0] ? parseInt(params[0], 10) : 255;
213
+ return { method: 'string', args: [`'${col.name}'`, len], modifiers };
214
+ }
215
+ if (baseType === 'char' || baseType === 'character') {
216
+ const len = params[0] ? parseInt(params[0], 10) : 1;
217
+ return { method: 'char', args: [`'${col.name}'`, len], modifiers };
218
+ }
219
+ if (baseType === 'text' || baseType === 'mediumtext' ||
220
+ baseType === 'longtext' || baseType === 'tinytext' || baseType === 'clob')
221
+ return { method: 'text', args: [`'${col.name}'`], modifiers };
222
+ if (baseType === 'blob' || baseType === 'mediumblob' || baseType === 'longblob' ||
223
+ baseType === 'tinyblob' || baseType === 'bytea' || baseType === 'binary' ||
224
+ baseType === 'varbinary')
225
+ return { method: 'binary', args: [`'${col.name}'`], modifiers };
226
+
227
+ // ── Date / time types ─────────────────────────────────────────────────────
228
+ if (baseType === 'date')
229
+ return { method: 'date', args: [`'${col.name}'`], modifiers };
230
+ if (baseType === 'datetime')
231
+ return { method: 'dateTime', args: [`'${col.name}'`], modifiers };
232
+ if (baseType === 'timestamp' || baseType === 'timestamptz')
233
+ return { method: 'timestamp',args: [`'${col.name}'`], modifiers };
234
+ if (baseType === 'time' || baseType === 'timetz')
235
+ return { method: 'time', args: [`'${col.name}'`], modifiers };
236
+ if (baseType === 'year')
237
+ return { method: 'year', args: [`'${col.name}'`], modifiers };
238
+
239
+ // ── JSON / UUID / Enum ────────────────────────────────────────────────────
240
+ if (baseType === 'json' || baseType === 'jsonb')
241
+ return { method: 'json', args: [`'${col.name}'`], modifiers };
242
+ if (baseType === 'uuid')
243
+ return { method: 'uuid', args: [`'${col.name}'`], modifiers };
244
+ if (baseType === 'enum') {
245
+ // Fallback: store as string, pick length from longest enum value
246
+ const maxLen = params.reduce((acc, p) => Math.max(acc, p.replace(/['"]/g, '').length), 50);
247
+ return { method: 'string', args: [`'${col.name}'`, maxLen], modifiers };
248
+ }
249
+
250
+ // Fallback
251
+ return { method: 'string', args: [`'${col.name}'`], modifiers };
252
+ }
253
+
254
+ // ─── Migration Generator ──────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Generate a complete Migration class from a parsed table definition.
258
+ *
259
+ * @param {object} tableInfo Output of parseCreateTable
260
+ * @returns {{ filename: string, className: string, code: string }}
261
+ */
262
+ function generateMigration(tableInfo) {
263
+ const { tableName, columns = [], foreignKeys = [] } = tableInfo;
264
+
265
+ // Timestamp prefix YYYYMMDD_HHmmss
266
+ const now = new Date();
267
+ const pad = n => String(n).padStart(2, '0');
268
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
269
+ + `_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
270
+
271
+ // PascalCase class name: blog_posts → CreateBlogPostsTable
272
+ const className = 'Create'
273
+ + tableName.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')
274
+ + 'Table';
275
+
276
+ // Detect timestamps shorthand
277
+ const useTimestamps = columns.some(c => c.name === 'created_at') &&
278
+ columns.some(c => c.name === 'updated_at');
279
+
280
+ // Build up() body lines
281
+ const upLines = [];
282
+ for (const col of columns) {
283
+ if (useTimestamps && (col.name === 'created_at' || col.name === 'updated_at')) continue;
284
+
285
+ const bp = columnToBlueprint(col);
286
+ const argsStr = bp.args.join(', ');
287
+ let line = `table.${bp.method}(${argsStr})`;
288
+
289
+ for (const mod of bp.modifiers) {
290
+ line += `.${mod}`;
291
+ }
292
+ upLines.push(` ${line};`);
293
+ }
294
+
295
+ // Explicit foreign key constraints
296
+ for (const fk of foreignKeys) {
297
+ upLines.push(
298
+ ` table.foreign('${fk.column}')`
299
+ + `.references('${fk.referencedColumn}')`
300
+ + `.on('${fk.referencedTable}');`
301
+ );
302
+ }
303
+
304
+ if (useTimestamps) {
305
+ upLines.push(` table.timestamps();`);
306
+ }
307
+
308
+ const code = [
309
+ `const { Schema } = require('outlet-orm');`,
310
+ ``,
311
+ `class ${className} {`,
312
+ ` async up(schema) {`,
313
+ ` await schema.create('${tableName}', (table) => {`,
314
+ ...upLines,
315
+ ` });`,
316
+ ` }`,
317
+ ``,
318
+ ` async down(schema) {`,
319
+ ` await schema.dropIfExists('${tableName}');`,
320
+ ` }`,
321
+ `}`,
322
+ ``,
323
+ `module.exports = new ${className}();`,
324
+ ``
325
+ ].join('\n');
326
+
327
+ return {
328
+ filename: `${ts}_create_${tableName}_table.js`,
329
+ className,
330
+ code
331
+ };
332
+ }
333
+
334
+ // ─── Seeder Generator ─────────────────────────────────────────────────────────
335
+
336
+ /**
337
+ * Generate a Seeder class from a table name and an array of row objects.
338
+ *
339
+ * @param {string} tableName
340
+ * @param {object[]} rows
341
+ * @returns {{ filename: string, className: string, code: string }}
342
+ */
343
+ function generateSeeder(tableName, rows) {
344
+ const className = tableName
345
+ .split('_')
346
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
347
+ .join('') + 'Seeder';
348
+
349
+ const rowsJson = JSON.stringify(rows, null, 4);
350
+
351
+ const code = [
352
+ `class ${className} {`,
353
+ ` async run(db) {`,
354
+ ` const rows = ${rowsJson};`,
355
+ ` for (const row of rows) {`,
356
+ ` await db.table('${tableName}').insert(row);`,
357
+ ` }`,
358
+ ` }`,
359
+ `}`,
360
+ ``,
361
+ `module.exports = new ${className}();`,
362
+ ``
363
+ ].join('\n');
364
+
365
+ return {
366
+ filename: `${tableName}_seeder.js`,
367
+ className,
368
+ code
369
+ };
370
+ }
371
+
372
+ // ─── Batch helpers ────────────────────────────────────────────────────────────
373
+
374
+ /**
375
+ * Parse a full SQL dump (multiple CREATE TABLE statements) and return
376
+ * migration objects for every table found.
377
+ *
378
+ * @param {string} sql
379
+ * @returns {Array<{ filename: string, className: string, code: string }>}
380
+ */
381
+ function reverseFromSql(sql) {
382
+ if (!sql || typeof sql !== 'string') return [];
383
+ const regex = /CREATE\s+TABLE\s+[\s\S]*?;/gi;
384
+ const stmts = sql.match(regex) || [];
385
+ return stmts
386
+ .map(stmt => parseCreateTable(stmt))
387
+ .filter(Boolean)
388
+ .map(info => generateMigration(info));
389
+ }
390
+
391
+ // ─── Database connection helpers ──────────────────────────────────────────────
392
+
393
+ async function getDatabaseConfig() {
394
+ const driver = (await question('Driver (mysql/postgres/sqlite): ')).trim().toLowerCase();
395
+ const config = { driver };
396
+
397
+ if (driver === 'mysql') {
398
+ config.host = (await question('Host (default: localhost): ')) || 'localhost';
399
+ config.port = parseInt((await question('Port (default: 3306): ')) || '3306', 10);
400
+ config.database = await question('Database: ');
401
+ config.user = await question('User: ');
402
+ config.password = await question('Password: ');
403
+ } else if (driver === 'postgres' || driver === 'postgresql') {
404
+ config.host = (await question('Host (default: localhost): ')) || 'localhost';
405
+ config.port = parseInt((await question('Port (default: 5432): ')) || '5432', 10);
406
+ config.database = await question('Database: ');
407
+ config.user = await question('User: ');
408
+ config.password = await question('Password: ');
409
+ } else if (driver === 'sqlite') {
410
+ config.database = await question('SQLite file path: ');
411
+ } else {
412
+ throw new Error(`Unsupported driver: ${driver}`);
413
+ }
414
+
415
+ return config;
416
+ }
417
+
418
+ async function fetchTablesList(connection, driver) {
419
+ if (driver === 'mysql') {
420
+ const rows = await connection.query('SHOW TABLES');
421
+ const key = Object.keys(rows[0])[0];
422
+ return rows.map(r => r[key]);
423
+ }
424
+ if (driver === 'postgres' || driver === 'postgresql') {
425
+ const rows = await connection.query(
426
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
427
+ );
428
+ return rows.map(r => r.table_name);
429
+ }
430
+ if (driver === 'sqlite') {
431
+ const rows = await connection.query(
432
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
433
+ );
434
+ return rows.map(r => r.name);
435
+ }
436
+ throw new Error(`Unsupported driver: ${driver}`);
437
+ }
438
+
439
+ async function fetchCreateTableSql(connection, driver, tableName) {
440
+ if (driver === 'mysql') {
441
+ const rows = await connection.query(`SHOW CREATE TABLE \`${tableName}\``);
442
+ return rows[0]['Create Table'];
443
+ }
444
+ if (driver === 'postgres' || driver === 'postgresql') {
445
+ // Reconstruct from information_schema
446
+ const cols = await connection.query(`
447
+ SELECT column_name, data_type, is_nullable, column_default
448
+ FROM information_schema.columns
449
+ WHERE table_name = '${tableName}'
450
+ ORDER BY ordinal_position
451
+ `);
452
+ let sql = `CREATE TABLE ${tableName} (\n`;
453
+ cols.forEach((c, i) => {
454
+ sql += ` ${c.column_name} ${c.data_type}`;
455
+ if (c.is_nullable === 'NO') sql += ' NOT NULL';
456
+ if (c.column_default) sql += ` DEFAULT ${c.column_default}`;
457
+ if (i < cols.length - 1) sql += ',';
458
+ sql += '\n';
459
+ });
460
+ sql += ');';
461
+ return sql;
462
+ }
463
+ if (driver === 'sqlite') {
464
+ const rows = await connection.query(
465
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name='${tableName}'`
466
+ );
467
+ return rows[0].sql;
468
+ }
469
+ throw new Error(`Unsupported driver: ${driver}`);
470
+ }
471
+
472
+ async function fetchTableRows(connection, driver, tableName) {
473
+ const query = driver === 'mysql'
474
+ ? `SELECT * FROM \`${tableName}\``
475
+ : `SELECT * FROM "${tableName}"`;
476
+ return connection.query(query);
477
+ }
478
+
479
+ // ─── Interactive reverse from database ────────────────────────────────────────
480
+
481
+ async function reverseFromDatabase() {
482
+ console.log('\n🔄 Reverse-engineering a live database\n');
483
+
484
+ const dbConfig = await getDatabaseConfig();
485
+
486
+ const { DatabaseConnection } = require('../src/index.js');
487
+ const connection = new DatabaseConnection(dbConfig);
488
+
489
+ console.log('\n⏳ Connecting…');
490
+ await connection.connect();
491
+ console.log('✅ Connected!\n');
492
+
493
+ const tables = await fetchTablesList(connection, dbConfig.driver);
494
+ if (!tables.length) {
495
+ console.error('❌ No tables found.');
496
+ await connection.close();
497
+ return;
498
+ }
499
+
500
+ console.log(`📋 ${tables.length} table(s) found:\n`);
501
+ tables.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
502
+ console.log('');
503
+
504
+ const migDir = (await question('Migration output dir (default: ./database/migrations): ')) || './database/migrations';
505
+ const seedDir = (await question('Seeder output dir (default: ./database/seeds): ')) || './database/seeds';
506
+ const seedRows = (await question('Generate seeders with actual row data? (y/N): ')).trim().toLowerCase() === 'y';
507
+
508
+ fs.mkdirSync(migDir, { recursive: true });
509
+ fs.mkdirSync(seedDir, { recursive: true });
510
+
511
+ console.log('\n🔍 Generating migrations…\n');
512
+ for (const tableName of tables) {
513
+ const sql = await fetchCreateTableSql(connection, dbConfig.driver, tableName);
514
+ const tableInfo = parseCreateTable(sql);
515
+ if (!tableInfo) { console.warn(` ⚠️ Could not parse schema for: ${tableName}`); continue; }
516
+
517
+ const mig = generateMigration(tableInfo);
518
+ fs.writeFileSync(path.join(migDir, mig.filename), mig.code, 'utf8');
519
+ console.log(` ✅ ${mig.filename}`);
520
+
521
+ if (seedRows) {
522
+ const rows = await fetchTableRows(connection, dbConfig.driver, tableName);
523
+ const seeder = generateSeeder(tableName, rows);
524
+ fs.writeFileSync(path.join(seedDir, seeder.filename), seeder.code, 'utf8');
525
+ console.log(` 🌱 ${seeder.filename} (${rows.length} row${rows.length !== 1 ? 's' : ''})`);
526
+ }
527
+ }
528
+
529
+ console.log(`\n✨ Done! Migrations → ${migDir}${seedRows ? ` Seeders → ${seedDir}` : ''}\n`);
530
+ await connection.close();
531
+ }
532
+
533
+ // ─── Interactive reverse from SQL file ────────────────────────────────────────
534
+
535
+ async function reverseFromFile() {
536
+ console.log('\n📄 Reverse-engineering from a SQL file\n');
537
+
538
+ const sqlFile = (await question('Path to SQL file: ')).trim();
539
+ if (!fs.existsSync(sqlFile)) {
540
+ console.error(`❌ File not found: ${sqlFile}`);
541
+ return;
542
+ }
543
+
544
+ const sql = fs.readFileSync(sqlFile, 'utf8');
545
+ const migDir = (await question('Migration output dir (default: ./database/migrations): ')) || './database/migrations';
546
+ fs.mkdirSync(migDir, { recursive: true });
547
+
548
+ const migrations = reverseFromSql(sql);
549
+ if (!migrations.length) {
550
+ console.error('❌ No CREATE TABLE statements found.');
551
+ return;
552
+ }
553
+
554
+ console.log(`\n✅ ${migrations.length} migration(s) generated:\n`);
555
+ for (const mig of migrations) {
556
+ fs.writeFileSync(path.join(migDir, mig.filename), mig.code, 'utf8');
557
+ console.log(` ✅ ${mig.filename}`);
558
+ }
559
+
560
+ console.log(`\n✨ Done! Migrations → ${migDir}\n`);
561
+ }
562
+
563
+ // ─── Main CLI ─────────────────────────────────────────────────────────────────
564
+
565
+ async function main() {
566
+ console.log('\n╔══════════════════════════════════════════╗');
567
+ console.log('║ Outlet ORM — Database Reverse Tool ║');
568
+ console.log('╚══════════════════════════════════════════╝\n');
569
+ console.log(' 1. Reverse from SQL file → Migrations');
570
+ console.log(' 2. Reverse from database → Migrations + Seeders');
571
+ console.log(' 3. Quit\n');
572
+
573
+ const choice = (await question('Your choice: ')).trim();
574
+
575
+ switch (choice) {
576
+ case '1': await reverseFromFile(); break;
577
+ case '2': await reverseFromDatabase(); break;
578
+ case '3': console.log('Goodbye! 👋\n'); break;
579
+ default: console.log('❌ Invalid choice.\n');
580
+ }
581
+
582
+ if (rl) rl.close();
583
+ }
584
+
585
+ // ─── Exports (for testing) ────────────────────────────────────────────────────
586
+
587
+ if (require.main === module) {
588
+ main().catch(err => {
589
+ console.error('❌ Fatal:', err.message);
590
+ if (rl) rl.close();
591
+ process.exit(1);
592
+ });
593
+ } else {
594
+ module.exports = {
595
+ parseCreateTable,
596
+ splitDefinitions,
597
+ columnToBlueprint,
598
+ generateMigration,
599
+ generateSeeder,
600
+ reverseFromSql,
601
+ };
602
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "outlet-orm",
3
- "version": "4.2.1",
3
+ "version": "5.5.1",
4
4
  "description": "A Laravel Eloquent-inspired ORM for Node.js with support for MySQL, PostgreSQL, and SQLite",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
7
7
  "bin": {
8
- "outlet-init": "./bin/init.js",
9
- "outlet-convert": "./bin/convert.js",
10
- "outlet-migrate": "./bin/migrate.js"
8
+ "outlet-init": "bin/init.js",
9
+ "outlet-convert": "bin/convert.js",
10
+ "outlet-migrate": "bin/migrate.js",
11
+ "outlet-reverse": "bin/reverse.js"
11
12
  },
12
13
  "files": [
13
14
  "src/**",
14
- "lib/**",
15
15
  "bin/**",
16
16
  "types/**",
17
17
  "README.md",
@@ -21,15 +21,17 @@
21
21
  "test": "jest",
22
22
  "test:watch": "jest --watch",
23
23
  "test:coverage": "jest --coverage",
24
- "lint": "eslint \"{src,bin,lib}/**/*.js\"",
25
- "lint:fix": "eslint \"{src,bin,lib}/**/*.js\" --fix",
24
+ "lint": "eslint \"{src,bin}/**/*.js\"",
25
+ "lint:fix": "eslint \"{src,bin}/**/*.js\" --fix",
26
26
  "migrate": "node bin/migrate.js migrate",
27
27
  "migrate:make": "node bin/migrate.js make",
28
28
  "migrate:rollback": "node bin/migrate.js rollback --steps 1",
29
29
  "migrate:status": "node bin/migrate.js status",
30
30
  "migrate:reset": "node bin/migrate.js reset --yes",
31
31
  "migrate:refresh": "node bin/migrate.js refresh --yes",
32
- "migrate:fresh": "node bin/migrate.js fresh --yes"
32
+ "migrate:fresh": "node bin/migrate.js fresh --yes",
33
+ "seed": "node bin/migrate.js seed",
34
+ "seed:make": "node bin/migrate.js make:seed"
33
35
  },
34
36
  "keywords": [
35
37
  "orm",
@@ -46,14 +48,15 @@
46
48
  "license": "MIT",
47
49
  "repository": {
48
50
  "type": "git",
49
- "url": "https://github.com/omgbwa-yasse/outlet-orm.git"
51
+ "url": "git+https://github.com/omgbwa-yasse/outlet-orm.git"
50
52
  },
51
53
  "bugs": {
52
54
  "url": "https://github.com/omgbwa-yasse/outlet-orm/issues"
53
55
  },
54
56
  "homepage": "https://github.com/omgbwa-yasse/outlet-orm#readme",
55
57
  "dependencies": {
56
- "dotenv": "^16.4.5"
58
+ "dotenv": "^16.4.5",
59
+ "pluralize": "^8.0.0"
57
60
  },
58
61
  "peerDependencies": {
59
62
  "mysql2": "^3.15.2",
@@ -61,9 +64,15 @@
61
64
  "sqlite3": "^5.1.6"
62
65
  },
63
66
  "peerDependenciesMeta": {
64
- "mysql2": { "optional": true },
65
- "pg": { "optional": true },
66
- "sqlite3": { "optional": true }
67
+ "mysql2": {
68
+ "optional": true
69
+ },
70
+ "pg": {
71
+ "optional": true
72
+ },
73
+ "sqlite3": {
74
+ "optional": true
75
+ }
67
76
  },
68
77
  "devDependencies": {
69
78
  "@types/node": "^20.10.0",
@@ -0,0 +1,4 @@
1
+ // Re-export DatabaseConnection from parent for backward compatibility
2
+ module.exports = {
3
+ DatabaseConnection: require('../DatabaseConnection')
4
+ };