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.
@@ -3,6 +3,17 @@
3
3
  * Provides a fluent interface for creating and modifying database tables
4
4
  */
5
5
 
6
+ function quoteIdentifier(identifier) {
7
+ if (!identifier || typeof identifier !== 'string') {
8
+ throw new Error('Invalid SQL identifier');
9
+ }
10
+ // Strict allowlist: only alphanumeric and underscore — no fallback, no blocklist.
11
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
12
+ throw new Error('Invalid SQL identifier');
13
+ }
14
+ return `\`${identifier}\``;
15
+ }
16
+
6
17
  class Schema {
7
18
  constructor(connection) {
8
19
  this.connection = connection;
@@ -51,16 +62,16 @@ class Schema {
51
62
  let sql;
52
63
 
53
64
  switch (driver) {
54
- case 'mysql':
55
- sql = `RENAME TABLE ${from} TO ${to}`;
56
- break;
57
- case 'postgres':
58
- case 'postgresql':
59
- case 'sqlite':
60
- sql = `ALTER TABLE ${from} RENAME TO ${to}`;
61
- break;
62
- default:
63
- throw new Error(`Unsupported driver: ${driver}`);
65
+ case 'mysql':
66
+ sql = `RENAME TABLE ${quoteIdentifier(from)} TO ${quoteIdentifier(to)}`;
67
+ break;
68
+ case 'postgres':
69
+ case 'postgresql':
70
+ case 'sqlite':
71
+ sql = `ALTER TABLE ${quoteIdentifier(from)} RENAME TO ${quoteIdentifier(to)}`;
72
+ break;
73
+ default:
74
+ throw new Error(`Unsupported driver: ${driver}`);
64
75
  }
65
76
 
66
77
  await this.connection.execute(sql);
@@ -72,7 +83,7 @@ class Schema {
72
83
  * @param {string} tableName
73
84
  */
74
85
  async drop(tableName) {
75
- const sql = `DROP TABLE ${tableName}`;
86
+ const sql = `DROP TABLE ${quoteIdentifier(tableName)}`;
76
87
  await this.connection.execute(sql);
77
88
  console.log(`✓ Table '${tableName}' dropped successfully`);
78
89
  }
@@ -82,7 +93,7 @@ class Schema {
82
93
  * @param {string} tableName
83
94
  */
84
95
  async dropIfExists(tableName) {
85
- const sql = `DROP TABLE IF EXISTS ${tableName}`;
96
+ const sql = `DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}`;
86
97
  await this.connection.execute(sql);
87
98
  console.log(`✓ Table '${tableName}' dropped if existed`);
88
99
  }
@@ -95,26 +106,30 @@ class Schema {
95
106
  async hasTable(tableName) {
96
107
  const driver = this.connection.config.driver;
97
108
  let sql;
109
+ let params = [];
98
110
 
99
111
  switch (driver) {
100
- case 'mysql':
101
- sql = `SELECT COUNT(*) as count FROM information_schema.tables
102
- WHERE table_schema = DATABASE() AND table_name = '${tableName}'`;
103
- break;
104
- case 'postgres':
105
- case 'postgresql':
106
- sql = `SELECT COUNT(*) as count FROM information_schema.tables
107
- WHERE table_schema = 'public' AND table_name = '${tableName}'`;
108
- break;
109
- case 'sqlite':
110
- sql = `SELECT COUNT(*) as count FROM sqlite_master
111
- WHERE type='table' AND name='${tableName}'`;
112
- break;
113
- default:
114
- throw new Error(`Unsupported driver: ${driver}`);
112
+ case 'mysql':
113
+ sql = `SELECT COUNT(*) as count FROM information_schema.tables
114
+ WHERE table_schema = DATABASE() AND table_name = ?`;
115
+ params = [tableName];
116
+ break;
117
+ case 'postgres':
118
+ case 'postgresql':
119
+ sql = `SELECT COUNT(*) as count FROM information_schema.tables
120
+ WHERE table_schema = 'public' AND table_name = $1`;
121
+ params = [tableName];
122
+ break;
123
+ case 'sqlite':
124
+ sql = `SELECT COUNT(*) as count FROM sqlite_master
125
+ WHERE type='table' AND name=?`;
126
+ params = [tableName];
127
+ break;
128
+ default:
129
+ throw new Error(`Unsupported driver: ${driver}`);
115
130
  }
116
131
 
117
- const result = await this.connection.execute(sql);
132
+ const result = await this.connection.execute(sql, params);
118
133
  return result[0].count > 0;
119
134
  }
120
135
 
@@ -127,30 +142,34 @@ class Schema {
127
142
  async hasColumn(tableName, columnName) {
128
143
  const driver = this.connection.config.driver;
129
144
  let sql;
145
+ let params = [];
130
146
 
131
147
  switch (driver) {
132
- case 'mysql':
133
- sql = `SELECT COUNT(*) as count FROM information_schema.columns
148
+ case 'mysql':
149
+ sql = `SELECT COUNT(*) as count FROM information_schema.columns
134
150
  WHERE table_schema = DATABASE()
135
- AND table_name = '${tableName}'
136
- AND column_name = '${columnName}'`;
137
- break;
138
- case 'postgres':
139
- case 'postgresql':
140
- sql = `SELECT COUNT(*) as count FROM information_schema.columns
151
+ AND table_name = ?
152
+ AND column_name = ?`;
153
+ params = [tableName, columnName];
154
+ break;
155
+ case 'postgres':
156
+ case 'postgresql':
157
+ sql = `SELECT COUNT(*) as count FROM information_schema.columns
141
158
  WHERE table_schema = 'public'
142
- AND table_name = '${tableName}'
143
- AND column_name = '${columnName}'`;
144
- break;
145
- case 'sqlite':
146
- sql = `SELECT COUNT(*) as count FROM pragma_table_info('${tableName}')
147
- WHERE name = '${columnName}'`;
148
- break;
149
- default:
150
- throw new Error(`Unsupported driver: ${driver}`);
159
+ AND table_name = $1
160
+ AND column_name = $2`;
161
+ params = [tableName, columnName];
162
+ break;
163
+ case 'sqlite':
164
+ sql = `SELECT COUNT(*) as count FROM pragma_table_info(?)
165
+ WHERE name = ?`;
166
+ params = [tableName, columnName];
167
+ break;
168
+ default:
169
+ throw new Error(`Unsupported driver: ${driver}`);
151
170
  }
152
171
 
153
- const result = await this.connection.execute(sql);
172
+ const result = await this.connection.execute(sql, params);
154
173
  return result[0].count > 0;
155
174
  }
156
175
  }
@@ -333,6 +352,12 @@ class Blueprint {
333
352
  const column = new ColumnDefinition(columnName, 'BIGINT');
334
353
  column.unsigned();
335
354
  this.columns.push(column);
355
+
356
+ column.constrained = (table = null) => {
357
+ const foreignKey = this.foreign(columnName);
358
+ return foreignKey.constrained(table);
359
+ };
360
+
336
361
  return column;
337
362
  }
338
363
 
@@ -453,7 +478,7 @@ class Blueprint {
453
478
  const columnDefinitions = this.columns.map(col => col.toSql(driver)).join(',\n ');
454
479
  const constraints = this.getConstraints();
455
480
 
456
- let sql = `CREATE TABLE ${this.tableName} (\n ${columnDefinitions}`;
481
+ let sql = `CREATE TABLE ${quoteIdentifier(this.tableName)} (\n ${columnDefinitions}`;
457
482
 
458
483
  if (constraints) {
459
484
  sql += `,\n ${constraints}`;
@@ -474,61 +499,65 @@ class Blueprint {
474
499
  // Add new columns
475
500
  const driver = this.connection.config.driver;
476
501
  for (const column of this.columns) {
477
- let sql = `ALTER TABLE ${this.tableName} ADD COLUMN ${column.toSql(driver)}`;
502
+ let sql = `ALTER TABLE ${quoteIdentifier(this.tableName)} ADD COLUMN ${column.toSql(driver)}`;
478
503
  statements.push(sql);
479
504
  }
480
505
 
481
506
  // Process commands
482
507
  for (const command of this.commands) {
483
508
  switch (command.type) {
484
- case 'dropColumn':
485
- for (const col of command.columns) {
486
- statements.push(`ALTER TABLE ${this.tableName} DROP COLUMN ${col}`);
487
- }
488
- break;
489
-
490
- case 'renameColumn':
491
- statements.push(`ALTER TABLE ${this.tableName} CHANGE ${command.from} ${command.to}`);
492
- break;
493
-
494
- case 'foreign': {
495
- const fk = command.foreignKey;
496
- statements.push(
497
- `ALTER TABLE ${this.tableName} ADD CONSTRAINT ${fk.name} ` +
498
- `FOREIGN KEY (${fk.column}) REFERENCES ${fk.references.table}(${fk.references.column})` +
499
- (fk.onDelete ? ` ON DELETE ${fk.onDelete}` : '') +
500
- (fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : '')
501
- );
502
- break;
509
+ case 'dropColumn':
510
+ for (const col of command.columns) {
511
+ statements.push(`ALTER TABLE ${quoteIdentifier(this.tableName)} DROP COLUMN ${quoteIdentifier(col)}`);
503
512
  }
513
+ break;
504
514
 
505
- case 'dropForeign': {
506
- const fkName = `${this.tableName}_${command.columns.join('_')}_foreign`;
507
- statements.push(`ALTER TABLE ${this.tableName} DROP FOREIGN KEY ${fkName}`);
508
- break;
515
+ case 'renameColumn':
516
+ if (driver === 'mysql') {
517
+ statements.push(`ALTER TABLE ${quoteIdentifier(this.tableName)} RENAME COLUMN ${quoteIdentifier(command.from)} TO ${quoteIdentifier(command.to)}`);
518
+ } else {
519
+ statements.push(`ALTER TABLE ${quoteIdentifier(this.tableName)} RENAME COLUMN ${quoteIdentifier(command.from)} TO ${quoteIdentifier(command.to)}`);
509
520
  }
521
+ break;
522
+
523
+ case 'foreign': {
524
+ const fk = command.foreignKey;
525
+ statements.push(
526
+ `ALTER TABLE ${quoteIdentifier(this.tableName)} ADD CONSTRAINT ${quoteIdentifier(fk.name)} ` +
527
+ `FOREIGN KEY (${quoteIdentifier(fk.column)}) REFERENCES ${quoteIdentifier(fk._ref.table)}(${quoteIdentifier(fk._ref.column)})` +
528
+ (fk._onDelete ? ` ON DELETE ${fk._onDelete}` : '') +
529
+ (fk._onUpdate ? ` ON UPDATE ${fk._onUpdate}` : '')
530
+ );
531
+ break;
532
+ }
533
+
534
+ case 'dropForeign': {
535
+ const fkName = `${this.tableName}_${command.columns.join('_')}_foreign`;
536
+ statements.push(`ALTER TABLE ${quoteIdentifier(this.tableName)} DROP FOREIGN KEY ${quoteIdentifier(fkName)}`);
537
+ break;
538
+ }
510
539
 
511
- case 'index':
512
- statements.push(
513
- `ALTER TABLE ${this.tableName} ADD INDEX ${command.name} (${command.columns.join(', ')})`
514
- );
515
- break;
516
-
517
- case 'unique':
518
- statements.push(
519
- `ALTER TABLE ${this.tableName} ADD UNIQUE ${command.name} (${command.columns.join(', ')})`
520
- );
521
- break;
522
-
523
- case 'fulltext':
524
- statements.push(
525
- `ALTER TABLE ${this.tableName} ADD FULLTEXT ${command.name} (${command.columns.join(', ')})`
526
- );
527
- break;
528
-
529
- case 'dropIndex':
530
- statements.push(`ALTER TABLE ${this.tableName} DROP INDEX ${command.name}`);
531
- break;
540
+ case 'index':
541
+ statements.push(
542
+ `ALTER TABLE ${quoteIdentifier(this.tableName)} ADD INDEX ${quoteIdentifier(command.name)} (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`
543
+ );
544
+ break;
545
+
546
+ case 'unique':
547
+ statements.push(
548
+ `ALTER TABLE ${quoteIdentifier(this.tableName)} ADD UNIQUE ${quoteIdentifier(command.name)} (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`
549
+ );
550
+ break;
551
+
552
+ case 'fulltext':
553
+ statements.push(
554
+ `ALTER TABLE ${quoteIdentifier(this.tableName)} ADD FULLTEXT ${quoteIdentifier(command.name)} (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`
555
+ );
556
+ break;
557
+
558
+ case 'dropIndex':
559
+ statements.push(`ALTER TABLE ${quoteIdentifier(this.tableName)} DROP INDEX ${quoteIdentifier(command.name)}`);
560
+ break;
532
561
  }
533
562
  }
534
563
 
@@ -548,7 +577,7 @@ class Blueprint {
548
577
  // so skip table-level PRIMARY KEY constraints to avoid duplication.
549
578
  const hasSqliteAutoInc = driver === 'sqlite' && this.columns.some(col => col.isAutoIncrement);
550
579
  if (primaryKeys.length > 0 && !hasSqliteAutoInc) {
551
- const pkColumns = primaryKeys.map(col => col.name).join(', ');
580
+ const pkColumns = primaryKeys.map(col => quoteIdentifier(col.name)).join(', ');
552
581
  constraints.push(`PRIMARY KEY (${pkColumns})`);
553
582
  }
554
583
 
@@ -556,26 +585,26 @@ class Blueprint {
556
585
  for (const command of this.commands) {
557
586
  if (command.type === 'foreign') {
558
587
  const fk = command.foreignKey;
559
- let constraint = `CONSTRAINT ${fk.name} FOREIGN KEY (${fk.column}) ` +
560
- `REFERENCES ${fk.references.table}(${fk.references.column})`;
588
+ let constraint = `CONSTRAINT ${quoteIdentifier(fk.name)} FOREIGN KEY (${quoteIdentifier(fk.column)}) ` +
589
+ `REFERENCES ${quoteIdentifier(fk._ref.table)}(${quoteIdentifier(fk._ref.column)})`;
561
590
 
562
- if (fk.onDelete) {
563
- constraint += ` ON DELETE ${fk.onDelete}`;
591
+ if (fk._onDelete) {
592
+ constraint += ` ON DELETE ${fk._onDelete}`;
564
593
  }
565
- if (fk.onUpdate) {
566
- constraint += ` ON UPDATE ${fk.onUpdate}`;
594
+ if (fk._onUpdate) {
595
+ constraint += ` ON UPDATE ${fk._onUpdate}`;
567
596
  }
568
597
 
569
598
  constraints.push(constraint);
570
599
  } else if (command.type === 'unique') {
571
600
  if (driver === 'sqlite') {
572
- constraints.push(`UNIQUE (${command.columns.join(', ')})`);
601
+ constraints.push(`UNIQUE (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`);
573
602
  } else {
574
- constraints.push(`UNIQUE KEY ${command.name} (${command.columns.join(', ')})`);
603
+ constraints.push(`UNIQUE KEY ${quoteIdentifier(command.name)} (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`);
575
604
  }
576
605
  } else if (command.type === 'index') {
577
606
  if (driver !== 'sqlite') {
578
- constraints.push(`KEY ${command.name} (${command.columns.join(', ')})`);
607
+ constraints.push(`KEY ${quoteIdentifier(command.name)} (${command.columns.map(c => quoteIdentifier(c)).join(', ')})`);
579
608
  }
580
609
  }
581
610
  }
@@ -664,7 +693,7 @@ class ColumnDefinition {
664
693
  * Generate SQL for this column
665
694
  */
666
695
  toSql(driver = 'mysql') {
667
- let sql = `${this.name} ${this.getTypeDefinition(driver)}`;
696
+ let sql = `${quoteIdentifier(this.name)} ${this.getTypeDefinition(driver)}`;
668
697
 
669
698
  if (this.isUnsigned && ['INT', 'BIGINT', 'TINYINT'].includes(this.type) && driver !== 'sqlite') {
670
699
  sql += ' UNSIGNED';
@@ -677,7 +706,7 @@ class ColumnDefinition {
677
706
  if (this.isAutoIncrement) {
678
707
  if (driver === 'sqlite') {
679
708
  // In SQLite, autoincrement must be declared as INTEGER PRIMARY KEY AUTOINCREMENT
680
- sql = `${this.name} INTEGER PRIMARY KEY AUTOINCREMENT`;
709
+ sql = `${quoteIdentifier(this.name)} INTEGER PRIMARY KEY AUTOINCREMENT`;
681
710
  } else {
682
711
  sql += ' AUTO_INCREMENT';
683
712
  }
@@ -698,7 +727,7 @@ class ColumnDefinition {
698
727
  }
699
728
 
700
729
  if (this.commentText) {
701
- sql += ` COMMENT '${this.commentText}'`;
730
+ sql += ` COMMENT '${this.commentText.replace(/'/g, '\'\'')}'`;
702
731
  }
703
732
 
704
733
  return sql;
@@ -718,7 +747,7 @@ class ColumnDefinition {
718
747
  return precision ? `FLOAT(${precision}, ${scale})` : 'FLOAT';
719
748
  case 'ENUM': {
720
749
  if (driver === 'sqlite') return 'TEXT';
721
- const enumValues = values.map(v => `'${v}'`).join(', ');
750
+ const enumValues = values.map(v => `'${v.replace(/'/g, '\'\'')}'`).join(', ');
722
751
  return `ENUM(${enumValues})`;
723
752
  }
724
753
  default:
@@ -728,7 +757,7 @@ class ColumnDefinition {
728
757
 
729
758
  formatDefaultValue() {
730
759
  if (typeof this.defaultValue === 'string') {
731
- return `'${this.defaultValue}'`;
760
+ return `'${this.defaultValue.replace(/'/g, '\'\'')}'`;
732
761
  }
733
762
  return this.defaultValue;
734
763
  }
@@ -740,41 +769,52 @@ class ColumnDefinition {
740
769
  class ForeignKeyDefinition {
741
770
  constructor(column) {
742
771
  this.column = column;
743
- this.references = { table: null, column: 'id' };
744
- this.onDelete = null;
745
- this.onUpdate = null;
772
+ this._ref = { table: null, column: 'id' };
773
+ this._onDelete = null;
774
+ this._onUpdate = null;
746
775
  this.name = null;
747
776
  }
748
777
 
749
778
  references(column) {
750
- this.references.column = column;
779
+ this._ref.column = column;
751
780
  return this;
752
781
  }
753
782
 
754
783
  on(table) {
755
- this.references.table = table;
784
+ this._ref.table = table;
756
785
  this.name = `${table}_${this.column}_foreign`;
757
786
  return this;
758
787
  }
759
788
 
760
789
  constrained(table = null) {
761
790
  if (table) {
762
- this.references.table = table;
791
+ this._ref.table = table;
763
792
  } else {
764
793
  // Infer table name from column name (remove _id suffix)
765
- this.references.table = this.column.replace(/_id$/, '') + 's';
794
+ const pluralize = require('pluralize');
795
+ this._ref.table = pluralize(this.column.replace(/_id$/, ''));
766
796
  }
767
- this.name = `${this.references.table}_${this.column}_foreign`;
797
+ this.name = `${this._ref.table}_${this.column}_foreign`;
768
798
  return this;
769
799
  }
770
800
 
771
801
  onDelete(action) {
772
- this.onDelete = action.toUpperCase();
802
+ const ALLOWED_FK_ACTIONS = ['CASCADE', 'RESTRICT', 'SET NULL', 'NO ACTION', 'SET DEFAULT'];
803
+ const normalized = action.toUpperCase();
804
+ if (!ALLOWED_FK_ACTIONS.includes(normalized)) {
805
+ throw new Error(`Invalid foreign key action: "${normalized}". Allowed: ${ALLOWED_FK_ACTIONS.join(', ')}`);
806
+ }
807
+ this._onDelete = normalized;
773
808
  return this;
774
809
  }
775
810
 
776
811
  onUpdate(action) {
777
- this.onUpdate = action.toUpperCase();
812
+ const ALLOWED_FK_ACTIONS = ['CASCADE', 'RESTRICT', 'SET NULL', 'NO ACTION', 'SET DEFAULT'];
813
+ const normalized = action.toUpperCase();
814
+ if (!ALLOWED_FK_ACTIONS.includes(normalized)) {
815
+ throw new Error(`Invalid foreign key action: "${normalized}". Allowed: ${ALLOWED_FK_ACTIONS.join(', ')}`);
816
+ }
817
+ this._onUpdate = normalized;
778
818
  return this;
779
819
  }
780
820
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Base Seeder Class
3
+ * All seeders should extend this class
4
+ */
5
+
6
+ function assertTableName(table) {
7
+ if (typeof table !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
8
+ throw new Error(`Invalid table name: "${table}"`);
9
+ }
10
+ return table;
11
+ }
12
+
13
+ class Seeder {
14
+ constructor(connection, manager = null) {
15
+ this.connection = connection;
16
+ this.manager = manager;
17
+ }
18
+
19
+ async run() {
20
+ throw new Error('Seeder run() method must be implemented');
21
+ }
22
+
23
+ async call(seeder) {
24
+ if (!this.manager) {
25
+ throw new Error('Seeder manager is required to call nested seeders');
26
+ }
27
+ await this.manager.runSeeder(seeder);
28
+ }
29
+
30
+ async insert(table, rows) {
31
+ const safeTable = assertTableName(table);
32
+
33
+ if (Array.isArray(rows)) {
34
+ if (rows.length === 0) return { affectedRows: 0 };
35
+ return await this.connection.insertMany(safeTable, rows);
36
+ }
37
+
38
+ return await this.connection.insert(safeTable, rows);
39
+ }
40
+
41
+ async truncate(table) {
42
+ const safeTable = assertTableName(table);
43
+ const driver = this.connection?.config?.driver;
44
+
45
+ switch (driver) {
46
+ case 'mysql':
47
+ await this.connection.execute(`TRUNCATE TABLE ${safeTable}`);
48
+ break;
49
+ case 'postgres':
50
+ case 'postgresql':
51
+ case 'sqlite':
52
+ await this.connection.execute(`DELETE FROM ${safeTable}`);
53
+ break;
54
+ default:
55
+ throw new Error(`Unsupported driver for truncate: ${driver}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ module.exports = Seeder;
@@ -0,0 +1,105 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+
4
+ class SeederManager {
5
+ constructor(connection, seedsPath = './database/seeds') {
6
+ this.connection = connection;
7
+ this.seedsPath = path.resolve(process.cwd(), seedsPath);
8
+ }
9
+
10
+ async run(target = null) {
11
+ const files = await this.getSeederFiles();
12
+
13
+ if (files.length === 0) {
14
+ console.log('✓ No seeders found');
15
+ return;
16
+ }
17
+
18
+ const toRun = this.filterTargetSeeders(files, target);
19
+
20
+ if (toRun.length === 0) {
21
+ throw new Error(`Seeder not found: ${target}`);
22
+ }
23
+
24
+ console.log(`Running ${toRun.length} seeder(s)...\n`);
25
+
26
+ for (const file of toRun) {
27
+ await this.runSeeder(file);
28
+ }
29
+
30
+ console.log('\n✓ Seeding completed successfully');
31
+ }
32
+
33
+ filterTargetSeeders(files, target) {
34
+ if (!target) {
35
+ const databaseSeeder = files.find(file => file.toLowerCase() === 'databaseseeder.js');
36
+ return databaseSeeder ? [databaseSeeder] : files;
37
+ }
38
+
39
+ const normalizedTarget = String(target).toLowerCase();
40
+
41
+ return files.filter((file) => {
42
+ const base = path.basename(file, '.js').toLowerCase();
43
+ return file.toLowerCase() === normalizedTarget
44
+ || base === normalizedTarget
45
+ || `${base}.js` === normalizedTarget;
46
+ });
47
+ }
48
+
49
+ async runSeeder(seederRef) {
50
+ const seederPath = this.resolveSeederPath(seederRef);
51
+
52
+ try {
53
+ delete require.cache[require.resolve(seederPath)];
54
+ const ExportedSeeder = require(seederPath);
55
+ const SeederClass = ExportedSeeder?.default || ExportedSeeder;
56
+
57
+ if (typeof SeederClass !== 'function') {
58
+ throw new Error('Seeder module must export a class');
59
+ }
60
+
61
+ const seeder = new SeederClass(this.connection, this);
62
+
63
+ if (typeof seeder.run !== 'function') {
64
+ throw new Error('Seeder class must implement run()');
65
+ }
66
+
67
+ await seeder.run();
68
+ console.log(`✓ ${path.basename(seederPath)}`);
69
+ } catch (error) {
70
+ console.error(`✗ Failed to run seeder: ${path.basename(seederPath)}`);
71
+ console.error(error.message);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ resolveSeederPath(seederRef) {
77
+ if (typeof seederRef !== 'string') {
78
+ throw new Error('Seeder reference must be a file path or filename string');
79
+ }
80
+
81
+ if (path.isAbsolute(seederRef)) {
82
+ return seederRef;
83
+ }
84
+
85
+ const hasExt = path.extname(seederRef) === '.js';
86
+ const fileName = hasExt ? seederRef : `${seederRef}.js`;
87
+ return path.join(this.seedsPath, fileName);
88
+ }
89
+
90
+ async getSeederFiles() {
91
+ try {
92
+ const files = await fs.readdir(this.seedsPath);
93
+ return files
94
+ .filter(file => file.endsWith('.js'))
95
+ .sort();
96
+ } catch (error) {
97
+ if (error.code === 'ENOENT') {
98
+ return [];
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = SeederManager;
package/src/index.js CHANGED
@@ -14,10 +14,20 @@ const MorphOneRelation = require('./Relations/MorphOneRelation');
14
14
  const MorphManyRelation = require('./Relations/MorphManyRelation');
15
15
  const MorphToRelation = require('./Relations/MorphToRelation');
16
16
 
17
+ // Schema & Migrations (v5.0.0 - moved from lib/)
18
+ const { Schema, Blueprint, ColumnDefinition, ForeignKeyDefinition } = require('./Schema/Schema');
19
+ const Migration = require('./Migrations/Migration');
20
+ const MigrationManager = require('./Migrations/MigrationManager');
21
+ const Seeder = require('./Seeders/Seeder');
22
+ const SeederManager = require('./Seeders/SeederManager');
23
+
17
24
  module.exports = {
25
+ // Core
18
26
  Model,
19
27
  QueryBuilder,
20
28
  DatabaseConnection,
29
+
30
+ // Relations
21
31
  Relation,
22
32
  HasOneRelation,
23
33
  HasManyRelation,
@@ -27,5 +37,19 @@ module.exports = {
27
37
  HasOneThroughRelation,
28
38
  MorphOneRelation,
29
39
  MorphManyRelation,
30
- MorphToRelation
40
+ MorphToRelation,
41
+
42
+ // Schema Builder (v5.0.0)
43
+ Schema,
44
+ Blueprint,
45
+ ColumnDefinition,
46
+ ForeignKeyDefinition,
47
+
48
+ // Migrations (v5.0.0)
49
+ Migration,
50
+ MigrationManager,
51
+
52
+ // Seeders
53
+ Seeder,
54
+ SeederManager
31
55
  };
package/types/index.d.ts CHANGED
@@ -657,4 +657,18 @@ declare module 'outlet-orm' {
657
657
  static hasTable(name: string): Promise<boolean>;
658
658
  static hasColumn(table: string, column: string): Promise<boolean>;
659
659
  }
660
+
661
+ export class Seeder {
662
+ constructor(connection: DatabaseConnection, manager?: SeederManager | null);
663
+ run(): Promise<void>;
664
+ call(seeder: string): Promise<void>;
665
+ insert(table: string, rows: Record<string, any> | Record<string, any>[]): Promise<any>;
666
+ truncate(table: string): Promise<void>;
667
+ }
668
+
669
+ export class SeederManager {
670
+ constructor(connection: DatabaseConnection, seedsPath?: string);
671
+ run(target?: string | null): Promise<void>;
672
+ runSeeder(seederRef: string): Promise<void>;
673
+ }
660
674
  }
@@ -1,4 +0,0 @@
1
- // Re-export DatabaseConnection from src for CLI usage (named export for destructuring)
2
- module.exports = {
3
- DatabaseConnection: require('../../src/DatabaseConnection')
4
- };