ilana-orm 1.0.9 → 1.0.12

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/cli/ilana.js CHANGED
@@ -33,7 +33,7 @@ const defaultConfig = {
33
33
  async function loadConfig() {
34
34
  const configPathJs = path.join(process.cwd(), 'ilana.config.js');
35
35
  const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
36
-
36
+
37
37
  // Try .mjs first (ES modules)
38
38
  if (fs.existsSync(configPathMjs)) {
39
39
  try {
@@ -45,7 +45,7 @@ async function loadConfig() {
45
45
  process.exit(1);
46
46
  }
47
47
  }
48
-
48
+
49
49
  // Try .js (CommonJS)
50
50
  if (fs.existsSync(configPathJs)) {
51
51
  delete require.cache[configPathJs];
@@ -61,7 +61,7 @@ async function loadConfig() {
61
61
  }
62
62
  }
63
63
  }
64
-
64
+
65
65
  // No config file found
66
66
  console.error('No ilana.config.js or ilana.config.mjs found. Run "npx ilana setup" first.');
67
67
  process.exit(1);
@@ -260,7 +260,7 @@ function generateModel(name, options = {}) {
260
260
 
261
261
  function getModelTemplate(className, tableName) {
262
262
  const isESModule = isESModuleProject();
263
-
263
+
264
264
  if (isTypeScriptProject()) {
265
265
  return `import Model from 'ilana-orm/orm/Model';
266
266
  // import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
@@ -305,7 +305,7 @@ export default class ${className} extends Model {
305
305
  }
306
306
  `;
307
307
  }
308
-
308
+
309
309
  if (isESModule) {
310
310
  return `import Model from 'ilana-orm/orm/Model';
311
311
  // import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
@@ -352,7 +352,7 @@ class ${className} extends Model {
352
352
  export default ${className};
353
353
  `;
354
354
  }
355
-
355
+
356
356
  return `const Model = require('ilana-orm/orm/Model');
357
357
  // const { MoneyCast, EncryptedCast } = require('ilana-orm/orm/CustomCasts');
358
358
 
@@ -401,7 +401,7 @@ module.exports = ${className};
401
401
 
402
402
  function getPivotModelTemplate(className, tableName) {
403
403
  const isESModule = isESModuleProject();
404
-
404
+
405
405
  if (isTypeScriptProject()) {
406
406
  return `import Model from 'ilana-orm/orm/Model';
407
407
 
@@ -415,7 +415,7 @@ export default class ${className} extends Model {
415
415
  }
416
416
  `;
417
417
  }
418
-
418
+
419
419
  if (isESModule) {
420
420
  return `import Model from 'ilana-orm/orm/Model';
421
421
 
@@ -431,7 +431,7 @@ class ${className} extends Model {
431
431
  export default ${className};
432
432
  `;
433
433
  }
434
-
434
+
435
435
  return `const Model = require('ilana-orm/orm/Model');
436
436
 
437
437
  class ${className} extends Model {
@@ -531,7 +531,206 @@ module.exports = ${className}Seeder;
531
531
  console.log(`Created seeder: seeds/${fileName}`);
532
532
  }
533
533
 
534
- // CLI Commands
534
+ function getObserverTemplate(className, modelName) {
535
+ const isESModule = isESModuleProject();
536
+
537
+ if (isTypeScriptProject()) {
538
+ const importStatement = modelName ? `import ${modelName} from '../models/${modelName}.js';\n\n` : '';
539
+ const modelType = modelName || 'any';
540
+
541
+ return `${importStatement}export default class ${className}Observer {
542
+ async creating(model: ${modelType}): Promise<void> {
543
+ // Logic before creating model
544
+ }
545
+
546
+ async created(model: ${modelType}): Promise<void> {
547
+ // Logic after creating model
548
+ }
549
+
550
+ async updating(model: ${modelType}): Promise<void> {
551
+ // Logic before updating model
552
+ }
553
+
554
+ async updated(model: ${modelType}): Promise<void> {
555
+ // Logic after updating model
556
+ }
557
+
558
+ async saving(model: ${modelType}): Promise<void> {
559
+ // Logic before saving (create or update)
560
+ }
561
+
562
+ async saved(model: ${modelType}): Promise<void> {
563
+ // Logic after saving (create or update)
564
+ }
565
+
566
+ async deleting(model: ${modelType}): Promise<void> {
567
+ // Logic before deleting model
568
+ }
569
+
570
+ async deleted(model: ${modelType}): Promise<void> {
571
+ // Logic after deleting model
572
+ }
573
+
574
+ async restoring(model: ${modelType}): Promise<void> {
575
+ // Logic before restoring soft-deleted model
576
+ }
577
+
578
+ async restored(model: ${modelType}): Promise<void> {
579
+ // Logic after restoring soft-deleted model
580
+ }
581
+ }
582
+ `;
583
+ }
584
+
585
+ if (isESModule) {
586
+ const importStatement = modelName ? `import ${modelName} from '../models/${modelName}.js';\n\n` : '';
587
+
588
+ return `${importStatement}class ${className}Observer {
589
+ async creating(model) {
590
+ // Logic before creating model
591
+ }
592
+
593
+ async created(model) {
594
+ // Logic after creating model
595
+ }
596
+
597
+ async updating(model) {
598
+ // Logic before updating model
599
+ }
600
+
601
+ async updated(model) {
602
+ // Logic after updating model
603
+ }
604
+
605
+ async saving(model) {
606
+ // Logic before saving (create or update)
607
+ }
608
+
609
+ async saved(model) {
610
+ // Logic after saving (create or update)
611
+ }
612
+
613
+ async deleting(model) {
614
+ // Logic before deleting model
615
+ }
616
+
617
+ async deleted(model) {
618
+ // Logic after deleting model
619
+ }
620
+
621
+ async restoring(model) {
622
+ // Logic before restoring soft-deleted model
623
+ }
624
+
625
+ async restored(model) {
626
+ // Logic after restoring soft-deleted model
627
+ }
628
+ }
629
+
630
+ export default ${className}Observer;
631
+ `;
632
+ }
633
+
634
+ const importStatement = modelName ? `const ${modelName} = require('../models/${modelName}');\n\n` : '';
635
+
636
+ return `${importStatement}class ${className}Observer {
637
+ async creating(model) {
638
+ // Logic before creating model
639
+ }
640
+
641
+ async created(model) {
642
+ // Logic after creating model
643
+ }
644
+
645
+ async updating(model) {
646
+ // Logic before updating model
647
+ }
648
+
649
+ async updated(model) {
650
+ // Logic after updating model
651
+ }
652
+
653
+ async saving(model) {
654
+ // Logic before saving (create or update)
655
+ }
656
+
657
+ async saved(model) {
658
+ // Logic after saving (create or update)
659
+ }
660
+
661
+ async deleting(model) {
662
+ // Logic before deleting model
663
+ }
664
+
665
+ async deleted(model) {
666
+ // Logic after deleting model
667
+ }
668
+
669
+ async restoring(model) {
670
+ // Logic before restoring soft-deleted model
671
+ }
672
+
673
+ async restored(model) {
674
+ // Logic after restoring soft-deleted model
675
+ }
676
+ }
677
+
678
+ module.exports = ${className}Observer;
679
+ `;
680
+ }
681
+
682
+ function getCastTemplate(className) {
683
+ const isESModule = isESModuleProject();
684
+
685
+ if (isTypeScriptProject()) {
686
+ return `export default class ${className}Cast {
687
+ get(value: any): any {
688
+ // Transform value when retrieving from database
689
+ return value;
690
+ }
691
+
692
+ set(value: any): any {
693
+ // Transform value when storing to database
694
+ return value;
695
+ }
696
+ }
697
+ `;
698
+ }
699
+
700
+ if (isESModule) {
701
+ return `class ${className}Cast {
702
+ get(value) {
703
+ // Transform value when retrieving from database
704
+ return value;
705
+ }
706
+
707
+ set(value) {
708
+ // Transform value when storing to database
709
+ return value;
710
+ }
711
+ }
712
+
713
+ export default ${className}Cast;
714
+ `;
715
+ }
716
+
717
+ return `class ${className}Cast {
718
+ get(value) {
719
+ // Transform value when retrieving from database
720
+ return value;
721
+ }
722
+
723
+ set(value) {
724
+ // Transform value when storing to database
725
+ return value;
726
+ }
727
+ }
728
+
729
+ module.exports = ${className}Cast;
730
+ `;
731
+ }
732
+
733
+
535
734
  const commands = {
536
735
  async setup() {
537
736
  console.log('Setting up Ilana ORM...');
@@ -548,7 +747,7 @@ const commands = {
548
747
  // Create config file if it doesn't exist
549
748
  const isESModule = isESModuleProject();
550
749
  const configPath = isESModule ? 'ilana.config.mjs' : 'ilana.config.js';
551
-
750
+
552
751
  if (!fs.existsSync(configPath)) {
553
752
  const configTemplate = isESModule ? getESModuleConfigTemplate() : getCommonJSConfigTemplate();
554
753
  fs.writeFileSync(configPath, configTemplate);
@@ -699,6 +898,268 @@ DB_TIMEZONE=UTC
699
898
  generateModel(name, options);
700
899
  },
701
900
 
901
+ async migrate(...args) {
902
+ await initializeDatabase();
903
+ const runner = new MigrationRunner();
904
+
905
+ let connection;
906
+ let onlyFile;
907
+ let toFile;
908
+
909
+ for (let i = 0; i < args.length; i++) {
910
+ const arg = args[i];
911
+ if (arg === '--connection' && args[i + 1]) {
912
+ connection = args[i + 1];
913
+ i++;
914
+ } else if (arg.startsWith('--connection=')) {
915
+ connection = arg.split('=')[1];
916
+ } else if (arg === '--only' && args[i + 1]) {
917
+ onlyFile = args[i + 1];
918
+ i++;
919
+ } else if (arg.startsWith('--only=')) {
920
+ onlyFile = arg.split('=')[1];
921
+ } else if (arg === '--to' && args[i + 1]) {
922
+ toFile = args[i + 1];
923
+ i++;
924
+ } else if (arg.startsWith('--to=')) {
925
+ toFile = arg.split('=')[1];
926
+ } else if (!arg.startsWith('--')) {
927
+ connection = arg;
928
+ }
929
+ }
930
+
931
+ await runner.migrate(connection, onlyFile, toFile);
932
+ process.exit(0);
933
+ },
934
+
935
+ async 'migrate:fresh'(...args) {
936
+ await initializeDatabase();
937
+ const runner = new MigrationRunner();
938
+
939
+ let connection;
940
+ let withSeed = false;
941
+
942
+ for (const arg of args) {
943
+ if (arg === '--seed') {
944
+ withSeed = true;
945
+ } else if (arg.startsWith('--connection=')) {
946
+ connection = arg.split('=')[1];
947
+ } else if (!arg.startsWith('--')) {
948
+ connection = arg;
949
+ }
950
+ }
951
+
952
+ await runner.fresh(connection);
953
+
954
+ if (withSeed) {
955
+ await commands.seed();
956
+ }
957
+
958
+ process.exit(0);
959
+ },
960
+
961
+ async 'migrate:list'(connection) {
962
+ await initializeDatabase();
963
+ const runner = new MigrationRunner();
964
+ await runner.list(connection);
965
+ process.exit(0);
966
+ },
967
+
968
+ async 'migrate:unlock'(connection) {
969
+ await initializeDatabase();
970
+ const runner = new MigrationRunner();
971
+ await runner.unlock(connection);
972
+ process.exit(0);
973
+ },
974
+
975
+ async 'migrate:rollback'(...args) {
976
+ await initializeDatabase();
977
+ const runner = new MigrationRunner();
978
+
979
+ let steps = 1;
980
+ let connection;
981
+ let toFile;
982
+
983
+ for (let i = 0; i < args.length; i++) {
984
+ const arg = args[i];
985
+ if (arg === '--step' && args[i + 1]) {
986
+ steps = parseInt(args[i + 1]);
987
+ i++;
988
+ } else if (arg.startsWith('--step=')) {
989
+ steps = parseInt(arg.split('=')[1]);
990
+ } else if (arg === '--connection' && args[i + 1]) {
991
+ connection = args[i + 1];
992
+ i++;
993
+ } else if (arg.startsWith('--connection=')) {
994
+ connection = arg.split('=')[1];
995
+ } else if (arg === '--to' && args[i + 1]) {
996
+ toFile = args[i + 1];
997
+ i++;
998
+ } else if (arg.startsWith('--to=')) {
999
+ toFile = arg.split('=')[1];
1000
+ } else if (!isNaN(parseInt(arg))) {
1001
+ steps = parseInt(arg);
1002
+ } else if (!arg.startsWith('--')) {
1003
+ connection = arg;
1004
+ }
1005
+ }
1006
+
1007
+ await runner.rollback(steps, connection, toFile);
1008
+ process.exit(0);
1009
+ },
1010
+
1011
+ async 'migrate:reset'(connection) {
1012
+ await initializeDatabase();
1013
+ const runner = new MigrationRunner();
1014
+ await runner.reset(connection);
1015
+ process.exit(0);
1016
+ },
1017
+
1018
+ async 'migrate:refresh'(connection) {
1019
+ await initializeDatabase();
1020
+ const runner = new MigrationRunner();
1021
+ await runner.refresh(connection);
1022
+ process.exit(0);
1023
+ },
1024
+
1025
+ async 'migrate:status'(connection) {
1026
+ await initializeDatabase();
1027
+ const runner = new MigrationRunner();
1028
+ await runner.status(connection);
1029
+ process.exit(0);
1030
+ },
1031
+
1032
+ async seed(...args) {
1033
+ await initializeDatabase();
1034
+
1035
+ let seederName;
1036
+ let connection;
1037
+
1038
+ for (let i = 0; i < args.length; i++) {
1039
+ const arg = args[i];
1040
+ if (arg === '--class' && args[i + 1]) {
1041
+ seederName = args[i + 1];
1042
+ i++;
1043
+ } else if (arg.startsWith('--class=')) {
1044
+ seederName = arg.split('=')[1];
1045
+ } else if (arg === '--connection' && args[i + 1]) {
1046
+ connection = args[i + 1];
1047
+ i++;
1048
+ } else if (arg.startsWith('--connection=')) {
1049
+ connection = arg.split('=')[1];
1050
+ } else if (!arg.startsWith('--')) {
1051
+ if (!seederName) seederName = arg;
1052
+ else connection = arg;
1053
+ }
1054
+ }
1055
+
1056
+ const seedsPath = path.join(process.cwd(), 'database/seeds');
1057
+ if (!fs.existsSync(seedsPath)) {
1058
+ console.log('No seeds directory found');
1059
+ process.exit(0);
1060
+ }
1061
+
1062
+ const seedFiles = fs.readdirSync(seedsPath)
1063
+ .filter(file => file.endsWith('.ts') || file.endsWith('.js'))
1064
+ .sort();
1065
+
1066
+ if (seedFiles.length === 0) {
1067
+ console.log('No seed files found');
1068
+ process.exit(0);
1069
+ }
1070
+
1071
+ const filesToRun = seederName
1072
+ ? seedFiles.filter(file => file.includes(seederName))
1073
+ : seedFiles;
1074
+
1075
+ console.log(`Running ${filesToRun.length} seeders...`);
1076
+
1077
+ for (const file of filesToRun) {
1078
+ console.log(`Seeding: ${file}`);
1079
+ const filepath = path.join(seedsPath, file);
1080
+ delete require.cache[filepath];
1081
+ const seederModule = require(filepath);
1082
+ const SeederClass = seederModule.default || seederModule;
1083
+ const seeder = new SeederClass();
1084
+
1085
+ if (connection) {
1086
+ seeder.connection = connection;
1087
+ }
1088
+
1089
+ if (typeof seeder.run === 'function') {
1090
+ await seeder.run();
1091
+ }
1092
+
1093
+ console.log(`Seeded: ${file}`);
1094
+ }
1095
+
1096
+ console.log('Seeding completed');
1097
+ process.exit(0);
1098
+ },
1099
+
1100
+ async 'db:seed'(seederName) {
1101
+ return commands.seed(seederName);
1102
+ },
1103
+
1104
+ async 'db:wipe'(connection) {
1105
+ await initializeDatabase();
1106
+ const runner = new MigrationRunner();
1107
+ await runner.wipe(connection);
1108
+ process.exit(0);
1109
+ },
1110
+
1111
+ async 'make:observer'(name, ...flags) {
1112
+ if (!name) {
1113
+ console.error('Observer name is required');
1114
+ console.log('Usage: ilana make:observer <ObserverName> [--model=ModelName]');
1115
+ process.exit(1);
1116
+ }
1117
+
1118
+ let modelName = '';
1119
+
1120
+ for (const flag of flags) {
1121
+ if (flag.startsWith('--model=')) {
1122
+ modelName = flag.split('=')[1];
1123
+ }
1124
+ }
1125
+
1126
+ const className = toPascalCase(name.replace('Observer', ''));
1127
+ const fileName = `${className}Observer${getFileExtension()}`;
1128
+ const filePath = path.join(process.cwd(), 'observers', fileName);
1129
+
1130
+ if (!fs.existsSync(path.dirname(filePath))) {
1131
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1132
+ }
1133
+
1134
+ const template = getObserverTemplate(className, modelName);
1135
+ fs.writeFileSync(filePath, template);
1136
+ console.log(`Created observer: observers/${fileName}`);
1137
+
1138
+ if (modelName) {
1139
+ console.log(`Observer configured for model: ${modelName}`);
1140
+ }
1141
+ },
1142
+
1143
+ async 'make:cast'(name) {
1144
+ if (!name) {
1145
+ console.error('Cast name is required');
1146
+ console.log('Usage: ilana make:cast <CastName>');
1147
+ process.exit(1);
1148
+ }
1149
+
1150
+ const className = toPascalCase(name.replace('Cast', ''));
1151
+ const fileName = `${className}Cast${getFileExtension()}`;
1152
+ const filePath = path.join(process.cwd(), 'casts', fileName);
1153
+
1154
+ if (!fs.existsSync(path.dirname(filePath))) {
1155
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1156
+ }
1157
+
1158
+ const template = getCastTemplate(className);
1159
+ fs.writeFileSync(filePath, template);
1160
+ console.log(`Created cast: casts/${fileName}`);
1161
+ },
1162
+
702
1163
  help() {
703
1164
  console.log(`
704
1165
  Ilana ORM CLI
@@ -715,6 +1176,24 @@ Available commands:
715
1176
  -mfs Generate model + migration + factory + seeder
716
1177
 
717
1178
  make:migration <name> Create a new migration file
1179
+ make:factory <name> Create a new factory
1180
+ make:seeder <name> Create a new seeder
1181
+ make:observer <name> Create a new observer
1182
+ make:cast <name> Create a new cast
1183
+
1184
+ migrate [connection] Run all pending migrations
1185
+ migrate:rollback [steps] Rollback the last batch of migrations
1186
+ migrate:reset [connection] Rollback all migrations
1187
+ migrate:fresh [connection] Drop all tables and re-run migrations
1188
+ migrate:refresh [connection] Reset and re-run all migrations
1189
+ migrate:status [connection] Show migration status
1190
+ migrate:list [connection] List completed migrations
1191
+ migrate:unlock [connection] Unlock migrations (if stuck)
1192
+
1193
+ seed [name] Run database seeders
1194
+ db:seed [name] Alias for seed command
1195
+ db:wipe [connection] Drop all tables
1196
+
718
1197
  help Show this help message
719
1198
 
720
1199
  Examples:
@@ -725,6 +1204,14 @@ Examples:
725
1204
  ilana make:model Permission --all
726
1205
  ilana make:model UserPost --pivot
727
1206
  ilana make:migration create_users_table
1207
+ ilana make:observer UserObserver --model=User
1208
+ ilana make:cast MoneyCast
1209
+ ilana migrate
1210
+ ilana migrate mysql
1211
+ ilana migrate:rollback 2 postgres
1212
+ ilana migrate:fresh --seed
1213
+ ilana seed UserSeeder
1214
+ ilana db:wipe
728
1215
  `);
729
1216
  }
730
1217
  };
@@ -53,6 +53,27 @@ class MigrationRunner {
53
53
  table.timestamp('executed_at').defaultTo(Database.getInstance().fn.now());
54
54
  });
55
55
  }
56
+
57
+ // Ensure migration lock table exists
58
+ const lockTableName = `${this.tableName}_lock`;
59
+ const hasLockTable = await schema.hasTable(lockTableName);
60
+
61
+ if (!hasLockTable) {
62
+ await schema.createTable(lockTableName, (table) => {
63
+ table.increments('id');
64
+ table.boolean('is_locked').defaultTo(false);
65
+ table.timestamp('locked_at').nullable();
66
+ table.string('locked_by').nullable();
67
+ table.timestamp('created_at').defaultTo(Database.getInstance().fn.now());
68
+ });
69
+
70
+ // Insert initial lock record
71
+ await Database.table(lockTableName).insert({
72
+ is_locked: false,
73
+ locked_at: null,
74
+ locked_by: null
75
+ });
76
+ }
56
77
  }
57
78
 
58
79
  async getPendingMigrations() {
@@ -77,94 +98,106 @@ class MigrationRunner {
77
98
  }
78
99
 
79
100
  async migrate(connection, onlyFile, toFile) {
80
- let pendingMigrations = await this.getPendingMigrations();
101
+ await this.acquireLock(connection);
81
102
 
82
- if (onlyFile) {
83
- pendingMigrations = pendingMigrations.filter(m => m.includes(onlyFile));
84
- }
103
+ try {
104
+ let pendingMigrations = await this.getPendingMigrations();
85
105
 
86
- if (toFile) {
87
- const toIndex = pendingMigrations.findIndex(m => m.includes(toFile));
88
- if (toIndex >= 0) {
89
- pendingMigrations = pendingMigrations.slice(0, toIndex + 1);
106
+ if (onlyFile) {
107
+ pendingMigrations = pendingMigrations.filter(m => m.includes(onlyFile));
90
108
  }
91
- }
92
109
 
93
- if (pendingMigrations.length === 0) {
94
- console.log('Nothing to migrate.');
95
- return;
96
- }
110
+ if (toFile) {
111
+ const toIndex = pendingMigrations.findIndex(m => m.includes(toFile));
112
+ if (toIndex >= 0) {
113
+ pendingMigrations = pendingMigrations.slice(0, toIndex + 1);
114
+ }
115
+ }
97
116
 
98
- const batch = await this.getNextBatchNumber();
99
- const schema = new SchemaBuilder(connection);
117
+ if (pendingMigrations.length === 0) {
118
+ console.log('Nothing to migrate.');
119
+ return;
120
+ }
100
121
 
101
- console.log(`Running ${pendingMigrations.length} migrations...`);
122
+ const batch = await this.getNextBatchNumber();
123
+ const schema = new SchemaBuilder(connection);
102
124
 
103
- for (const migrationFile of pendingMigrations) {
104
- console.log(`Migrating: ${migrationFile}`);
125
+ console.log(`Running ${pendingMigrations.length} migrations...`);
105
126
 
106
- const migration = await this.loadMigration(migrationFile);
107
- const migrationConnection = migration.connection || connection;
108
- const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
127
+ for (const migrationFile of pendingMigrations) {
128
+ console.log(`Migrating: ${migrationFile}`);
109
129
 
110
- await migration.up(migrationSchema);
130
+ const migration = await this.loadMigration(migrationFile);
131
+ const migrationConnection = migration.connection || connection;
132
+ const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
111
133
 
112
- await Database.table(this.tableName, migrationConnection).insert({
113
- migration: migrationFile,
114
- batch,
115
- executed_at: new Date()
116
- });
134
+ await migration.up(migrationSchema);
117
135
 
118
- console.log(`Migrated: ${migrationFile}`);
119
- }
136
+ await Database.table(this.tableName, migrationConnection).insert({
137
+ migration: migrationFile,
138
+ batch,
139
+ executed_at: new Date()
140
+ });
120
141
 
121
- console.log('Migration completed.');
142
+ console.log(`Migrated: ${migrationFile}`);
143
+ }
144
+
145
+ console.log('Migration completed.');
146
+ } finally {
147
+ await this.releaseLock(connection);
148
+ }
122
149
  }
123
150
 
124
151
  async rollback(steps = 1, connection, toFile) {
125
- const executedMigrations = await Database.table(this.tableName, connection)
126
- .select('*')
127
- .orderBy('batch', 'desc')
128
- .orderBy('migration', 'desc');
152
+ await this.acquireLock(connection);
129
153
 
130
- if (executedMigrations.length === 0) {
131
- console.log('Nothing to rollback.');
132
- return;
133
- }
154
+ try {
155
+ const executedMigrations = await Database.table(this.tableName, connection)
156
+ .select('*')
157
+ .orderBy('batch', 'desc')
158
+ .orderBy('migration', 'desc');
159
+
160
+ if (executedMigrations.length === 0) {
161
+ console.log('Nothing to rollback.');
162
+ return;
163
+ }
134
164
 
135
- let migrationsToRollback = executedMigrations;
165
+ let migrationsToRollback = executedMigrations;
136
166
 
137
- if (toFile) {
138
- const toIndex = executedMigrations.findIndex(m => m.migration.includes(toFile));
139
- if (toIndex >= 0) {
140
- migrationsToRollback = executedMigrations.slice(0, toIndex + 1);
167
+ if (toFile) {
168
+ const toIndex = executedMigrations.findIndex(m => m.migration.includes(toFile));
169
+ if (toIndex >= 0) {
170
+ migrationsToRollback = executedMigrations.slice(0, toIndex + 1);
171
+ }
172
+ } else {
173
+ const batches = [...new Set(executedMigrations.map(m => m.batch))].slice(0, steps);
174
+ migrationsToRollback = executedMigrations.filter(m => batches.includes(m.batch));
141
175
  }
142
- } else {
143
- const batches = [...new Set(executedMigrations.map(m => m.batch))].slice(0, steps);
144
- migrationsToRollback = executedMigrations.filter(m => batches.includes(m.batch));
145
- }
146
176
 
147
- const schema = new SchemaBuilder(connection);
177
+ const schema = new SchemaBuilder(connection);
148
178
 
149
- console.log(`Rolling back ${migrationsToRollback.length} migrations...`);
179
+ console.log(`Rolling back ${migrationsToRollback.length} migrations...`);
150
180
 
151
- for (const migrationRecord of migrationsToRollback) {
152
- console.log(`Rolling back: ${migrationRecord.migration}`);
181
+ for (const migrationRecord of migrationsToRollback) {
182
+ console.log(`Rolling back: ${migrationRecord.migration}`);
153
183
 
154
- const migration = await this.loadMigration(migrationRecord.migration);
155
- const migrationConnection = migration.connection || connection;
156
- const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
184
+ const migration = await this.loadMigration(migrationRecord.migration);
185
+ const migrationConnection = migration.connection || connection;
186
+ const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
157
187
 
158
- await migration.down(migrationSchema);
188
+ await migration.down(migrationSchema);
159
189
 
160
- await Database.table(this.tableName, migrationConnection)
161
- .where('migration', migrationRecord.migration)
162
- .delete();
190
+ await Database.table(this.tableName, migrationConnection)
191
+ .where('migration', migrationRecord.migration)
192
+ .delete();
163
193
 
164
- console.log(`Rolled back: ${migrationRecord.migration}`);
165
- }
194
+ console.log(`Rolled back: ${migrationRecord.migration}`);
195
+ }
166
196
 
167
- console.log('Rollback completed.');
197
+ console.log('Rollback completed.');
198
+ } finally {
199
+ await this.releaseLock(connection);
200
+ }
168
201
  }
169
202
 
170
203
  async reset(connection) {
@@ -220,8 +253,37 @@ class MigrationRunner {
220
253
  }
221
254
 
222
255
  async unlock(connection) {
223
- // In a real implementation, this would unlock migration locks
224
- console.log('Migration locks cleared.');
256
+ await this.ensureMigrationsTable();
257
+ const lockTableName = `${this.tableName}_lock`;
258
+
259
+ try {
260
+ const lockRecord = await Database.table(lockTableName, connection).first();
261
+
262
+ if (!lockRecord) {
263
+ console.log('No migration lock found.');
264
+ return;
265
+ }
266
+
267
+ if (!lockRecord.is_locked) {
268
+ console.log('Migrations are not locked.');
269
+ return;
270
+ }
271
+
272
+ await Database.table(lockTableName, connection)
273
+ .where('id', lockRecord.id)
274
+ .update({
275
+ is_locked: false,
276
+ locked_at: null,
277
+ locked_by: null
278
+ });
279
+
280
+ console.log('Migration lock cleared successfully.');
281
+ console.log(`Previous lock was held by: ${lockRecord.locked_by || 'unknown'}`);
282
+ console.log(`Lock was acquired at: ${lockRecord.locked_at || 'unknown'}`);
283
+ } catch (error) {
284
+ console.error('Error clearing migration lock:', error.message);
285
+ throw error;
286
+ }
225
287
  }
226
288
 
227
289
  async wipe(connection) {
@@ -485,6 +547,49 @@ module.exports = ${className};
485
547
  const match = name.match(/create_(.+)_table/);
486
548
  return match ? match[1] : 'table_name';
487
549
  }
550
+
551
+ async acquireLock(connection) {
552
+ await this.ensureMigrationsTable();
553
+ const lockTableName = `${this.tableName}_lock`;
554
+ const lockHolder = `${process.env.USER || process.env.USERNAME || 'unknown'}@${require('os').hostname()}`;
555
+
556
+ const lockRecord = await Database.table(lockTableName, connection).first();
557
+
558
+ if (lockRecord && lockRecord.is_locked) {
559
+ throw new Error(
560
+ `Migration is locked by ${lockRecord.locked_by} at ${lockRecord.locked_at}. ` +
561
+ 'Use "ilana migrate:unlock" to clear the lock if no migration is running.'
562
+ );
563
+ }
564
+
565
+ await Database.table(lockTableName, connection)
566
+ .where('id', lockRecord.id)
567
+ .update({
568
+ is_locked: true,
569
+ locked_at: new Date(),
570
+ locked_by: lockHolder
571
+ });
572
+ }
573
+
574
+ async releaseLock(connection) {
575
+ const lockTableName = `${this.tableName}_lock`;
576
+
577
+ try {
578
+ const lockRecord = await Database.table(lockTableName, connection).first();
579
+
580
+ if (lockRecord) {
581
+ await Database.table(lockTableName, connection)
582
+ .where('id', lockRecord.id)
583
+ .update({
584
+ is_locked: false,
585
+ locked_at: null,
586
+ locked_by: null
587
+ });
588
+ }
589
+ } catch (error) {
590
+ console.warn('Warning: Could not release migration lock:', error.message);
591
+ }
592
+ }
488
593
  }
489
594
 
490
595
  module.exports = MigrationRunner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilana-orm",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "A fully-featured, Eloquent-style ORM for Node.js with TypeScript support",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -34,6 +34,31 @@
34
34
  "import": "./database/connection.mjs",
35
35
  "require": "./database/connection.js",
36
36
  "types": "./database/connection.d.ts"
37
+ },
38
+ "./database/schema-builder": {
39
+ "require": "./database/schema-builder.js"
40
+ },
41
+ "./orm/Seeder": {
42
+ "require": "./orm/Seeder.js"
43
+ },
44
+ "./orm/Factory": {
45
+ "require": "./orm/Factory.js"
46
+ },
47
+ "./orm/ModelRegistry": {
48
+ "import": "./orm/ModelRegistry.mjs",
49
+ "require": "./orm/ModelRegistry.js",
50
+ "types": "./orm/ModelRegistry.d.ts"
51
+ },
52
+ "./orm/Relation": {
53
+ "import": "./orm/Relation.mjs",
54
+ "require": "./orm/Relation.js",
55
+ "types": "./orm/Relation.d.ts"
56
+ },
57
+ "./orm/MigrationRunner": {
58
+ "require": "./orm/MigrationRunner.js"
59
+ },
60
+ "./database/DB": {
61
+ "require": "./database/DB.js"
37
62
  }
38
63
  },
39
64
  "bin": {
@@ -110,4 +135,4 @@
110
135
  "engines": {
111
136
  "node": ">=16.0.0"
112
137
  }
113
- }
138
+ }