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
package/cli/ilana.js
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Database = require('../database/connection');
|
|
6
|
+
const MigrationRunner = require('../orm/MigrationRunner');
|
|
7
|
+
|
|
8
|
+
// Default database configuration
|
|
9
|
+
const defaultConfig = {
|
|
10
|
+
default: 'mysql',
|
|
11
|
+
connections: {
|
|
12
|
+
mysql: {
|
|
13
|
+
client: 'mysql2',
|
|
14
|
+
connection: {
|
|
15
|
+
host: 'localhost',
|
|
16
|
+
port: 3306,
|
|
17
|
+
user: 'root',
|
|
18
|
+
password: '',
|
|
19
|
+
database: 'ilana_orm'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
migrations: {
|
|
24
|
+
directory: './migrations',
|
|
25
|
+
tableName: 'migrations'
|
|
26
|
+
},
|
|
27
|
+
seeds: {
|
|
28
|
+
directory: './seeds'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Load configuration and auto-initialize
|
|
33
|
+
function loadConfig() {
|
|
34
|
+
const configPath = path.join(process.cwd(), 'ilana.config.js');
|
|
35
|
+
if (fs.existsSync(configPath)) {
|
|
36
|
+
delete require.cache[configPath];
|
|
37
|
+
const config = require(configPath);
|
|
38
|
+
// Config file already initializes database
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
// No config file found - don't initialize with hardcoded default
|
|
42
|
+
console.error('No ilana.config.js found. Run "npx ilana setup" first.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Initialize database
|
|
47
|
+
function initializeDatabase() {
|
|
48
|
+
loadConfig(); // Config handles initialization
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper functions
|
|
52
|
+
function isTypeScriptProject() {
|
|
53
|
+
return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getFileExtension() {
|
|
57
|
+
return isTypeScriptProject() ? '.ts' : '.js';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toPascalCase(str) {
|
|
61
|
+
return str.replace(/(^|_)(.)/g, (_, __, char) => char.toUpperCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toSnakeCase(str) {
|
|
65
|
+
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pluralize(str) {
|
|
69
|
+
if (str.endsWith('y')) return str.slice(0, -1) + 'ies';
|
|
70
|
+
if (str.endsWith('s')) return str + 'es';
|
|
71
|
+
return str + 's';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generateModel(name, options = {}) {
|
|
75
|
+
const className = toPascalCase(name);
|
|
76
|
+
const tableName = pluralize(toSnakeCase(name));
|
|
77
|
+
const fileName = `${className}${getFileExtension()}`;
|
|
78
|
+
const filePath = path.join(process.cwd(), 'models', fileName);
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(filePath)) {
|
|
81
|
+
console.error(`Model ${className} already exists at models/${fileName}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
86
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const template = options.pivot ? getPivotModelTemplate(className, tableName) : getModelTemplate(className, tableName);
|
|
90
|
+
fs.writeFileSync(filePath, template);
|
|
91
|
+
console.log(`Created model: models/${fileName}`);
|
|
92
|
+
|
|
93
|
+
if (options.migration || options.all) {
|
|
94
|
+
const migrationName = `create_${tableName}_table`;
|
|
95
|
+
const runner = new MigrationRunner();
|
|
96
|
+
runner.generateMigration(migrationName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.factory || options.all) {
|
|
100
|
+
generateFactory(className);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options.seed || options.all) {
|
|
104
|
+
generateSeeder(className);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getModelTemplate(className, tableName) {
|
|
109
|
+
if (isTypeScriptProject()) {
|
|
110
|
+
return `import Model from 'ilana-orm/orm/Model';
|
|
111
|
+
// import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
|
|
112
|
+
|
|
113
|
+
export default class ${className} extends Model {
|
|
114
|
+
protected static table = '${tableName}';
|
|
115
|
+
protected static timestamps = true;
|
|
116
|
+
protected static softDeletes = false;
|
|
117
|
+
|
|
118
|
+
// For UUID primary keys, uncomment:
|
|
119
|
+
// protected static keyType = 'string' as const;
|
|
120
|
+
// protected static incrementing = false;
|
|
121
|
+
|
|
122
|
+
protected fillable: string[] = [];
|
|
123
|
+
protected hidden: string[] = [];
|
|
124
|
+
protected appends: string[] = [];
|
|
125
|
+
protected casts = {
|
|
126
|
+
// Basic casts
|
|
127
|
+
// is_active: 'boolean' as const,
|
|
128
|
+
// metadata: 'json' as const,
|
|
129
|
+
// tags: 'array' as const,
|
|
130
|
+
|
|
131
|
+
// Custom casts
|
|
132
|
+
// price: new MoneyCast(),
|
|
133
|
+
// secret: new EncryptedCast('your-key'),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Define relationships here
|
|
137
|
+
// example() {
|
|
138
|
+
// return this.hasMany(RelatedModel, 'foreign_key');
|
|
139
|
+
// }
|
|
140
|
+
|
|
141
|
+
// Define scopes here
|
|
142
|
+
// static scopeActive(query: any) {
|
|
143
|
+
// query.where('is_active', true);
|
|
144
|
+
// }
|
|
145
|
+
|
|
146
|
+
// Register for polymorphic relationships
|
|
147
|
+
// static {
|
|
148
|
+
// this.register();
|
|
149
|
+
// }
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `const Model = require('ilana-orm/orm/Model');
|
|
155
|
+
// const { MoneyCast, EncryptedCast } = require('ilana-orm/orm/CustomCasts');
|
|
156
|
+
|
|
157
|
+
class ${className} extends Model {
|
|
158
|
+
static table = '${tableName}';
|
|
159
|
+
static timestamps = true;
|
|
160
|
+
static softDeletes = false;
|
|
161
|
+
|
|
162
|
+
// For UUID primary keys, uncomment:
|
|
163
|
+
// static keyType = 'string';
|
|
164
|
+
// static incrementing = false;
|
|
165
|
+
|
|
166
|
+
fillable = [];
|
|
167
|
+
hidden = [];
|
|
168
|
+
appends = [];
|
|
169
|
+
casts = {
|
|
170
|
+
// Basic casts
|
|
171
|
+
// is_active: 'boolean',
|
|
172
|
+
// metadata: 'json',
|
|
173
|
+
// tags: 'array',
|
|
174
|
+
|
|
175
|
+
// Custom casts
|
|
176
|
+
// price: new MoneyCast(),
|
|
177
|
+
// secret: new EncryptedCast('your-key'),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Define relationships here
|
|
181
|
+
// example() {
|
|
182
|
+
// return this.hasMany(RelatedModel, 'foreign_key');
|
|
183
|
+
// }
|
|
184
|
+
|
|
185
|
+
// Define scopes here
|
|
186
|
+
// static scopeActive(query) {
|
|
187
|
+
// query.where('is_active', true);
|
|
188
|
+
// }
|
|
189
|
+
|
|
190
|
+
// Register for polymorphic relationships
|
|
191
|
+
// static {
|
|
192
|
+
// this.register();
|
|
193
|
+
// }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = ${className};
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getPivotModelTemplate(className, tableName) {
|
|
201
|
+
if (isTypeScriptProject()) {
|
|
202
|
+
return `import Model from 'ilana-orm/orm/Model';
|
|
203
|
+
|
|
204
|
+
export default class ${className} extends Model {
|
|
205
|
+
protected static table = '${tableName}';
|
|
206
|
+
protected static timestamps = true;
|
|
207
|
+
|
|
208
|
+
protected fillable: string[] = [];
|
|
209
|
+
|
|
210
|
+
// Define pivot relationships here
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return `const Model = require('ilana-orm/orm/Model');
|
|
216
|
+
|
|
217
|
+
class ${className} extends Model {
|
|
218
|
+
static table = '${tableName}';
|
|
219
|
+
static timestamps = true;
|
|
220
|
+
|
|
221
|
+
fillable = [];
|
|
222
|
+
|
|
223
|
+
// Define pivot relationships here
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = ${className};
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function generateFactory(className) {
|
|
231
|
+
const fileName = `${className}Factory${getFileExtension()}`;
|
|
232
|
+
const filePath = path.join(process.cwd(), 'database/factories', fileName);
|
|
233
|
+
|
|
234
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
235
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const template = isTypeScriptProject() ?
|
|
239
|
+
`import { defineFactory } from 'ilana-orm/orm/Factory';
|
|
240
|
+
import ${className} from '../../models/${className}';
|
|
241
|
+
|
|
242
|
+
export default defineFactory(${className}, (faker) => ({
|
|
243
|
+
// Define your factory attributes here
|
|
244
|
+
// name: faker.person.fullName(),
|
|
245
|
+
// email: faker.internet.email(),
|
|
246
|
+
}))
|
|
247
|
+
.state('example', (faker) => ({
|
|
248
|
+
// Define state modifications here
|
|
249
|
+
}));
|
|
250
|
+
` :
|
|
251
|
+
`const { defineFactory } = require('ilana-orm/orm/Factory');
|
|
252
|
+
const ${className} = require('../../models/${className}');
|
|
253
|
+
|
|
254
|
+
module.exports = defineFactory(${className}, (faker) => ({
|
|
255
|
+
// Define your factory attributes here
|
|
256
|
+
// name: faker.person.fullName(),
|
|
257
|
+
// email: faker.internet.email(),
|
|
258
|
+
}))
|
|
259
|
+
.state('example', (faker) => ({
|
|
260
|
+
// Define state modifications here
|
|
261
|
+
}));
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
fs.writeFileSync(filePath, template);
|
|
265
|
+
console.log(`Created factory: factories/${fileName}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function generateSeeder(className) {
|
|
269
|
+
const fileName = `${className}Seeder${getFileExtension()}`;
|
|
270
|
+
const filePath = path.join(process.cwd(), 'database/seeds', fileName);
|
|
271
|
+
|
|
272
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
273
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const template = isTypeScriptProject() ?
|
|
277
|
+
`import Seeder from 'ilana-orm/orm/Seeder';
|
|
278
|
+
import ${className} from '../../models/${className}';
|
|
279
|
+
import '../factories/${className}Factory';
|
|
280
|
+
|
|
281
|
+
export default class ${className}Seeder extends Seeder {
|
|
282
|
+
async run(): Promise<void> {
|
|
283
|
+
console.log('Seeding ${className.toLowerCase()}s...');
|
|
284
|
+
|
|
285
|
+
// Create sample data
|
|
286
|
+
await ${className}.factory().times(10).create();
|
|
287
|
+
|
|
288
|
+
console.log('${className}s seeded successfully');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
` :
|
|
292
|
+
`const Seeder = require('ilana-orm/orm/Seeder');
|
|
293
|
+
const ${className} = require('../../models/${className}');
|
|
294
|
+
require('../factories/${className}Factory');
|
|
295
|
+
|
|
296
|
+
class ${className}Seeder extends Seeder {
|
|
297
|
+
async run() {
|
|
298
|
+
console.log('Seeding ${className.toLowerCase()}s...');
|
|
299
|
+
|
|
300
|
+
// Create sample data
|
|
301
|
+
await ${className}.factory().times(10).create();
|
|
302
|
+
|
|
303
|
+
console.log('${className}s seeded successfully');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = ${className}Seeder;
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync(filePath, template);
|
|
311
|
+
console.log(`Created seeder: seeds/${fileName}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// CLI Commands
|
|
315
|
+
const commands = {
|
|
316
|
+
async setup() {
|
|
317
|
+
console.log('Setting up Ilana ORM...');
|
|
318
|
+
|
|
319
|
+
// Create directories
|
|
320
|
+
const dirs = ['models', 'database/migrations', 'database/factories', 'database/seeds'];
|
|
321
|
+
for (const dir of dirs) {
|
|
322
|
+
if (!fs.existsSync(dir)) {
|
|
323
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
324
|
+
console.log(`Created directory: ${dir}/`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Create config file if it doesn't exist
|
|
329
|
+
const configPath = 'ilana.config.js';
|
|
330
|
+
if (!fs.existsSync(configPath)) {
|
|
331
|
+
const configTemplate = `require('dotenv').config();
|
|
332
|
+
const Database = require('ilana-orm/database/connection');
|
|
333
|
+
|
|
334
|
+
const config = {
|
|
335
|
+
default: process.env.DB_CONNECTION || 'mysql',
|
|
336
|
+
timezone: process.env.DB_TIMEZONE || 'UTC',
|
|
337
|
+
|
|
338
|
+
connections: {
|
|
339
|
+
sqlite: {
|
|
340
|
+
client: 'sqlite3',
|
|
341
|
+
connection: {
|
|
342
|
+
filename: process.env.DB_FILENAME || './database.sqlite'
|
|
343
|
+
},
|
|
344
|
+
useNullAsDefault: true
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
mysql: {
|
|
348
|
+
client: 'mysql2',
|
|
349
|
+
connection: {
|
|
350
|
+
host: process.env.DB_HOST || 'localhost',
|
|
351
|
+
port: process.env.DB_PORT || 3306,
|
|
352
|
+
user: process.env.DB_USERNAME || 'root',
|
|
353
|
+
password: process.env.DB_PASSWORD || '',
|
|
354
|
+
database: process.env.DB_DATABASE || 'your_database',
|
|
355
|
+
timezone: process.env.DB_TIMEZONE || 'UTC'
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
postgres: {
|
|
360
|
+
client: 'pg',
|
|
361
|
+
connection: {
|
|
362
|
+
host: process.env.DB_HOST || 'localhost',
|
|
363
|
+
port: process.env.DB_PORT || 5432,
|
|
364
|
+
user: process.env.DB_USERNAME || 'postgres',
|
|
365
|
+
password: process.env.DB_PASSWORD || '',
|
|
366
|
+
database: process.env.DB_DATABASE || 'your_database'
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
migrations: {
|
|
372
|
+
directory: './database/migrations',
|
|
373
|
+
tableName: 'migrations'
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
seeds: {
|
|
377
|
+
directory: './database/seeds'
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Auto-initialize database connections
|
|
382
|
+
Database.configure(config);
|
|
383
|
+
|
|
384
|
+
module.exports = config;
|
|
385
|
+
`;
|
|
386
|
+
fs.writeFileSync(configPath, configTemplate);
|
|
387
|
+
console.log(`Created config file: ${configPath}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Create .env file if it doesn't exist
|
|
391
|
+
const envPath = '.env';
|
|
392
|
+
if (!fs.existsSync(envPath)) {
|
|
393
|
+
const envTemplate = `# Database Configuration
|
|
394
|
+
DB_CONNECTION=mysql
|
|
395
|
+
DB_HOST=localhost
|
|
396
|
+
DB_PORT=3306
|
|
397
|
+
DB_DATABASE=your_database
|
|
398
|
+
DB_USERNAME=root
|
|
399
|
+
DB_PASSWORD=
|
|
400
|
+
DB_TIMEZONE=UTC
|
|
401
|
+
|
|
402
|
+
# SQLite Configuration
|
|
403
|
+
# DB_CONNECTION=sqlite
|
|
404
|
+
# DB_FILENAME=./database.sqlite
|
|
405
|
+
|
|
406
|
+
# MySQL Configuration (alternative)
|
|
407
|
+
# DB_HOST=localhost
|
|
408
|
+
# DB_PORT=3306
|
|
409
|
+
# DB_DATABASE=your_database
|
|
410
|
+
# DB_USERNAME=root
|
|
411
|
+
# DB_PASSWORD=
|
|
412
|
+
# DB_TIMEZONE=America/New_York
|
|
413
|
+
|
|
414
|
+
# PostgreSQL Configuration
|
|
415
|
+
# DB_CONNECTION=postgres
|
|
416
|
+
# DB_HOST=localhost
|
|
417
|
+
# DB_PORT=5432
|
|
418
|
+
# DB_DATABASE=your_database
|
|
419
|
+
# DB_USERNAME=postgres
|
|
420
|
+
# DB_PASSWORD=
|
|
421
|
+
# DB_TIMEZONE=Europe/London
|
|
422
|
+
`;
|
|
423
|
+
fs.writeFileSync(envPath, envTemplate);
|
|
424
|
+
console.log(`Created environment file: ${envPath}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
console.log('\nIlana ORM setup complete!');
|
|
428
|
+
console.log('\nNext steps:');
|
|
429
|
+
console.log('1. Update .env file with your database credentials');
|
|
430
|
+
console.log('2. Run: ilana make:model User -m');
|
|
431
|
+
console.log('3. Run: ilana migrate');
|
|
432
|
+
},
|
|
433
|
+
async 'make:migration'(name, ...flags) {
|
|
434
|
+
if (!name) {
|
|
435
|
+
console.error('Migration name is required');
|
|
436
|
+
console.log('Usage: ilana make:migration <name> [--table=table] [--create=table]');
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let tableName = '';
|
|
441
|
+
let isCreate = false;
|
|
442
|
+
|
|
443
|
+
for (const flag of flags) {
|
|
444
|
+
if (flag.startsWith('--table=')) {
|
|
445
|
+
tableName = flag.split('=')[1];
|
|
446
|
+
} else if (flag.startsWith('--create=')) {
|
|
447
|
+
tableName = flag.split('=')[1];
|
|
448
|
+
isCreate = true;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
initializeDatabase();
|
|
453
|
+
const runner = new MigrationRunner();
|
|
454
|
+
runner.generateMigration(name, tableName, isCreate);
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
async 'make:factory'(name) {
|
|
458
|
+
if (!name) {
|
|
459
|
+
console.error('Factory name is required');
|
|
460
|
+
console.log('Usage: ilana make:factory <FactoryName>');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const className = name.replace('Factory', '');
|
|
465
|
+
generateFactory(className);
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
async 'make:seeder'(name) {
|
|
469
|
+
if (!name) {
|
|
470
|
+
console.error('Seeder name is required');
|
|
471
|
+
console.log('Usage: ilana make:seeder <SeederName>');
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const className = name.replace('Seeder', '');
|
|
476
|
+
generateSeeder(className);
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
async 'make:model'(name, ...flags) {
|
|
480
|
+
if (!name) {
|
|
481
|
+
console.error('Model name is required');
|
|
482
|
+
console.log('Usage: ilana make:model <ModelName> [options]');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Validate model name format
|
|
487
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
488
|
+
console.error('Model name must start with uppercase letter and contain only letters and numbers');
|
|
489
|
+
console.error('Invalid: App_Post, App/Post, app_post');
|
|
490
|
+
console.error('Valid: AppPost, User, BlogPost');
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const options = {};
|
|
495
|
+
|
|
496
|
+
for (const flag of flags) {
|
|
497
|
+
switch (flag) {
|
|
498
|
+
case '-m':
|
|
499
|
+
case '--migration':
|
|
500
|
+
options.migration = true;
|
|
501
|
+
break;
|
|
502
|
+
case '-f':
|
|
503
|
+
case '--factory':
|
|
504
|
+
options.factory = true;
|
|
505
|
+
break;
|
|
506
|
+
case '-s':
|
|
507
|
+
case '--seed':
|
|
508
|
+
options.seed = true;
|
|
509
|
+
break;
|
|
510
|
+
case '-p':
|
|
511
|
+
case '--pivot':
|
|
512
|
+
options.pivot = true;
|
|
513
|
+
break;
|
|
514
|
+
case '-a':
|
|
515
|
+
case '--all':
|
|
516
|
+
options.all = true;
|
|
517
|
+
break;
|
|
518
|
+
case '-mfs':
|
|
519
|
+
options.migration = true;
|
|
520
|
+
options.factory = true;
|
|
521
|
+
options.seed = true;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (options.migration || options.all) {
|
|
527
|
+
initializeDatabase();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
generateModel(name, options);
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
async migrate(...args) {
|
|
534
|
+
initializeDatabase();
|
|
535
|
+
const runner = new MigrationRunner();
|
|
536
|
+
|
|
537
|
+
let connection;
|
|
538
|
+
let onlyFile;
|
|
539
|
+
let toFile;
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < args.length; i++) {
|
|
542
|
+
const arg = args[i];
|
|
543
|
+
if (arg === '--connection' && args[i + 1]) {
|
|
544
|
+
connection = args[i + 1];
|
|
545
|
+
i++;
|
|
546
|
+
} else if (arg.startsWith('--connection=')) {
|
|
547
|
+
connection = arg.split('=')[1];
|
|
548
|
+
} else if (arg === '--only' && args[i + 1]) {
|
|
549
|
+
onlyFile = args[i + 1];
|
|
550
|
+
i++;
|
|
551
|
+
} else if (arg.startsWith('--only=')) {
|
|
552
|
+
onlyFile = arg.split('=')[1];
|
|
553
|
+
} else if (arg === '--to' && args[i + 1]) {
|
|
554
|
+
toFile = args[i + 1];
|
|
555
|
+
i++;
|
|
556
|
+
} else if (arg.startsWith('--to=')) {
|
|
557
|
+
toFile = arg.split('=')[1];
|
|
558
|
+
} else if (!arg.startsWith('--')) {
|
|
559
|
+
connection = arg;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await runner.migrate(connection, onlyFile, toFile);
|
|
564
|
+
process.exit(0);
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
async 'migrate:fresh'(...args) {
|
|
568
|
+
initializeDatabase();
|
|
569
|
+
const runner = new MigrationRunner();
|
|
570
|
+
|
|
571
|
+
let connection;
|
|
572
|
+
let withSeed = false;
|
|
573
|
+
|
|
574
|
+
for (const arg of args) {
|
|
575
|
+
if (arg === '--seed') {
|
|
576
|
+
withSeed = true;
|
|
577
|
+
} else if (arg.startsWith('--connection=')) {
|
|
578
|
+
connection = arg.split('=')[1];
|
|
579
|
+
} else if (!arg.startsWith('--')) {
|
|
580
|
+
connection = arg;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await runner.fresh(connection);
|
|
585
|
+
|
|
586
|
+
if (withSeed) {
|
|
587
|
+
await commands.seed();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
process.exit(0);
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
async 'migrate:list'(connection) {
|
|
594
|
+
initializeDatabase();
|
|
595
|
+
const runner = new MigrationRunner();
|
|
596
|
+
await runner.list(connection);
|
|
597
|
+
process.exit(0);
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
async 'migrate:unlock'(connection) {
|
|
601
|
+
initializeDatabase();
|
|
602
|
+
const runner = new MigrationRunner();
|
|
603
|
+
await runner.unlock(connection);
|
|
604
|
+
process.exit(0);
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
async 'migrate:rollback'(...args) {
|
|
608
|
+
initializeDatabase();
|
|
609
|
+
const runner = new MigrationRunner();
|
|
610
|
+
|
|
611
|
+
let steps = 1;
|
|
612
|
+
let connection;
|
|
613
|
+
let toFile;
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < args.length; i++) {
|
|
616
|
+
const arg = args[i];
|
|
617
|
+
if (arg === '--step' && args[i + 1]) {
|
|
618
|
+
steps = parseInt(args[i + 1]);
|
|
619
|
+
i++;
|
|
620
|
+
} else if (arg.startsWith('--step=')) {
|
|
621
|
+
steps = parseInt(arg.split('=')[1]);
|
|
622
|
+
} else if (arg === '--connection' && args[i + 1]) {
|
|
623
|
+
connection = args[i + 1];
|
|
624
|
+
i++;
|
|
625
|
+
} else if (arg.startsWith('--connection=')) {
|
|
626
|
+
connection = arg.split('=')[1];
|
|
627
|
+
} else if (arg === '--to' && args[i + 1]) {
|
|
628
|
+
toFile = args[i + 1];
|
|
629
|
+
i++;
|
|
630
|
+
} else if (arg.startsWith('--to=')) {
|
|
631
|
+
toFile = arg.split('=')[1];
|
|
632
|
+
} else if (!isNaN(parseInt(arg))) {
|
|
633
|
+
steps = parseInt(arg);
|
|
634
|
+
} else if (!arg.startsWith('--')) {
|
|
635
|
+
connection = arg;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
await runner.rollback(steps, connection, toFile);
|
|
640
|
+
process.exit(0);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
async 'migrate:reset'(connection) {
|
|
644
|
+
initializeDatabase();
|
|
645
|
+
const runner = new MigrationRunner();
|
|
646
|
+
await runner.reset(connection);
|
|
647
|
+
process.exit(0);
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
async 'migrate:refresh'(connection) {
|
|
651
|
+
initializeDatabase();
|
|
652
|
+
const runner = new MigrationRunner();
|
|
653
|
+
await runner.refresh(connection);
|
|
654
|
+
process.exit(0);
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
async 'migrate:status'(connection) {
|
|
658
|
+
initializeDatabase();
|
|
659
|
+
const runner = new MigrationRunner();
|
|
660
|
+
await runner.status(connection);
|
|
661
|
+
process.exit(0);
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
async seed(...args) {
|
|
665
|
+
initializeDatabase();
|
|
666
|
+
|
|
667
|
+
let seederName;
|
|
668
|
+
let connection;
|
|
669
|
+
|
|
670
|
+
for (let i = 0; i < args.length; i++) {
|
|
671
|
+
const arg = args[i];
|
|
672
|
+
if (arg === '--class' && args[i + 1]) {
|
|
673
|
+
seederName = args[i + 1];
|
|
674
|
+
i++;
|
|
675
|
+
} else if (arg.startsWith('--class=')) {
|
|
676
|
+
seederName = arg.split('=')[1];
|
|
677
|
+
} else if (arg === '--connection' && args[i + 1]) {
|
|
678
|
+
connection = args[i + 1];
|
|
679
|
+
i++;
|
|
680
|
+
} else if (arg.startsWith('--connection=')) {
|
|
681
|
+
connection = arg.split('=')[1];
|
|
682
|
+
} else if (!arg.startsWith('--')) {
|
|
683
|
+
if (!seederName) seederName = arg;
|
|
684
|
+
else connection = arg;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const seedsPath = path.join(process.cwd(), 'database/seeds');
|
|
689
|
+
if (!fs.existsSync(seedsPath)) {
|
|
690
|
+
console.log('No seeds directory found');
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const seedFiles = fs.readdirSync(seedsPath)
|
|
695
|
+
.filter(file => file.endsWith('.ts') || file.endsWith('.js'))
|
|
696
|
+
.sort();
|
|
697
|
+
|
|
698
|
+
if (seedFiles.length === 0) {
|
|
699
|
+
console.log('No seed files found');
|
|
700
|
+
process.exit(0);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const filesToRun = seederName
|
|
704
|
+
? seedFiles.filter(file => file.includes(seederName))
|
|
705
|
+
: seedFiles;
|
|
706
|
+
|
|
707
|
+
console.log(`Running ${filesToRun.length} seeders...`);
|
|
708
|
+
|
|
709
|
+
for (const file of filesToRun) {
|
|
710
|
+
console.log(`Seeding: ${file}`);
|
|
711
|
+
const filepath = path.join(seedsPath, file);
|
|
712
|
+
delete require.cache[filepath];
|
|
713
|
+
const seederModule = require(filepath);
|
|
714
|
+
const SeederClass = seederModule.default || seederModule;
|
|
715
|
+
const seeder = new SeederClass();
|
|
716
|
+
|
|
717
|
+
if (connection) {
|
|
718
|
+
seeder.connection = connection;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (typeof seeder.run === 'function') {
|
|
722
|
+
await seeder.run();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
console.log(`Seeded: ${file}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
console.log('Seeding completed');
|
|
729
|
+
process.exit(0);
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
async 'db:seed'(seederName) {
|
|
733
|
+
return commands.seed(seederName);
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
async 'db:wipe'(connection) {
|
|
737
|
+
initializeDatabase();
|
|
738
|
+
const runner = new MigrationRunner();
|
|
739
|
+
await runner.wipe(connection);
|
|
740
|
+
process.exit(0);
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
async 'make:observer'(name, ...flags) {
|
|
744
|
+
if (!name) {
|
|
745
|
+
console.error('Observer name is required');
|
|
746
|
+
console.log('Usage: ilana make:observer <ObserverName> [--model=ModelName]');
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let modelName = '';
|
|
751
|
+
|
|
752
|
+
for (const flag of flags) {
|
|
753
|
+
if (flag.startsWith('--model=')) {
|
|
754
|
+
modelName = flag.split('=')[1];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const className = toPascalCase(name.replace('Observer', ''));
|
|
759
|
+
const fileName = `${className}Observer${getFileExtension()}`;
|
|
760
|
+
const filePath = path.join(process.cwd(), 'observers', fileName);
|
|
761
|
+
|
|
762
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
763
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const template = isTypeScriptProject() ?
|
|
767
|
+
`${modelName ? `import ${modelName} from '../models/${modelName}';\n\n` : ''}export default class ${className}Observer {
|
|
768
|
+
async creating(model: ${modelName || 'any'}): Promise<void> {}
|
|
769
|
+
async created(model: ${modelName || 'any'}): Promise<void> {}
|
|
770
|
+
async updating(model: ${modelName || 'any'}): Promise<void> {}
|
|
771
|
+
async updated(model: ${modelName || 'any'}): Promise<void> {}
|
|
772
|
+
async saving(model: ${modelName || 'any'}): Promise<void> {}
|
|
773
|
+
async saved(model: ${modelName || 'any'}): Promise<void> {}
|
|
774
|
+
async deleting(model: ${modelName || 'any'}): Promise<void> {}
|
|
775
|
+
async deleted(model: ${modelName || 'any'}): Promise<void> {}
|
|
776
|
+
}
|
|
777
|
+
` :
|
|
778
|
+
`${modelName ? `const ${modelName} = require('../models/${modelName}');\n\n` : ''}class ${className}Observer {
|
|
779
|
+
async creating(model) {}
|
|
780
|
+
async created(model) {}
|
|
781
|
+
async updating(model) {}
|
|
782
|
+
async updated(model) {}
|
|
783
|
+
async saving(model) {}
|
|
784
|
+
async saved(model) {}
|
|
785
|
+
async deleting(model) {}
|
|
786
|
+
async deleted(model) {}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
module.exports = ${className}Observer;
|
|
790
|
+
`;
|
|
791
|
+
|
|
792
|
+
fs.writeFileSync(filePath, template);
|
|
793
|
+
console.log(`Created observer: observers/${fileName}`);
|
|
794
|
+
|
|
795
|
+
if (modelName) {
|
|
796
|
+
console.log(`Observer configured for model: ${modelName}`);
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
async 'make:cast'(name) {
|
|
801
|
+
if (!name) {
|
|
802
|
+
console.error('Cast name is required');
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const className = toPascalCase(name.replace('Cast', ''));
|
|
807
|
+
const fileName = `${className}Cast${getFileExtension()}`;
|
|
808
|
+
const filePath = path.join(process.cwd(), 'casts', fileName);
|
|
809
|
+
|
|
810
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
811
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const template = isTypeScriptProject() ?
|
|
815
|
+
`import { CustomCast } from 'ilana-orm/orm/Model';
|
|
816
|
+
|
|
817
|
+
export default class ${className}Cast implements CustomCast {
|
|
818
|
+
get(value: any): any {
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
set(value: any): any {
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
` :
|
|
827
|
+
`class ${className}Cast {
|
|
828
|
+
get(value) {
|
|
829
|
+
return value;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
set(value) {
|
|
833
|
+
return value;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
module.exports = ${className}Cast;
|
|
838
|
+
`;
|
|
839
|
+
|
|
840
|
+
fs.writeFileSync(filePath, template);
|
|
841
|
+
console.log(`Created cast: casts/${fileName}`);
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
help() {
|
|
845
|
+
console.log(`
|
|
846
|
+
Ilana ORM CLI
|
|
847
|
+
|
|
848
|
+
Available commands:
|
|
849
|
+
setup Initialize Ilana ORM project structure
|
|
850
|
+
|
|
851
|
+
make:model <name> [options] Create a new model
|
|
852
|
+
-m, --migration Generate migration
|
|
853
|
+
-f, --factory Generate factory
|
|
854
|
+
-s, --seed Generate seeder
|
|
855
|
+
-p, --pivot Generate pivot model
|
|
856
|
+
-a, --all Generate all (model + migration + factory + seeder)
|
|
857
|
+
-mfs Generate model + migration + factory + seeder
|
|
858
|
+
|
|
859
|
+
make:migration <name> Create a new migration file
|
|
860
|
+
migrate [connection] Run all pending migrations
|
|
861
|
+
migrate:rollback [steps] [connection] Rollback the last batch of migrations
|
|
862
|
+
migrate:reset [connection] Rollback all migrations
|
|
863
|
+
migrate:refresh [connection] Reset and re-run all migrations
|
|
864
|
+
migrate:status [connection] Show migration status
|
|
865
|
+
seed [name] Run database seeders
|
|
866
|
+
db:seed [name] Alias for seed command
|
|
867
|
+
help Show this help message
|
|
868
|
+
|
|
869
|
+
Examples:
|
|
870
|
+
ilana setup
|
|
871
|
+
ilana make:model User -m
|
|
872
|
+
ilana make:model Post --migration --factory
|
|
873
|
+
ilana make:model UserRole -mfs
|
|
874
|
+
ilana make:model Permission --all
|
|
875
|
+
ilana make:model UserPost --pivot
|
|
876
|
+
ilana make:migration create_users_table
|
|
877
|
+
ilana migrate
|
|
878
|
+
ilana migrate mysql
|
|
879
|
+
ilana migrate:rollback 2 postgres
|
|
880
|
+
ilana seed UserSeeder
|
|
881
|
+
`);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// Parse command line arguments
|
|
886
|
+
async function main() {
|
|
887
|
+
const args = process.argv.slice(2);
|
|
888
|
+
const command = args[0];
|
|
889
|
+
const params = args.slice(1);
|
|
890
|
+
|
|
891
|
+
if (!command || command === 'help') {
|
|
892
|
+
commands.help();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const commandHandler = commands[command];
|
|
897
|
+
|
|
898
|
+
if (!commandHandler) {
|
|
899
|
+
console.error(`Unknown command: ${command}`);
|
|
900
|
+
commands.help();
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
await commandHandler(...params);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error('Error:', error.message);
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Handle uncaught errors
|
|
913
|
+
process.on('unhandledRejection', (error) => {
|
|
914
|
+
console.error('Unhandled rejection:', error);
|
|
915
|
+
process.exit(1);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
process.on('uncaughtException', (error) => {
|
|
919
|
+
console.error('Uncaught exception:', error);
|
|
920
|
+
process.exit(1);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Run CLI
|
|
924
|
+
if (require.main === module) {
|
|
925
|
+
main();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
module.exports = commands;
|