ilana-orm 1.0.13 → 1.0.15

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 CHANGED
@@ -104,17 +104,23 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
104
104
  ## Installation
105
105
 
106
106
  ```bash
107
- # npm
108
107
  npm install ilana-orm
108
+ ```
109
109
 
110
- # yarn
111
- yarn add ilana-orm
110
+ ### Database Drivers
112
111
 
113
- # pnpm
114
- pnpm add ilana-orm
115
- ```
112
+ Install only the database driver you need:
116
113
 
117
- **All database drivers (PostgreSQL, MySQL, SQLite) and dotenv are included by default** - no additional installation required!
114
+ ```bash
115
+ # PostgreSQL
116
+ npm install pg
117
+
118
+ # MySQL
119
+ npm install mysql2
120
+
121
+ # SQLite
122
+ npm install sqlite3
123
+ ```
118
124
 
119
125
  ## Quick Start
120
126
 
@@ -4771,6 +4777,32 @@ class CustomCast {
4771
4777
 
4772
4778
  We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
4773
4779
 
4780
+ ## Security
4781
+
4782
+ ### Reporting Vulnerabilities
4783
+
4784
+ If you discover a security vulnerability, please email raphyabak@gmail.com with:
4785
+ - Description of the vulnerability
4786
+ - Steps to reproduce
4787
+ - Potential impact
4788
+
4789
+ We will respond within 48 hours.
4790
+
4791
+ ### Security Considerations
4792
+
4793
+ **Database Drivers**: Database drivers (pg, mysql2, sqlite3) are optional dependencies. Install only what you need to reduce your security surface:
4794
+ ```bash
4795
+ npm install pg # PostgreSQL
4796
+ npm install mysql2 # MySQL
4797
+ npm install sqlite3 # SQLite
4798
+ ```
4799
+
4800
+ **SQL Injection Protection**: IlanaORM uses parameterized queries via Knex.js. Always use query builder methods instead of raw SQL.
4801
+
4802
+ **Filesystem Access**: The CLI and migration tools require filesystem access. Review migration files before running.
4803
+
4804
+ **Network Access**: Database drivers make network connections. Ensure proper firewall and connection security.
4805
+
4774
4806
  ## License
4775
4807
 
4776
4808
  MIT License - see the [LICENSE](LICENSE) file for details.
package/cli/ilana.js CHANGED
@@ -90,6 +90,17 @@ function isESModuleProject() {
90
90
  return false;
91
91
  }
92
92
 
93
+ function getProjectStructure() {
94
+ const hasSrc = fs.existsSync(path.join(process.cwd(), 'src'));
95
+ return {
96
+ hasSrc,
97
+ modelsDir: hasSrc ? 'src/models' : 'models',
98
+ databaseDir: hasSrc ? 'src/database' : 'database',
99
+ observersDir: hasSrc ? 'src/observers' : 'observers',
100
+ castsDir: hasSrc ? 'src/casts' : 'casts'
101
+ };
102
+ }
103
+
93
104
  function getFileExtension() {
94
105
  return isTypeScriptProject() ? '.ts' : '.js';
95
106
  }
@@ -109,8 +120,9 @@ function pluralize(str) {
109
120
  }
110
121
 
111
122
  function getESModuleConfigTemplate() {
123
+ const structure = getProjectStructure();
112
124
  return `import 'dotenv/config';
113
- import Database from 'ilana-orm/database/connection';
125
+ import Database from 'ilana-orm/database/connection.js';
114
126
 
115
127
  const config = {
116
128
  default: process.env.DB_CONNECTION || 'mysql',
@@ -150,12 +162,12 @@ const config = {
150
162
  },
151
163
 
152
164
  migrations: {
153
- directory: './database/migrations',
165
+ directory: './${structure.databaseDir}/migrations',
154
166
  tableName: 'migrations'
155
167
  },
156
168
 
157
169
  seeds: {
158
- directory: './database/seeds'
170
+ directory: './${structure.databaseDir}/seeds'
159
171
  }
160
172
  };
161
173
 
@@ -167,6 +179,7 @@ export default config;
167
179
  }
