@nitronjs/framework 0.2.2 → 0.2.4

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.
Files changed (71) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +17 -19
  4. package/lib/Auth/Auth.js +167 -0
  5. package/lib/Build/CssBuilder.js +9 -0
  6. package/lib/Build/FileAnalyzer.js +16 -0
  7. package/lib/Build/HydrationBuilder.js +17 -0
  8. package/lib/Build/Manager.js +15 -0
  9. package/lib/Build/colors.js +4 -0
  10. package/lib/Build/plugins.js +84 -20
  11. package/lib/Console/Commands/DevCommand.js +13 -9
  12. package/lib/Console/Commands/MakeCommand.js +24 -10
  13. package/lib/Console/Commands/MigrateCommand.js +4 -3
  14. package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
  15. package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
  16. package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
  17. package/lib/Console/Commands/SeedCommand.js +8 -28
  18. package/lib/Console/Commands/StorageLinkCommand.js +20 -5
  19. package/lib/Console/Output.js +143 -0
  20. package/lib/Core/Config.js +2 -1
  21. package/lib/Core/Paths.js +8 -8
  22. package/lib/Database/DB.js +141 -51
  23. package/lib/Database/Drivers/MySQLDriver.js +102 -157
  24. package/lib/Database/Migration/Checksum.js +3 -8
  25. package/lib/Database/Migration/MigrationRepository.js +25 -35
  26. package/lib/Database/Migration/MigrationRunner.js +59 -67
  27. package/lib/Database/Model.js +165 -75
  28. package/lib/Database/QueryBuilder.js +43 -0
  29. package/lib/Database/QueryValidation.js +51 -30
  30. package/lib/Database/Schema/Blueprint.js +25 -36
  31. package/lib/Database/Schema/Manager.js +31 -68
  32. package/lib/Database/Seeder/SeederRunner.js +24 -145
  33. package/lib/Date/DateTime.js +9 -0
  34. package/lib/Encryption/Encryption.js +52 -0
  35. package/lib/Faker/Faker.js +11 -0
  36. package/lib/Filesystem/Storage.js +120 -0
  37. package/lib/HMR/Server.js +79 -9
  38. package/lib/Hashing/Hash.js +41 -0
  39. package/lib/Http/Server.js +179 -151
  40. package/lib/Logging/{Manager.js → Log.js} +68 -80
  41. package/lib/Mail/Mail.js +187 -0
  42. package/lib/Route/Router.js +416 -0
  43. package/lib/Session/File.js +135 -233
  44. package/lib/Session/Manager.js +117 -171
  45. package/lib/Session/Memory.js +28 -38
  46. package/lib/Session/Session.js +71 -107
  47. package/lib/Support/Str.js +103 -0
  48. package/lib/Translation/Lang.js +54 -0
  49. package/lib/View/Client/hmr-client.js +87 -51
  50. package/lib/View/Client/nitronjs-icon.png +0 -0
  51. package/lib/View/{Manager.js → View.js} +44 -29
  52. package/lib/index.d.ts +49 -27
  53. package/lib/index.js +19 -13
  54. package/package.json +1 -1
  55. package/skeleton/app/Controllers/HomeController.js +7 -1
  56. package/skeleton/package.json +2 -0
  57. package/skeleton/resources/css/global.css +1 -0
  58. package/skeleton/resources/views/Site/Home.tsx +456 -79
  59. package/skeleton/tsconfig.json +6 -1
  60. package/lib/Auth/Manager.js +0 -111
  61. package/lib/Database/Connection.js +0 -61
  62. package/lib/Database/Manager.js +0 -162
  63. package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
  64. package/lib/Database/Seeder/SeederRepository.js +0 -45
  65. package/lib/Encryption/Manager.js +0 -47
  66. package/lib/Filesystem/Manager.js +0 -74
  67. package/lib/Hashing/Manager.js +0 -25
  68. package/lib/Mail/Manager.js +0 -120
  69. package/lib/Route/Loader.js +0 -80
  70. package/lib/Route/Manager.js +0 -286
  71. package/lib/Translation/Manager.js +0 -49
@@ -1,6 +1,17 @@
1
1
  import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
2
2
  import Environment from "../Core/Environment.js";
3
3
 
