@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.
- package/README.md +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +17 -19
- package/lib/Auth/Auth.js +167 -0
- package/lib/Build/CssBuilder.js +9 -0
- package/lib/Build/FileAnalyzer.js +16 -0
- package/lib/Build/HydrationBuilder.js +17 -0
- package/lib/Build/Manager.js +15 -0
- package/lib/Build/colors.js +4 -0
- package/lib/Build/plugins.js +84 -20
- package/lib/Console/Commands/DevCommand.js +13 -9
- package/lib/Console/Commands/MakeCommand.js +24 -10
- package/lib/Console/Commands/MigrateCommand.js +4 -3
- package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
- package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
- package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
- package/lib/Console/Commands/SeedCommand.js +8 -28
- package/lib/Console/Commands/StorageLinkCommand.js +20 -5
- package/lib/Console/Output.js +143 -0
- package/lib/Core/Config.js +2 -1
- package/lib/Core/Paths.js +8 -8
- package/lib/Database/DB.js +141 -51
- package/lib/Database/Drivers/MySQLDriver.js +102 -157
- package/lib/Database/Migration/Checksum.js +3 -8
- package/lib/Database/Migration/MigrationRepository.js +25 -35
- package/lib/Database/Migration/MigrationRunner.js +59 -67
- package/lib/Database/Model.js +165 -75
- package/lib/Database/QueryBuilder.js +43 -0
- package/lib/Database/QueryValidation.js +51 -30
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +24 -145
- package/lib/Date/DateTime.js +9 -0
- package/lib/Encryption/Encryption.js +52 -0
- package/lib/Faker/Faker.js +11 -0
- package/lib/Filesystem/Storage.js +120 -0
- package/lib/HMR/Server.js +79 -9
- package/lib/Hashing/Hash.js +41 -0
- package/lib/Http/Server.js +179 -151
- package/lib/Logging/{Manager.js → Log.js} +68 -80
- package/lib/Mail/Mail.js +187 -0
- package/lib/Route/Router.js +416 -0
- package/lib/Session/File.js +135 -233
- package/lib/Session/Manager.js +117 -171
- package/lib/Session/Memory.js +28 -38
- package/lib/Session/Session.js +71 -107
- package/lib/Support/Str.js +103 -0
- package/lib/Translation/Lang.js +54 -0
- package/lib/View/Client/hmr-client.js +87 -51
- package/lib/View/Client/nitronjs-icon.png +0 -0
- package/lib/View/{Manager.js → View.js} +44 -29
- package/lib/index.d.ts +49 -27
- package/lib/index.js +19 -13
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/package.json +2 -0
- package/skeleton/resources/css/global.css +1 -0
- package/skeleton/resources/views/Site/Home.tsx +456 -79
- package/skeleton/tsconfig.json +6 -1
- package/lib/Auth/Manager.js +0 -111
- package/lib/Database/Connection.js +0 -61
- package/lib/Database/Manager.js +0 -162
- package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
- package/lib/Database/Seeder/SeederRepository.js +0 -45
- package/lib/Encryption/Manager.js +0 -47
- package/lib/Filesystem/Manager.js +0 -74
- package/lib/Hashing/Manager.js +0 -25
- package/lib/Mail/Manager.js +0 -120
- package/lib/Route/Loader.js +0 -80
- package/lib/Route/Manager.js +0 -286
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
await connection.raw(`DROP TABLE IF EXISTS \`${tableName}\``);
|
|
20
|
+
await DB.rawQuery(`DROP TABLE IF EXISTS \`${tableName}\``);
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
58
|
+
const val = typeof column.modifiers.default === 'string'
|
|
93
59
|
? `'${column.modifiers.default}'`
|
|
94
60
|
: column.modifiers.default;
|
|
95
|
-
sql += ` DEFAULT ${
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
Output.warn("No seeders directory found");
|
|
25
13
|
return { success: true, ran: [] };
|
|
26
14
|
}
|
|
27
15
|
|
|
28
|
-
|
|
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
|
-
|
|
19
|
+
Output.warn("No seeder files found");
|
|
34
20
|
return { success: true, ran: [] };
|
|
35
21
|
}
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
const
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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;
|
package/lib/Date/DateTime.js
CHANGED
|
@@ -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;
|