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/README.md +166 -53
- package/bin/init.js +18 -0
- package/bin/migrate.js +109 -7
- package/bin/reverse.js +602 -0
- package/package.json +22 -13
- package/src/Database/DatabaseConnection.js +4 -0
- package/src/DatabaseConnection.js +98 -46
- package/{lib → src}/Migrations/Migration.js +48 -48
- package/{lib → src}/Migrations/MigrationManager.js +22 -19
- package/src/Model.js +30 -7
- package/src/QueryBuilder.js +134 -35
- package/src/RawExpression.js +11 -0
- package/src/Relations/BelongsToManyRelation.js +466 -466
- package/{lib → src}/Schema/Schema.js +157 -117
- package/src/Seeders/Seeder.js +60 -0
- package/src/Seeders/SeederManager.js +105 -0
- package/src/index.js +25 -1
- package/types/index.d.ts +14 -0
- package/lib/Database/DatabaseConnection.js +0 -4
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
WHERE table_schema = DATABASE() AND table_name =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
133
|
-
|
|
148
|
+
case 'mysql':
|
|
149
|
+
sql = `SELECT COUNT(*) as count FROM information_schema.columns
|
|
134
150
|
WHERE table_schema = DATABASE()
|
|
135
|
-
AND table_name =
|
|
136
|
-
AND column_name =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 =
|
|
143
|
-
AND column_name =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
statements.push(`ALTER TABLE ${this.tableName}
|
|
508
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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.
|
|
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.
|
|
563
|
-
constraint += ` ON DELETE ${fk.
|
|
591
|
+
if (fk._onDelete) {
|
|
592
|
+
constraint += ` ON DELETE ${fk._onDelete}`;
|
|
564
593
|
}
|
|
565
|
-
if (fk.
|
|
566
|
-
constraint += ` ON UPDATE ${fk.
|
|
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.
|
|
744
|
-
this.
|
|
745
|
-
this.
|
|
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.
|
|
779
|
+
this._ref.column = column;
|
|
751
780
|
return this;
|
|
752
781
|
}
|
|
753
782
|
|
|
754
783
|
on(table) {
|
|
755
|
-
this.
|
|
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.
|
|
791
|
+
this._ref.table = table;
|
|
763
792
|
} else {
|
|
764
793
|
// Infer table name from column name (remove _id suffix)
|
|
765
|
-
|
|
794
|
+
const pluralize = require('pluralize');
|
|
795
|
+
this._ref.table = pluralize(this.column.replace(/_id$/, ''));
|
|
766
796
|
}
|
|
767
|
-
this.name = `${this.
|
|
797
|
+
this.name = `${this._ref.table}_${this.column}_foreign`;
|
|
768
798
|
return this;
|
|
769
799
|
}
|
|
770
800
|
|
|
771
801
|
onDelete(action) {
|
|
772
|
-
|
|
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
|
-
|
|
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
|
}
|