4
+ /**
5
+ * SQL query builder with fluent interface.
6
+ * Provides chainable methods for building safe parameterized queries.
7
+ *
8
+ * @example
9
+ * const users = await DB.table("users")
10
+ * .where("status", "active")
11
+ * .orderBy("created_at", "DESC")
12
+ * .limit(10)
13
+ * .get();
14
+ */
4
15
  class QueryBuilder {
5
16
  #table = null;
6
17
  #connection = null;
@@ -159,6 +170,18 @@ class QueryBuilder {
159
170
  }
160
171
 
161
172
  where(column, operator, value) {
173
+ if (typeof column === 'object' && column !== null && !Array.isArray(column)) {
174
+ for (const [key, val] of Object.entries(column)) {
175
+ if (Array.isArray(val) && val.length === 2) {
176
+ this.where(key, val[0], val[1]);
177
+ }
178
+ else {
179
+ this.where(key, '=', val);
180
+ }
181
+ }
182
+ return this;
183
+ }
184
+
162
185
  if (arguments.length === 2) {
163
186
  value = operator;
164
187
  operator = '=';
@@ -471,6 +494,26 @@ class QueryBuilder {
471
494
  return parseInt(result) || 0;
472
495
  }
473
496
 
497
+ async max(column) {
498
+ return await this.#aggregate('MAX', column);
499
+ }
500
+
501
+ async min(column) {
502
+ return await this.#aggregate('MIN', column);
503
+ }
504
+
505
+ async sum(column) {
506
+ const result = await this.#aggregate('SUM', column);
507
+
508
+ return parseFloat(result) || 0;
509
+ }
510
+
511
+ async avg(column) {
512
+ const result = await this.#aggregate('AVG', column);
513
+
514
+ return parseFloat(result) || 0;
515
+ }
516
+
474
517
  async #aggregate(func, column) {
475
518
  const alias = func.toLowerCase();
476
519
  const originalSelect = this.#selectColumns;
@@ -1,54 +1,68 @@
1
+ const RESERVED_KEYWORDS = new Set([
2
+ 'order', 'key', 'group', 'index', 'table', 'column', 'select', 'insert',
3
+ 'update', 'delete', 'from', 'where', 'join', 'left', 'right', 'inner',
4
+ 'outer', 'on', 'and', 'or', 'not', 'null', 'true', 'false', 'like',
5
+ 'in', 'between', 'is', 'as', 'by', 'asc', 'desc', 'limit', 'offset',
6
+ 'having', 'distinct', 'all', 'any', 'exists', 'case', 'when', 'then',
7
+ 'else', 'end', 'if', 'into', 'values', 'set', 'create', 'drop', 'alter',
8
+ 'add', 'primary', 'foreign', 'references', 'constraint', 'default',
9
+ 'unique', 'check', 'view', 'trigger', 'procedure', 'function', 'database',
10
+ 'schema', 'use', 'show', 'describe', 'explain', 'grant', 'revoke',
11
+ 'commit', 'rollback', 'transaction', 'lock', 'unlock', 'read', 'write',
12
+ 'range', 'rows', 'rank', 'row', 'status', 'type', 'level', 'value', 'name'
13
+ ]);
14
+
15
+ const ALLOWED_WHERE_OPERATORS = new Set([
16
+ '=', '!=', '<>', '<', '>', '<=', '>=',
17
+ 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
18
+ 'IN', 'NOT IN', 'IS', 'IS NOT',
19
+ 'BETWEEN', 'NOT BETWEEN',
20
+ 'REGEXP', 'NOT REGEXP', 'RLIKE', 'NOT RLIKE'
21
+ ]);
22
+
23
+ const IDENTIFIER_PATTERNS = [
24
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/,
25
+ /^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/,
26
+ /^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/
27
+ ];
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Public Functions
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
1
33
  export function validateIdentifier(identifier) {
2
34
  if (typeof identifier !== 'string' || identifier.length === 0) {
3
35
  throw new Error('Identifier must be a non-empty string');
4
36
  }
5
-
6
37
  if (identifier.length > 64) {
7
38
  throw new Error(`Invalid identifier: "${identifier}". Maximum length is 64 characters.`);
8
39
  }
9
-
10
40
  if (identifier === '*' || identifier === '*.*') {
11
41
  return identifier;
12
42
  }
13
-
14
43
  if (/--|#|\/\*|\*\//.test(identifier)) {
15
44
  throw new Error(`Invalid identifier: "${identifier}". SQL comments are not allowed.`);
16
45
  }
17
46
 
18
- const patterns = [
19
- /^[a-zA-Z_][a-zA-Z0-9_]*$/,
20
- /^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/,
21
- /^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/
22
- ];
23
-
24
- const isValid = patterns.some(pattern => pattern.test(identifier));
47
+ if (!IDENTIFIER_PATTERNS.some(p => p.test(identifier))) {
48
+ throw new Error(`Invalid identifier: "${identifier}". Must be alphanumeric with underscores.`);
49
+ }
25
50
 
26
- if (!isValid) {
27
- throw new Error(`Invalid identifier: "${identifier}". Must be alphanumeric with underscores, optionally qualified with table name.`);
51
+ if (identifier.includes('.')) {
52
+ const [table, column] = identifier.split('.');
53
+ return column === '*'
54
+ ? `${escapeKeyword(table)}.*`
55
+ : `${escapeKeyword(table)}.${escapeKeyword(column)}`;
28
56
  }
29
57
 
30
- return identifier;
58
+ return escapeKeyword(identifier);
31
59
  }
32
60
 
33
61
  export function validateWhereOperator(operator) {
34
62
  const normalized = String(operator).trim().toUpperCase();
35
- const allowedOperators = new Set([
36
- '=', '!=', '<>', '<', '>', '<=', '>=',
37
- 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
38
- 'IN', 'NOT IN',
39
- 'IS', 'IS NOT',
40
- 'BETWEEN', 'NOT BETWEEN',
41
- 'REGEXP', 'NOT REGEXP',
42
- 'RLIKE', 'NOT RLIKE'
43
- ]);
44
-
45
- if (!allowedOperators.has(normalized)) {
46
- throw new Error(
47
- `Invalid WHERE operator: '${operator}'. ` +
48
- `Allowed operators: ${[...allowedOperators].join(', ')}`
49
- );
63
+ if (!ALLOWED_WHERE_OPERATORS.has(normalized)) {
64
+ throw new Error(`Invalid WHERE operator: '${operator}'.`);
50
65
  }
51
-
52
66
  return normalized;
53
67
  }
54
68
 
@@ -57,6 +71,13 @@ export function validateDirection(direction) {
57
71
  if (upper !== 'ASC' && upper !== 'DESC') {
58
72
  throw new Error(`Invalid direction: ${direction}. Use 'ASC' or 'DESC'.`);
59
73
  }
60
-
61
74
  return upper;
62
75
  }
76
+
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+ // Private Functions
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+
81
+ function escapeKeyword(identifier) {
82
+ return RESERVED_KEYWORDS.has(identifier.toLowerCase()) ? `\`${identifier}\`` : identifier;
83
+ }
@@ -6,6 +6,10 @@ class Blueprint {
6
6
  this.#tableName = tableName;
7
7
  }
8
8
 
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+ // Public Methods
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
9
13
  getTableName() {
10
14
  return this.#tableName;
11
15
  }
@@ -14,43 +18,8 @@ class Blueprint {
14
18
  return this.#columns;
15
19
  }
16
20
 
17
- #addColumn(type, name, options = {}) {
18
- const column = {
19
- name,
20
- type,
21
- ...options,
22
- modifiers: {
23
- nullable: false,
24
- default: null,
25
- unique: false
26
- }
27
- };
28
-
29
- this.#columns.push(column);
30
-
31
- return {
32
- nullable: () => {
33
- column.modifiers.nullable = true;
34
- return this;
35
- },
36
- default: (value) => {
37
- column.modifiers.default = value;
38
- return this;
39
- },
40
- unique: () => {
41
- column.modifiers.unique = true;
42
- return this;
43
- }
44
- };
45
- }
46
-
47
21
  id() {
48
- const column = {
49
- name: 'id',
50
- type: 'id',
51
- modifiers: {}
52
- };
53
- this.#columns.push(column);
22
+ this.#columns.push({ name: 'id', type: 'id', modifiers: {} });
54
23
  return this;
55
24
  }
56
25
 
@@ -81,6 +50,26 @@ class Blueprint {
81
50
  json(name) {
82
51
  return this.#addColumn('json', name);
83
52
  }
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────
55
+ // Private Methods
56
+ // ─────────────────────────────────────────────────────────────────────────
57
+
58
+ #addColumn(type, name, options = {}) {
59
+ const column = {
60
+ name,
61
+ type,
62
+ ...options,
63
+ modifiers: { nullable: false, default: null, unique: false }
64
+ };
65
+ this.#columns.push(column);
66
+
67
+ return {
68
+ nullable: () => { column.modifiers.nullable = true; return this; },
69
+ default: (value) => { column.modifiers.default = value; return this; },
70
+ unique: () => { column.modifiers.unique = true; return this; }
71
+ };
72
+ }
84
73
  }
85
74
 
86
75
  export default Blueprint;
@@ -1,103 +1,66 @@
1
- import DatabaseManager from "../Manager.js";
1
+ import DB from "../DB.js";
2
2
  import Config from "../../Core/Config.js";
3
3
 
4
4
  export default class Schema {
5
5
 
6
+ // Public Methods
6
7
  static async create(tableName, callback) {
7
8
  const blueprint = new (await import('./Blueprint.js')).default(tableName);
8
9
  callback(blueprint);
9
-
10
- const sql = this.#buildCreateTableSQL(blueprint, false);
11
-
12
- const connection = DatabaseManager.getInstance().connection();
13
- await connection.raw(sql);
10
+ await DB.rawQuery(this.#buildCreateSQL(blueprint, false));
14
11
  }
15
12
 
16
13
  static async createIfNotExists(tableName, callback) {
17
14
  const blueprint = new (await import('./Blueprint.js')).default(tableName);
18
15
  callback(blueprint);
19
-
20
- const sql = this.#buildCreateTableSQL(blueprint, true);
21
-
22
- const connection = DatabaseManager.getInstance().connection();
23
- await connection.raw(sql);
16
+ await DB.rawQuery(this.#buildCreateSQL(blueprint, true));
24
17
  }
25
18
 
26
19
  static async dropIfExists(tableName) {
27
- const connection = DatabaseManager.getInstance().connection();
28
- await connection.raw(`DROP TABLE IF EXISTS \`${tableName}\``);
20
+ await DB.rawQuery(`DROP TABLE IF EXISTS \`${tableName}\``);
29
21
  }
30
22
 
31
- static #buildCreateTableSQL(blueprint, ifNotExists = false) {
32
- const columns = blueprint.getColumns();
33
- const columnsSql = columns.map(col => this.#buildColumnSQL(col));
34
-
35
- const manager = DatabaseManager.getInstance();
36
- const connection = manager.connection();
37
- const connectionName = connection.getName();
38
- const databaseConfig = Config.all('database');
39
- const dbConfig = databaseConfig.connections[connectionName];
23
+ // Private Methods
24
+ static #buildCreateSQL(blueprint, ifNotExists = false) {
25
+ const columns = blueprint.getColumns().map(col => this.#buildColumnSQL(col));
26
+ const dbConfig = Config.all('database').connections.mysql;
40
27
 
41
28
  const charset = dbConfig.charset || 'utf8mb4';
42
29
  const collation = dbConfig.collation || 'utf8mb4_unicode_ci';
43
-
44
30
  const ifNotExistsClause = ifNotExists ? 'IF NOT EXISTS ' : '';
45
- let sql = `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n`;
46
- sql += ' ' + columnsSql.join(',\n ');
47
- sql += `\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
48
31
 
49
- return sql;
32
+ return `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n ${columns.join(',\n ')}\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
50
33
  }
51
34
 
52
35
  static #buildColumnSQL(column) {
53
- let sql = `\`${column.name}\` `;
54
-
55
- switch (column.type) {
56
- case 'id':
57
- sql += 'BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY';
58
- return sql;
59
- case 'string':
60
- sql += `VARCHAR(${column.length || 255})`;
61
- break;
62
- case 'text':
63
- sql += 'TEXT';
64
- break;
65
- case 'integer':
66
- sql += 'INT';
67
- break;
68
- case 'bigInteger':
69
- sql += 'BIGINT';
70
- break;
71
- case 'boolean':
72
- sql += 'TINYINT(1)';
73
- break;
74
- case 'timestamp':
75
- sql += 'TIMESTAMP';
76
- break;
77
- case 'json':
78
- sql += 'JSON';
79
- break;
80
- default:
81
- throw new Error(`Unknown column type: ${column.type}`);
36
+ if (column.type === 'id') {
37
+ return `\`${column.name}\` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`;
82
38
  }
83
39
 
84
- if (column.modifiers) {
85
- if (column.modifiers.nullable) {
86
- sql += ' NULL';
87
- } else {
88
- sql += ' NOT NULL';
89
- }
40
+ const typeMap = {
41
+ string: `VARCHAR(${column.length || 255})`,
42
+ text: 'TEXT',
43
+ integer: 'INT',
44
+ bigInteger: 'BIGINT',
45
+ boolean: 'TINYINT(1)',
46
+ timestamp: 'TIMESTAMP',
47
+ json: 'JSON'
48
+ };
49
+
50
+ const type = typeMap[column.type];
51
+ if (!type) throw new Error(`Unknown column type: ${column.type}`);
52
+
53
+ let sql = `\`${column.name}\` ${type}`;
90
54
 
55
+ if (column.modifiers) {
56
+ sql += column.modifiers.nullable ? ' NULL' : ' NOT NULL';
91
57
  if (column.modifiers.default !== null) {
92
- const defaultValue = typeof column.modifiers.default === 'string'
58
+ const val = typeof column.modifiers.default === 'string'
93
59
  ? `'${column.modifiers.default}'`
94
60
  : column.modifiers.default;
95
- sql += ` DEFAULT ${defaultValue}`;
96
- }
97
-
98
- if (column.modifiers.unique) {
99
- sql += ' UNIQUE';
61
+ sql += ` DEFAULT ${val}`;
100
62
  }
63
+ if (column.modifiers.unique) sql += ' UNIQUE';
101
64
  }
102
65
 
103
66
  return sql;
@@ -1,183 +1,62 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
- import Checksum from '../Migration/Checksum.js';
5
- import SeederRepository from './SeederRepository.js';
6
4
  import Paths from '../../Core/Paths.js';
7
-
8
- const COLORS = {
9
- reset: '\x1b[0m',
10
- red: '\x1b[31m',
11
- green: '\x1b[32m',
12
- yellow: '\x1b[33m',
13
- cyan: '\x1b[36m',
14
- dim: '\x1b[2m',
15
- bold: '\x1b[1m'
16
- };
5
+ import Output from '../../Console/Output.js';
17
6
 
18
7
  class SeederRunner {
19
-
20
- static async run(environment = 'prod') {
21
- const seedersDir = environment === 'dev' ? Paths.seedersDev : Paths.seedersProd;
8
+ static async run(seederName = null) {
9
+ const seedersDir = Paths.seeders;
22
10
 
23
11
  if (!fs.existsSync(seedersDir)) {
24
- console.log(`${COLORS.yellow}⚠️ No seeders directory found for ${environment}${COLORS.reset}`);
12
+ Output.warn("No seeders directory found");
25
13
  return { success: true, ran: [] };
26
14
  }
27
15
 
28
- const files = fs.readdirSync(seedersDir)
29
- .filter(f => f.endsWith('.js'))
30
- .sort();
16
+ let files = fs.readdirSync(seedersDir).filter(f => f.endsWith('.js')).sort();
31
17
 
32
18
  if (files.length === 0) {
33
- console.log(`${COLORS.yellow}⚠️ No seeder files found in ${environment}${COLORS.reset}`);
19
+ Output.warn("No seeder files found");
34
20
  return { success: true, ran: [] };
35
21
  }
36
22
 
37
- const executedNames = await SeederRepository.getExecutedNames();
38
-
39
- for (const file of files) {
40
- const fullName = `${environment}/${file}`;
41
- if (executedNames.has(fullName)) {
42
- const filePath = path.join(seedersDir, file);
43
- const currentChecksum = Checksum.fromFile(filePath);
44
- const storedChecksum = await SeederRepository.getChecksum(fullName);
45
-
46
- if (currentChecksum !== storedChecksum) {
47
- console.error(`${COLORS.red}❌ CHECKSUM MISMATCH: ${fullName}${COLORS.reset}`);
48
- console.error(`${COLORS.dim} Stored: ${storedChecksum}${COLORS.reset}`);
49
- console.error(`${COLORS.dim} Current: ${currentChecksum}${COLORS.reset}`);
50
- console.error(`${COLORS.red} Seeder files must NEVER be modified after execution.${COLORS.reset}`);
51
- console.error(`${COLORS.red} Create a NEW seeder for any data changes.${COLORS.reset}`);
52
- return {
53
- success: false,
54
- ran: [],
55
- error: new Error(`Checksum mismatch for seeder: ${fullName}`)
56
- };
57
- }
23
+ if (seederName) {
24
+ const targetFile = seederName.endsWith('.js') ? seederName : `${seederName}.js`;
25
+ files = files.filter(f => f === targetFile);
26
+ if (files.length === 0) {
27
+ Output.error(`Seeder not found: ${seederName}`);
28
+ return { success: false, ran: [], error: new Error(`Seeder not found: ${seederName}`) };
58
29
  }
59
30
  }
60
31
 
61
- const pending = files.filter(f => !executedNames.has(`${environment}/${f}`));
62
-
63
- if (pending.length === 0) {
64
- console.log(`${COLORS.green}✅ Nothing to seed. All ${environment} seeders are up to date.${COLORS.reset}`);
65
- return { success: true, ran: [] };
66
- }
67
-
68
- console.log(`${COLORS.cyan}🌱 Running ${environment} seeders${COLORS.reset}\n`);
32
+ Output.seederHeader();
69
33
 
70
34
  const executed = [];
71
35
 
72
36
  try {
73
- for (const file of pending) {
74
- const filePath = path.join(seedersDir, file);
75
- const fileUrl = pathToFileURL(filePath).href;
76
- const checksum = Checksum.fromFile(filePath);
77
- const fullName = `${environment}/${file}`;
78
-
79
- console.log(`${COLORS.dim}Seeding:${COLORS.reset} ${COLORS.cyan}${fullName}${COLORS.reset}`);
80
-
81
- const { default: seeder } = await import(fileUrl);
37
+ for (const file of files) {
38
+ const fileUrl = pathToFileURL(path.join(seedersDir, file)).href;
39
+ Output.pending("Seeding", file);
82
40
 
41
+ const { default: seeder } = await import(`${fileUrl}?t=${Date.now()}`);
83
42
  if (typeof seeder.run !== 'function') {
84
43
  throw new Error(`Seeder ${file} does not have a run() method`);
85
44
  }
86
45
 
87
46
  await seeder.run();
88
- await SeederRepository.log(fullName, checksum);
89
- executed.push(fullName);
90
-
91
- console.log(`${COLORS.green}✅ Seeded:${COLORS.reset} ${COLORS.cyan}${fullName}${COLORS.reset}\n`);
47
+ executed.push(file);
48
+ Output.done("Seeded", file);
92
49
  }
93
50
 
94
- console.log(`${COLORS.green}${COLORS.bold}✅ All ${environment} seeders completed successfully.${COLORS.reset}`);
51
+ Output.seederSuccess();
95
52
  return { success: true, ran: executed };
96
-
97
- } catch (error) {
98
- console.error(`\n${COLORS.red}❌ Seeding failed: ${error.message}${COLORS.reset}`);
99
- return { success: false, ran: executed, error };
100
- }
101
- }
102
-
103
- static async runAll() {
104
- console.log(`${COLORS.bold}Running prod seeders...${COLORS.reset}\n`);
105
- const prodResult = await this.run('prod');
106
-
107
- if (!prodResult.success) {
108
- return prodResult;
109
53
  }
110
-
111
- console.log(`\n${COLORS.bold}Running dev seeders...${COLORS.reset}\n`);
112
- const devResult = await this.run('dev');
113
-
114
- return {
115
- success: devResult.success,
116
- ran: [...prodResult.ran, ...devResult.ran],
117
- error: devResult.error
118
- };
119
- }
120
-
121
- static async status() {
122
- const prodDir = Paths.seedersProd;
123
- const devDir = Paths.seedersDev;
124
-
125
- const status = [];
126
-
127
- if (fs.existsSync(prodDir)) {
128
- const prodFiles = fs.readdirSync(prodDir).filter(f => f.endsWith('.js')).sort();
129
- for (const file of prodFiles) {
130
- const fullName = `prod/${file}`;
131
- const record = await SeederRepository.find(fullName);
132
- status.push({
133
- name: fullName,
134
- status: record ? 'Ran' : 'Pending',
135
- executedAt: record?.executed_at || null
136
- });
137
- }
138
- }
139
-
140
- if (fs.existsSync(devDir)) {
141
- const devFiles = fs.readdirSync(devDir).filter(f => f.endsWith('.js')).sort();
142
- for (const file of devFiles) {
143
- const fullName = `dev/${file}`;
144
- const record = await SeederRepository.find(fullName);
145
- status.push({
146
- name: fullName,
147
- status: record ? 'Ran' : 'Pending',
148
- executedAt: record?.executed_at || null
149
- });
150
- }
151
- }
152
-
153
- return status;
154
- }
155
-
156
- static async printStatus() {
157
- const status = await this.status();
158
-
159
- if (status.length === 0) {
160
- console.log(`${COLORS.yellow}⚠️ No seeders found.${COLORS.reset}`);
161
- return;
162
- }
163
-
164
- console.log(`\n${COLORS.bold}Seeder Status${COLORS.reset}\n`);
165
- console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
166
-
167
- for (const seeder of status) {
168
- const statusColor = seeder.status === 'Ran' ? COLORS.green : COLORS.yellow;
169
- const statusIcon = seeder.status === 'Ran' ? '✅' : '⏳';
170
-
171
- console.log(`${statusIcon} ${statusColor}${seeder.status.padEnd(7)}${COLORS.reset} ${seeder.name}`);
54
+ catch (error) {
55
+ Output.newline();
56
+ Output.error(`Seeding failed: ${error.message}`);
57
+ return { success: false, ran: executed, error };
172
58
  }
173
-
174
- console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}\n`);
175
-
176
- const ran = status.filter(s => s.status === 'Ran').length;
177
- const pending = status.filter(s => s.status === 'Pending').length;
178
- console.log(`${COLORS.dim}Total: ${status.length} | Ran: ${ran} | Pending: ${pending}${COLORS.reset}\n`);
179
59
  }
180
-
181
60
  }
182
61
 
183
62
  export default SeederRunner;
@@ -1,6 +1,15 @@
1
1
  import locale from './Locale.js';
2
2
  import Config from '../Core/Config.js';
3
3
 
4
+ /**
5
+ * Date and time utility with timezone and localization support.
6
+ * Uses config/app.js timezone and locale settings.
7
+ *
8
+ * @example
9
+ * DateTime.toSQL(); // "2025-01-15 10:30:00"
10
+ * DateTime.addDays(7); // 7 days from now
11
+ * DateTime.diffForHumans(ts); // "2 hours ago"
12
+ */
4
13
  class DateTime {
5
14
  static #getDate(date = null) {
6
15
  const timezone = Config.get('app.timezone', 'UTC');
@@ -0,0 +1,52 @@
1
+ import crypto from "crypto";
2
+
3
+ /**
4
+ * AES-256-CBC encryption and decryption utility.
5
+ * Uses APP_KEY from environment for secure encryption.
6
+ */
7
+ class Encryption {
8
+ /**
9
+ * Encrypts a value using AES-256-CBC.
10
+ * @param {string|Object} value - Value to encrypt (objects are JSON stringified)
11
+ * @returns {string} Encrypted string in format "iv:encryptedData"
12
+ */
13
+ static encrypt(value) {
14
+ if (typeof value === "object") {
15
+ value = JSON.stringify(value);
16
+ }
17
+
18
+ const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
19
+ const iv = crypto.randomBytes(16);
20
+ const cipher = crypto.createCipheriv("aes-256-cbc", secretKey, iv);
21
+
22
+ let encrypted = cipher.update(value, "utf8", "hex");
23
+ encrypted += cipher.final("hex");
24
+
25
+ return iv.toString("hex") + ":" + encrypted;
26
+ }
27
+
28
+ /**
29
+ * Decrypts an AES-256-CBC encrypted value.
30
+ * @param {string} encryptedValue - Encrypted string in format "iv:encryptedData"
31
+ * @returns {string|false} Decrypted string or false if decryption fails
32
+ */
33
+ static decrypt(encryptedValue) {
34
+ try {
35
+ const parts = encryptedValue.split(":");
36
+ const iv = Buffer.from(parts.shift(), "hex");
37
+ const encryptedText = Buffer.from(parts.join(":"), "hex");
38
+ const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
39
+ const decipher = crypto.createDecipheriv("aes-256-cbc", secretKey, iv);
40
+
41
+ let decrypted = decipher.update(encryptedText, "hex", "utf8");
42
+ decrypted += decipher.final("utf8");
43
+
44
+ return decrypted;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ }
51
+
52
+ export default Encryption;