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.
- package/README.md +4763 -0
- package/cli/ilana.js +928 -0
- package/database/DB.js +85 -0
- package/database/connection.js +114 -0
- package/database/schema-builder.js +219 -0
- package/ilana.config.js +61 -0
- package/ilana.png +0 -0
- package/index.js +34 -0
- package/orm/Collection.js +283 -0
- package/orm/CustomCasts.js +75 -0
- package/orm/Factory.js +372 -0
- package/orm/MigrationRunner.js +458 -0
- package/orm/Model.js +607 -0
- package/orm/ModelRegistry.js +26 -0
- package/orm/QueryBuilder.js +680 -0
- package/orm/Relation.js +299 -0
- package/orm/Seeder.js +153 -0
- package/package.json +71 -0
- package/test-role.js +26 -0
|
@@ -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;
|