168
180
 
169
181
  function getCommonJSConfigTemplate() {
182
+ const structure = getProjectStructure();
170
183
  return `require('dotenv').config();
171
184
  const Database = require('ilana-orm/database/connection');
172
185
 
@@ -208,12 +221,12 @@ const config = {
208
221
  },
209
222
 
210
223
  migrations: {
211
- directory: './database/migrations',
224
+ directory: './${structure.databaseDir}/migrations',
212
225
  tableName: 'migrations'
213
226
  },
214
227
 
215
228
  seeds: {
216
- directory: './database/seeds'
229
+ directory: './${structure.databaseDir}/seeds'
217
230
  }
218
231
  };
219
232
 
@@ -228,7 +241,8 @@ function generateModel(name, options = {}) {
228
241
  const className = toPascalCase(name);
229
242
  const tableName = pluralize(toSnakeCase(name));
230
243
  const fileName = `${className}${getFileExtension()}`;
231
- const filePath = path.join(process.cwd(), 'models', fileName);
244
+ const structure = getProjectStructure();
245
+ const filePath = path.join(process.cwd(), structure.modelsDir, fileName);
232
246
 
233
247
  if (fs.existsSync(filePath)) {
234
248
  console.error(`Model ${className} already exists at models/${fileName}`);
@@ -241,7 +255,7 @@ function generateModel(name, options = {}) {
241
255
 
242
256
  const template = options.pivot ? getPivotModelTemplate(className, tableName) : getModelTemplate(className, tableName);
243
257
  fs.writeFileSync(filePath, template);
244
- console.log(`Created model: models/${fileName}`);
258
+ console.log(`Created model: ${structure.modelsDir}/${fileName}`);
245
259
 
246
260
  if (options.migration || options.all) {
247
261
  const migrationName = `create_${tableName}_table`;
@@ -449,15 +463,17 @@ module.exports = ${className};
449
463
 
450
464
  function generateFactory(className) {
451
465
  const fileName = `${className}Factory${getFileExtension()}`;
452
- const filePath = path.join(process.cwd(), 'database/factories', fileName);
466
+ const structure = getProjectStructure();
467
+ const filePath = path.join(process.cwd(), structure.databaseDir, 'factories', fileName);
453
468
 
454
469
  if (!fs.existsSync(path.dirname(filePath))) {
455
470
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
456
471
  }
457
472
 
473
+ const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
458
474
  const template = isTypeScriptProject() ?
459
475
  `import { defineFactory } from 'ilana-orm/orm/Factory.js';
460
- import ${className} from '../../models/${className}.js';
476
+ import ${className} from '${modelPath}';
461
477
 
462
478
  export default defineFactory(${className}, (faker) => ({
463
479
  // Define your factory attributes here
@@ -469,7 +485,7 @@ export default defineFactory(${className}, (faker) => ({
469
485
  }));
470
486
  ` :
471
487
  `const { defineFactory } = require('ilana-orm/orm/Factory');
472
- const ${className} = require('../../models/${className}');
488
+ const ${className} = require('${modelPath.replace('.js', '')}');
473
489
 
474
490
  module.exports = defineFactory(${className}, (faker) => ({
475
491
  // Define your factory attributes here
@@ -482,20 +498,22 @@ module.exports = defineFactory(${className}, (faker) => ({
482
498
  `;
483
499
 
484
500
  fs.writeFileSync(filePath, template);
485
- console.log(`Created factory: factories/${fileName}`);
501
+ console.log(`Created factory: ${structure.databaseDir}/factories/${fileName}`);
486
502
  }
487
503
 
488
504
  function generateSeeder(className) {
489
505
  const fileName = `${className}Seeder${getFileExtension()}`;
490
- const filePath = path.join(process.cwd(), 'database/seeds', fileName);
506
+ const structure = getProjectStructure();
507
+ const filePath = path.join(process.cwd(), structure.databaseDir, 'seeds', fileName);
491
508
 
492
509
  if (!fs.existsSync(path.dirname(filePath))) {
493
510
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
494
511
  }
495
512
 
513
+ const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
496
514
  const template = isTypeScriptProject() ?
497
515
  `import Seeder from 'ilana-orm/orm/Seeder.js';
498
- import ${className} from '../../models/${className}.js';
516
+ import ${className} from '${modelPath}';
499
517
  import '../factories/${className}Factory.js';
500
518
 
501
519
  export default class ${className}Seeder extends Seeder {
@@ -510,7 +528,7 @@ export default class ${className}Seeder extends Seeder {
510
528
  }
511
529
  ` :
512
530
  `const Seeder = require('ilana-orm/orm/Seeder');
513
- const ${className} = require('../../models/${className}');
531
+ const ${className} = require('${modelPath.replace('.js', '')}');
514
532
  require('../factories/${className}Factory');
515
533
 
516
534
  class ${className}Seeder extends Seeder {
@@ -528,16 +546,16 @@ module.exports = ${className}Seeder;
528
546
  `;
529
547
 
530
548
  fs.writeFileSync(filePath, template);
531
- console.log(`Created seeder: seeds/${fileName}`);
549
+ console.log(`Created seeder: ${structure.databaseDir}/seeds/${fileName}`);
532
550
  }
533
551
 
534
552
  function getObserverTemplate(className, modelName) {
535
553
  const isESModule = isESModuleProject();
536
-
554
+
537
555
  if (isTypeScriptProject()) {
538
556
  const importStatement = modelName ? `import ${modelName} from '../models/${modelName}.js';\n\n` : '';
539
557
  const modelType = modelName || 'any';
540
-
558
+
541
559
  return `${importStatement}export default class ${className}Observer {
542
560
  async creating(model: ${modelType}): Promise<void> {
543
561
  // Logic before creating model
@@ -581,10 +599,10 @@ function getObserverTemplate(className, modelName) {
581
599
  }
582
600
  `;
583
601
  }
584
-
602
+
585
603
  if (isESModule) {
586
604
  const importStatement = modelName ? `import ${modelName} from '../models/${modelName}.js';\n\n` : '';
587
-
605
+
588
606
  return `${importStatement}class ${className}Observer {
589
607
  async creating(model) {
590
608
  // Logic before creating model
@@ -630,9 +648,9 @@ function getObserverTemplate(className, modelName) {
630
648
  export default ${className}Observer;
631
649
  `;
632
650
  }
633
-
651
+
634
652
  const importStatement = modelName ? `const ${modelName} = require('../models/${modelName}');\n\n` : '';
635
-
653
+
636
654
  return `${importStatement}class ${className}Observer {
637
655
  async creating(model) {
638
656
  // Logic before creating model
@@ -681,7 +699,7 @@ module.exports = ${className}Observer;
681
699
 
682
700
  function getCastTemplate(className) {
683
701
  const isESModule = isESModuleProject();
684
-
702
+
685
703
  if (isTypeScriptProject()) {
686
704
  return `export default class ${className}Cast {
687
705
  get(value: any): any {
@@ -696,7 +714,7 @@ function getCastTemplate(className) {
696
714
  }
697
715
  `;
698
716
  }
699
-
717
+
700
718
  if (isESModule) {
701
719
  return `class ${className}Cast {
702
720
  get(value) {
@@ -713,7 +731,7 @@ function getCastTemplate(className) {
713
731
  export default ${className}Cast;
714
732
  `;
715
733
  }
716
-
734
+
717
735
  return `class ${className}Cast {
718
736
  get(value) {
719
737
  // Transform value when retrieving from database
@@ -736,7 +754,13 @@ const commands = {
736
754
  console.log('Setting up Ilana ORM...');
737
755
 
738
756
  // Create directories
739
- const dirs = ['models', 'database/migrations', 'database/factories', 'database/seeds'];
757
+ const structure = getProjectStructure();
758
+ const dirs = [
759
+ structure.modelsDir,
760
+ `${structure.databaseDir}/migrations`,
761
+ `${structure.databaseDir}/factories`,
762
+ `${structure.databaseDir}/seeds`
763
+ ];
740
764
  for (const dir of dirs) {
741
765
  if (!fs.existsSync(dir)) {
742
766
  fs.mkdirSync(dir, { recursive: true });
@@ -1053,7 +1077,8 @@ DB_TIMEZONE=UTC
1053
1077
  }
1054
1078
  }
1055
1079
 
1056
- const seedsPath = path.join(process.cwd(), 'database/seeds');
1080
+ const structure = getProjectStructure();
1081
+ const seedsPath = path.join(process.cwd(), structure.databaseDir, 'seeds');
1057
1082
  if (!fs.existsSync(seedsPath)) {
1058
1083
  console.log('No seeds directory found');
1059
1084
  process.exit(0);
@@ -1125,7 +1150,8 @@ DB_TIMEZONE=UTC
1125
1150
 
1126
1151
  const className = toPascalCase(name.replace('Observer', ''));
1127
1152
  const fileName = `${className}Observer${getFileExtension()}`;
1128
- const filePath = path.join(process.cwd(), 'observers', fileName);
1153
+ const structure = getProjectStructure();
1154
+ const filePath = path.join(process.cwd(), structure.observersDir, fileName);
1129
1155
 
1130
1156
  if (!fs.existsSync(path.dirname(filePath))) {
1131
1157
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -1133,7 +1159,7 @@ DB_TIMEZONE=UTC
1133
1159
 
1134
1160
  const template = getObserverTemplate(className, modelName);
1135
1161
  fs.writeFileSync(filePath, template);
1136
- console.log(`Created observer: observers/${fileName}`);
1162
+ console.log(`Created observer: ${structure.observersDir}/${fileName}`);
1137
1163
 
1138
1164
  if (modelName) {
1139
1165
  console.log(`Observer configured for model: ${modelName}`);
@@ -1149,7 +1175,8 @@ DB_TIMEZONE=UTC
1149
1175
 
1150
1176
  const className = toPascalCase(name.replace('Cast', ''));
1151
1177
  const fileName = `${className}Cast${getFileExtension()}`;
1152
- const filePath = path.join(process.cwd(), 'casts', fileName);
1178
+ const structure = getProjectStructure();
1179
+ const filePath = path.join(process.cwd(), structure.castsDir, fileName);
1153
1180
 
1154
1181
  if (!fs.existsSync(path.dirname(filePath))) {
1155
1182
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -1157,7 +1184,7 @@ DB_TIMEZONE=UTC
1157
1184
 
1158
1185
  const template = getCastTemplate(className);
1159
1186
  fs.writeFileSync(filePath, template);
1160
- console.log(`Created cast: casts/${fileName}`);
1187
+ console.log(`Created cast: ${structure.castsDir}/${fileName}`);
1161
1188
  },
1162
1189
 
1163
1190
  help() {
@@ -10,17 +10,28 @@ class Database {
10
10
 
11
11
  // Initialize all configured connections
12
12
  for (const [name, connConfig] of Object.entries(config.connections)) {
13
- const connection = knex({
14
- ...connConfig,
15
- migrations: config.migrations || {
16
- directory: './migrations',
17
- tableName: 'migrations'
18
- },
19
- seeds: config.seeds || {
20
- directory: './seeds'
13
+ try {
14
+ const connection = knex({
15
+ ...connConfig,
16
+ migrations: config.migrations || {
17
+ directory: './migrations',
18
+ tableName: 'migrations'
19
+ },
20
+ seeds: config.seeds || {
21
+ directory: './seeds'
22
+ }
23
+ });
24
+ this.connections.set(name, connection);
25
+ } catch (error) {
26
+ if (error.code === 'MODULE_NOT_FOUND' && error.message.includes(connConfig.client)) {
27
+ const driverMap = { pg: 'pg', mysql2: 'mysql2', sqlite3: 'sqlite3' };
28
+ const driver = driverMap[connConfig.client] || connConfig.client;
29
+ throw new Error(
30
+ `Database driver '${driver}' not installed. Install it with: npm install ${driver}`
31
+ );
21
32
  }
22
- });
23
- this.connections.set(name, connection);
33
+ throw error;
34
+ }
24
35
  }
25
36
 
26
37
  // Set default instance after all connections are created
package/orm/Model.js CHANGED
@@ -290,20 +290,7 @@ class Model {
290
290
  if (cast === 'json' || cast === 'array') {
291
291
  try { return JSON.parse(val); } catch { return val; }
292
292
  }
293
- if (cast === 'date' && val != null) {
294
- const config = this._getConfig();
295
- const timezone = this.constructor.timezone || config?.timezone || 'UTC';
296
-
297
- try {
298
- // Try moment-timezone first
299
- const moment = require('moment-timezone');
300
- // Parse the stored value as if it's in the configured timezone
301
- return moment.tz(val, timezone).format('YYYY-MM-DD HH:mm:ss');
302
- } catch (e) {
303
- // Fallback: return the stored value as-is since it's already in the correct timezone
304
- return val;
305
- }
306
- }
293
+ if (cast === 'date' && val != null) return val;
307
294
  return val;
308
295
  }
309
296
 
@@ -337,38 +324,29 @@ class Model {
337
324
  }
338
325
 
339
326
  _getCurrentTimestamp() {
327
+ const now = new Date();
340
328
  const config = this._getConfig();
341
329
  const timezone = this.constructor.timezone || config?.timezone || 'UTC';
342
-
343
- try {
344
- // Try to use moment-timezone if available
345
- const moment = require('moment-timezone');
346
- return moment().tz(timezone).toDate();
347
- } catch (e) {
348
- try {
349
- // Try to use date-fns-tz if available
350
- const { zonedTimeToUtc } = require('date-fns-tz');
351
- return zonedTimeToUtc(new Date(), timezone);
352
- } catch (e2) {
353
- // Fallback to simple but accurate method
354
- const now = new Date();
355
- if (timezone === 'UTC') return now;
356
-
357
- // Use toLocaleString for accurate timezone conversion
358
- const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000);
359
- const targetTime = new Date(utcTime + (this._getTimezoneOffset(timezone, now) * 60000));
360
- return targetTime;
361
- }
362
- }
363
- }
364
-
365
- _getTimezoneOffset(timezone, date) {
330
+
331
+ if (timezone === 'UTC') return now;
332
+
366
333
  try {
367
- const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
368
- const targetDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
369
- return (targetDate.getTime() - utcDate.getTime()) / 60000;
370
- } catch (e) {
371
- return 0; // Default to UTC
334
+ const formatter = new Intl.DateTimeFormat('en-US', {
335
+ timeZone: timezone,
336
+ year: 'numeric',
337
+ month: '2-digit',
338
+ day: '2-digit',
339
+ hour: '2-digit',
340
+ minute: '2-digit',
341
+ second: '2-digit',
342
+ hour12: false
343
+ });
344
+ const parts = Object.fromEntries(
345
+ formatter.formatToParts(now).map(p => [p.type, p.value])
346
+ );
347
+ return new Date(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`);
348
+ } catch {
349
+ return now;
372
350
  }
373
351
  }
374
352
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilana-orm",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
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",
@@ -114,14 +114,15 @@
114
114
  "ilana.png"
115
115
  ],
116
116
  "dependencies": {
117
- "knex": "^3.0.0",
118
- "uuid": "^9.0.0",
119
- "@faker-js/faker": "^8.0.0",
120
- "pg": "^8.0.0",
121
- "mysql2": "^3.0.0",
122
- "sqlite3": "^5.0.0",
123
- "dotenv": "^16.3.1",
124
- "moment-timezone": "^0.6.0"
117
+ "knex": "^3.1.0",
118
+ "uuid": "^10.0.0",
119
+ "@faker-js/faker": "^9.0.0",
120
+ "dotenv": "^16.4.0"
121
+ },
122
+ "optionalDependencies": {
123
+ "pg": "^8.13.0",
124
+ "mysql2": "^3.11.0",
125
+ "sqlite3": "^5.1.7"
125
126
  },
126
127
  "devDependencies": {
127
128
  "@types/node": "^20.0.0",