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 +498 -11
- package/orm/MigrationRunner.js +168 -63
- package/package.json +27 -2
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
|
-
|
|
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
|
};
|
package/orm/MigrationRunner.js
CHANGED
|
@@ -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
|
-
|
|
101
|
+
await this.acquireLock(connection);
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
pendingMigrations =
|
|
84
|
-
}
|
|
103
|
+
try {
|
|
104
|
+
let pendingMigrations = await this.getPendingMigrations();
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
117
|
+
if (pendingMigrations.length === 0) {
|
|
118
|
+
console.log('Nothing to migrate.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
100
121
|
|
|
101
|
-
|
|
122
|
+
const batch = await this.getNextBatchNumber();
|
|
123
|
+
const schema = new SchemaBuilder(connection);
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
console.log(`Migrating: ${migrationFile}`);
|
|
125
|
+
console.log(`Running ${pendingMigrations.length} migrations...`);
|
|
105
126
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
|
|
127
|
+
for (const migrationFile of pendingMigrations) {
|
|
128
|
+
console.log(`Migrating: ${migrationFile}`);
|
|
109
129
|
|
|
110
|
-
|
|
130
|
+
const migration = await this.loadMigration(migrationFile);
|
|
131
|
+
const migrationConnection = migration.connection || connection;
|
|
132
|
+
const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
|
|
111
133
|
|
|
112
|
-
|
|
113
|
-
migration: migrationFile,
|
|
114
|
-
batch,
|
|
115
|
-
executed_at: new Date()
|
|
116
|
-
});
|
|
134
|
+
await migration.up(migrationSchema);
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
await Database.table(this.tableName, migrationConnection).insert({
|
|
137
|
+
migration: migrationFile,
|
|
138
|
+
batch,
|
|
139
|
+
executed_at: new Date()
|
|
140
|
+
});
|
|
120
141
|
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
.select('*')
|
|
127
|
-
.orderBy('batch', 'desc')
|
|
128
|
-
.orderBy('migration', 'desc');
|
|
152
|
+
await this.acquireLock(connection);
|
|
129
153
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
165
|
+
let migrationsToRollback = executedMigrations;
|
|
136
166
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
177
|
+
const schema = new SchemaBuilder(connection);
|
|
148
178
|
|
|
149
|
-
|
|
179
|
+
console.log(`Rolling back ${migrationsToRollback.length} migrations...`);
|
|
150
180
|
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
for (const migrationRecord of migrationsToRollback) {
|
|
182
|
+
console.log(`Rolling back: ${migrationRecord.migration}`);
|
|
153
183
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
188
|
+
await migration.down(migrationSchema);
|
|
159
189
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
await Database.table(this.tableName, migrationConnection)
|
|
191
|
+
.where('migration', migrationRecord.migration)
|
|
192
|
+
.delete();
|
|
163
193
|
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
console.log(`Rolled back: ${migrationRecord.migration}`);
|
|
195
|
+
}
|
|
166
196
|
|
|
167
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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.
|
|
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
|
+
}
|