ilana-orm 1.0.0

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.
@@ -0,0 +1,458 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Database = require('../database/connection');
4
+ const SchemaBuilder = require('../database/schema-builder');
5
+
6
+ let config = {};
7
+
8
+ // Auto-load configuration on first import
9
+ (function autoLoadConfig() {
10
+ const configPath = path.join(process.cwd(), 'ilana.config.js');
11
+ if (fs.existsSync(configPath)) {
12
+ delete require.cache[configPath];
13
+ config = require(configPath) || {}; // Config file handles Database.configure()
14
+ }
15
+ })();
16
+
17
+ class MigrationRunner {
18
+ constructor() {
19
+ this.migrationsPath = config.migrations?.directory || './database/migrations';
20
+ this.tableName = config.migrations?.tableName || 'migrations';
21
+ }
22
+
23
+ async ensureMigrationsTable() {
24
+ const schema = new SchemaBuilder();
25
+ const hasTable = await schema.hasTable(this.tableName);
26
+
27
+ if (!hasTable) {
28
+ await schema.createTable(this.tableName, (table) => {
29
+ table.increments('id');
30
+ table.string('migration');
31
+ table.integer('batch');
32
+ table.timestamp('executed_at').defaultTo(Database.getInstance().fn.now());
33
+ });
34
+ }
35
+ }
36
+
37
+ async getPendingMigrations() {
38
+ await this.ensureMigrationsTable();
39
+
40
+ const executedMigrations = await Database.table(this.tableName)
41
+ .select('migration')
42
+ .then(rows => rows.map(row => row.migration));
43
+
44
+ const allMigrations = this.getAllMigrationFiles();
45
+
46
+ return allMigrations.filter(migration => !executedMigrations.includes(migration));
47
+ }
48
+
49
+ async getExecutedMigrations() {
50
+ await this.ensureMigrationsTable();
51
+
52
+ return Database.table(this.tableName)
53
+ .select('*')
54
+ .orderBy('batch', 'desc')
55
+ .orderBy('migration', 'desc');
56
+ }
57
+
58
+ async migrate(connection, onlyFile, toFile) {
59
+ let pendingMigrations = await this.getPendingMigrations();
60
+
61
+ if (onlyFile) {
62
+ pendingMigrations = pendingMigrations.filter(m => m.includes(onlyFile));
63
+ }
64
+
65
+ if (toFile) {
66
+ const toIndex = pendingMigrations.findIndex(m => m.includes(toFile));
67
+ if (toIndex >= 0) {
68
+ pendingMigrations = pendingMigrations.slice(0, toIndex + 1);
69
+ }
70
+ }
71
+
72
+ if (pendingMigrations.length === 0) {
73
+ console.log('Nothing to migrate.');
74
+ return;
75
+ }
76
+
77
+ const batch = await this.getNextBatchNumber();
78
+ const schema = new SchemaBuilder(connection);
79
+
80
+ console.log(`Running ${pendingMigrations.length} migrations...`);
81
+
82
+ for (const migrationFile of pendingMigrations) {
83
+ console.log(`Migrating: ${migrationFile}`);
84
+
85
+ const migration = await this.loadMigration(migrationFile);
86
+ const migrationConnection = migration.connection || connection;
87
+ const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
88
+
89
+ await migration.up(migrationSchema);
90
+
91
+ await Database.table(this.tableName, migrationConnection).insert({
92
+ migration: migrationFile,
93
+ batch,
94
+ executed_at: new Date()
95
+ });
96
+
97
+ console.log(`Migrated: ${migrationFile}`);
98
+ }
99
+
100
+ console.log('Migration completed.');
101
+ }
102
+
103
+ async rollback(steps = 1, connection, toFile) {
104
+ const executedMigrations = await Database.table(this.tableName, connection)
105
+ .select('*')
106
+ .orderBy('batch', 'desc')
107
+ .orderBy('migration', 'desc');
108
+
109
+ if (executedMigrations.length === 0) {
110
+ console.log('Nothing to rollback.');
111
+ return;
112
+ }
113
+
114
+ let migrationsToRollback = executedMigrations;
115
+
116
+ if (toFile) {
117
+ const toIndex = executedMigrations.findIndex(m => m.migration.includes(toFile));
118
+ if (toIndex >= 0) {
119
+ migrationsToRollback = executedMigrations.slice(0, toIndex + 1);
120
+ }
121
+ } else {
122
+ const batches = [...new Set(executedMigrations.map(m => m.batch))].slice(0, steps);
123
+ migrationsToRollback = executedMigrations.filter(m => batches.includes(m.batch));
124
+ }
125
+
126
+ const schema = new SchemaBuilder(connection);
127
+
128
+ console.log(`Rolling back ${migrationsToRollback.length} migrations...`);
129
+
130
+ for (const migrationRecord of migrationsToRollback) {
131
+ console.log(`Rolling back: ${migrationRecord.migration}`);
132
+
133
+ const migration = await this.loadMigration(migrationRecord.migration);
134
+ const migrationConnection = migration.connection || connection;
135
+ const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
136
+
137
+ await migration.down(migrationSchema);
138
+
139
+ await Database.table(this.tableName, migrationConnection)
140
+ .where('migration', migrationRecord.migration)
141
+ .delete();
142
+
143
+ console.log(`Rolled back: ${migrationRecord.migration}`);
144
+ }
145
+
146
+ console.log('Rollback completed.');
147
+ }
148
+
149
+ async reset(connection) {
150
+ const executedMigrations = await this.getExecutedMigrations();
151
+
152
+ if (executedMigrations.length === 0) {
153
+ console.log('Nothing to reset.');
154
+ return;
155
+ }
156
+
157
+ const schema = new SchemaBuilder(connection);
158
+
159
+ console.log(`Resetting ${executedMigrations.length} migrations...`);
160
+
161
+ for (const migrationRecord of executedMigrations) {
162
+ console.log(`Rolling back: ${migrationRecord.migration}`);
163
+
164
+ const migration = await this.loadMigration(migrationRecord.migration);
165
+ const migrationConnection = migration.connection || connection;
166
+ const migrationSchema = migrationConnection ? new SchemaBuilder(migrationConnection) : schema;
167
+
168
+ await migration.down(migrationSchema);
169
+
170
+ console.log(`Rolled back: ${migrationRecord.migration}`);
171
+ }
172
+
173
+ await Database.table(this.tableName, connection).delete();
174
+ console.log('Reset completed.');
175
+ }
176
+
177
+ async refresh(connection) {
178
+ await this.reset(connection);
179
+ await this.migrate(connection);
180
+ }
181
+
182
+ async fresh(connection) {
183
+ await this.wipe(connection);
184
+ await this.migrate(connection);
185
+ }
186
+
187
+ async list(connection) {
188
+ const executedMigrations = await Database.table(this.tableName, connection)
189
+ .select('*')
190
+ .orderBy('batch')
191
+ .orderBy('migration');
192
+
193
+ console.log('Executed Migrations:');
194
+ console.log('===================');
195
+
196
+ for (const migration of executedMigrations) {
197
+ console.log(`Batch ${migration.batch}: ${migration.migration} (${migration.executed_at})`);
198
+ }
199
+ }
200
+
201
+ async unlock(connection) {
202
+ // In a real implementation, this would unlock migration locks
203
+ console.log('Migration locks cleared.');
204
+ }
205
+
206
+ async wipe(connection) {
207
+ const schema = new SchemaBuilder(connection);
208
+ const knex = Database.connection(connection);
209
+
210
+ // Get all table names
211
+ let tables = [];
212
+
213
+ if (knex.client.config.client === 'sqlite3') {
214
+ const result = await knex.raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
215
+ tables = result.map((row) => row.name);
216
+ } else if (knex.client.config.client === 'mysql2') {
217
+ const result = await knex.raw('SHOW TABLES');
218
+ tables = result[0].map((row) => Object.values(row)[0]);
219
+ } else if (knex.client.config.client === 'pg') {
220
+ const result = await knex.raw("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
221
+ tables = result.rows.map((row) => row.tablename);
222
+ }
223
+
224
+ console.log(`Dropping ${tables.length} tables...`);
225
+
226
+ // Disable foreign key checks
227
+ if (knex.client.config.client === 'mysql2') {
228
+ await knex.raw('SET FOREIGN_KEY_CHECKS = 0');
229
+ }
230
+
231
+ for (const table of tables) {
232
+ await schema.dropTableIfExists(table);
233
+ console.log(`Dropped table: ${table}`);
234
+ }
235
+
236
+ // Re-enable foreign key checks
237
+ if (knex.client.config.client === 'mysql2') {
238
+ await knex.raw('SET FOREIGN_KEY_CHECKS = 1');
239
+ }
240
+
241
+ console.log('Database wiped.');
242
+ }
243
+
244
+ async status(connection) {
245
+ await this.ensureMigrationsTable();
246
+
247
+ const allMigrations = this.getAllMigrationFiles();
248
+ const executedMigrations = await Database.table(this.tableName, connection)
249
+ .select('migration')
250
+ .then(rows => rows.map(row => row.migration));
251
+
252
+ console.log('Migration Status:');
253
+ console.log('================');
254
+
255
+ for (const migration of allMigrations) {
256
+ const status = executedMigrations.includes(migration) ? 'Ran' : 'Pending';
257
+ console.log(`${status.padEnd(8)} ${migration}`);
258
+ }
259
+ }
260
+
261
+ generateMigration(name, tableName, isCreate) {
262
+ const timestamp = new Date().toISOString()
263
+ .replace(/[-:]/g, '')
264
+ .replace(/\..+/, '')
265
+ .replace('T', '');
266
+
267
+ const isTS = this.isTypeScriptProject();
268
+ const filename = `${timestamp}_${name}.${isTS ? 'ts' : 'js'}`;
269
+ const filepath = path.join(this.migrationsPath, filename);
270
+
271
+ const template = this.getMigrationTemplate(name, tableName, isCreate);
272
+
273
+ if (!fs.existsSync(this.migrationsPath)) {
274
+ fs.mkdirSync(this.migrationsPath, { recursive: true });
275
+ }
276
+
277
+ fs.writeFileSync(filepath, template);
278
+
279
+ console.log(`Created migration: ${filename}`);
280
+ return filename;
281
+ }
282
+
283
+ getAllMigrationFiles() {
284
+ if (!fs.existsSync(this.migrationsPath)) {
285
+ return [];
286
+ }
287
+
288
+ return fs.readdirSync(this.migrationsPath)
289
+ .filter(file => file.endsWith('.ts') || file.endsWith('.js'))
290
+ .sort();
291
+ }
292
+
293
+ async loadMigration(filename) {
294
+ const filepath = path.resolve(this.migrationsPath, filename);
295
+ delete require.cache[filepath];
296
+ const migrationModule = require(filepath);
297
+
298
+ const MigrationClass = migrationModule.default || migrationModule;
299
+
300
+ // Check if it's already an instance or needs to be instantiated
301
+ if (typeof MigrationClass === 'function') {
302
+ return new MigrationClass();
303
+ } else if (typeof MigrationClass === 'object' && MigrationClass.up && MigrationClass.down) {
304
+ return MigrationClass;
305
+ } else {
306
+ throw new Error(`Invalid migration format in ${filename}. Migration must export a class or object with up() and down() methods.`);
307
+ }
308
+ }
309
+
310
+ async getNextBatchNumber() {
311
+ const result = await Database.table(this.tableName)
312
+ .max('batch as max_batch')
313
+ .first();
314
+
315
+ return (result?.max_batch || 0) + 1;
316
+ }
317
+
318
+ getMigrationTemplate(name, tableName, isCreate) {
319
+ const className = this.toPascalCase(name);
320
+ const table = tableName || this.getTableNameFromMigration(name);
321
+
322
+ const isTS = this.isTypeScriptProject();
323
+
324
+ if (isCreate || name.includes('create_')) {
325
+ return isTS ?
326
+ `import SchemaBuilder from 'ilana-orm/database/schema-builder';
327
+
328
+ export default class ${className} {
329
+ // connection = 'mysql'; // Uncomment to use specific connection
330
+
331
+ async up(schema: SchemaBuilder): Promise<void> {
332
+ await schema.createTable('${table}', (table) => {
333
+ table.increments('id');
334
+ table.timestamps();
335
+ });
336
+ }
337
+
338
+ async down(schema: SchemaBuilder): Promise<void> {
339
+ await schema.dropTable('${table}');
340
+ }
341
+ }
342
+ ` :
343
+ `const SchemaBuilder = require('ilana-orm/database/schema-builder');
344
+
345
+ class ${className} {
346
+ // connection = 'mysql'; // Uncomment to use specific connection
347
+
348
+ async up(schema) {
349
+ await schema.createTable('${table}', (table) => {
350
+ table.increments('id');
351
+ table.timestamps();
352
+ });
353
+ }
354
+
355
+ async down(schema) {
356
+ await schema.dropTable('${table}');
357
+ }
358
+ }
359
+
360
+ module.exports = ${className};
361
+ `;
362
+ } else if (tableName) {
363
+ return isTS ?
364
+ `import SchemaBuilder from 'ilana-orm/database/schema-builder';
365
+
366
+ export default class ${className} {
367
+ // connection = 'mysql'; // Uncomment to use specific connection
368
+
369
+ async up(schema: SchemaBuilder): Promise<void> {
370
+ await schema.table('${table}', (table) => {
371
+ // Add your column modifications here
372
+ // table.string('new_column').nullable();
373
+ });
374
+ }
375
+
376
+ async down(schema: SchemaBuilder): Promise<void> {
377
+ await schema.table('${table}', (table) => {
378
+ // Reverse your modifications here
379
+ // table.dropColumn('new_column');
380
+ });
381
+ }
382
+ }
383
+ ` :
384
+ `const SchemaBuilder = require('ilana-orm/database/schema-builder');
385
+
386
+ class ${className} {
387
+ // connection = 'mysql'; // Uncomment to use specific connection
388
+
389
+ async up(schema) {
390
+ await schema.table('${table}', (table) => {
391
+ // Add your column modifications here
392
+ // table.string('new_column').nullable();
393
+ });
394
+ }
395
+
396
+ async down(schema) {
397
+ await schema.table('${table}', (table) => {
398
+ // Reverse your modifications here
399
+ // table.dropColumn('new_column');
400
+ });
401
+ }
402
+ }
403
+
404
+ module.exports = ${className};
405
+ `;
406
+ }
407
+
408
+ return isTS ?
409
+ `import SchemaBuilder from 'ilana-orm/database/schema-builder';
410
+
411
+ export default class ${className} {
412
+ // connection = 'mysql'; // Uncomment to use specific connection
413
+
414
+ async up(schema: SchemaBuilder): Promise<void> {
415
+ // Add your migration logic here
416
+ }
417
+
418
+ async down(schema: SchemaBuilder): Promise<void> {
419
+ // Add your rollback logic here
420
+ }
421
+ }
422
+ ` :
423
+ `const SchemaBuilder = require('ilana-orm/database/schema-builder');
424
+
425
+ class ${className} {
426
+ // connection = 'mysql'; // Uncomment to use specific connection
427
+
428
+ async up(schema) {
429
+ // Add your migration logic here
430
+ }
431
+
432
+ async down(schema) {
433
+ // Add your rollback logic here
434
+ }
435
+ }
436
+
437
+ module.exports = ${className};
438
+ `;
439
+ }
440
+
441
+ isTypeScriptProject() {
442
+ const fs = require('fs');
443
+ const path = require('path');
444
+ return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
445
+ }
446
+
447
+ toPascalCase(str) {
448
+ return str.replace(/(^|_)(.)/g, (_, __, char) => char.toUpperCase());
449
+ }
450
+
451
+ getTableNameFromMigration(name) {
452
+ // Extract table name from migration name
453
+ const match = name.match(/create_(.+)_table/);
454
+ return match ? match[1] : 'table_name';
455
+ }
456
+ }
457
+
458
+ module.exports = MigrationRunner;