@nitronjs/framework 0.2.24 → 0.2.26
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/cli/njs.js +2 -2
- package/lib/Database/DB.js +33 -5
- package/lib/Database/Drivers/MySQLDriver.js +7 -3
- package/lib/Database/Model.js +21 -9
- package/lib/Database/QueryBuilder.js +116 -27
- package/lib/Database/QueryValidation.js +32 -0
- package/lib/Database/Schema/Blueprint.js +28 -4
- package/lib/Database/Schema/Manager.js +47 -5
- package/lib/Encryption/Encryption.js +19 -11
- package/lib/Filesystem/Storage.js +3 -1
- package/lib/Http/Server.js +6 -1
- package/lib/Logging/Log.js +9 -3
- package/lib/Route/Router.js +36 -17
- package/lib/Session/Manager.js +15 -4
- package/lib/View/View.js +16 -15
- package/lib/index.d.ts +55 -11
- package/package.json +1 -1
- package/skeleton/app/Middlewares/VerifyCsrf.js +2 -2
package/cli/njs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const COLORS = {
|
|
4
4
|
reset: "\x1b[0m",
|
|
@@ -183,7 +183,7 @@ async function run() {
|
|
|
183
183
|
if (exitCode !== null) {
|
|
184
184
|
const { default: checkForUpdates } = await import("../lib/Console/UpdateChecker.js");
|
|
185
185
|
await checkForUpdates();
|
|
186
|
-
process.exitCode
|
|
186
|
+
process.exit(exitCode);
|
|
187
187
|
}
|
|
188
188
|
} catch (error) {
|
|
189
189
|
console.error(`${COLORS.red}Error: ${error.message}${COLORS.reset}`);
|
package/lib/Database/DB.js
CHANGED
|
@@ -74,17 +74,18 @@ class DB {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* Executes a raw SQL query.
|
|
78
|
-
* @param {string} sql - Raw SQL string
|
|
77
|
+
* Executes a raw SQL query with optional parameter bindings.
|
|
78
|
+
* @param {string} sql - Raw SQL string (use ? for parameter placeholders)
|
|
79
|
+
* @param {Array} bindings - Parameter bindings for safe query execution
|
|
79
80
|
* @returns {Promise<Array>} Query result
|
|
80
81
|
* @throws {Error} If database is disabled
|
|
81
82
|
*/
|
|
82
|
-
static async rawQuery(sql) {
|
|
83
|
+
static async rawQuery(sql, bindings = []) {
|
|
83
84
|
if (!this.#driver) {
|
|
84
85
|
throw new Error("Database is disabled (DATABASE_DRIVER=none)");
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
return await this.#driver.raw(sql);
|
|
88
|
+
return await this.#driver.raw(sql, bindings);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
@@ -105,7 +106,27 @@ class DB {
|
|
|
105
106
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
106
107
|
|
|
107
108
|
try {
|
|
108
|
-
const result = await this.#driver.withTransaction(
|
|
109
|
+
const result = await this.#driver.withTransaction((connection) => {
|
|
110
|
+
const connectionWrapper = {
|
|
111
|
+
query: (sql, bindings) => connection.execute(sql, bindings)
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const trx = {
|
|
115
|
+
table: (table, modelClass = null) => createQueryBuilder(table, connectionWrapper, modelClass),
|
|
116
|
+
rawQuery: (sql, bindings = []) => {
|
|
117
|
+
if (bindings.length > 0) {
|
|
118
|
+
return connection.execute(sql, bindings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return connection.query(sql);
|
|
122
|
+
},
|
|
123
|
+
query: (...args) => connection.query(...args),
|
|
124
|
+
execute: (...args) => connection.execute(...args)
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return callback(trx);
|
|
128
|
+
}, { signal: controller.signal });
|
|
129
|
+
|
|
109
130
|
clearTimeout(timeoutId);
|
|
110
131
|
|
|
111
132
|
return result;
|
|
@@ -147,6 +168,13 @@ class DB {
|
|
|
147
168
|
};
|
|
148
169
|
|
|
149
170
|
this.#driver = this.#createDriver("mysql");
|
|
171
|
+
|
|
172
|
+
const isHealthy = await this.#driver.healthCheck();
|
|
173
|
+
|
|
174
|
+
if (!isHealthy) {
|
|
175
|
+
this.#driver = null;
|
|
176
|
+
throw new Error("Database reconfiguration failed: health check failed. Check your credentials.");
|
|
177
|
+
}
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
/** @private */
|
|
@@ -30,11 +30,11 @@ class MySQLDriver {
|
|
|
30
30
|
charset: this.#config.charset || "utf8mb4",
|
|
31
31
|
waitForConnections: true,
|
|
32
32
|
connectionLimit: this.#config.pool?.max ?? 10,
|
|
33
|
+
idleTimeout: this.#config.pool?.idleTimeout ?? 60000,
|
|
33
34
|
queueLimit: this.#config.pool?.queueLimit ?? 100,
|
|
34
35
|
connectTimeout: 10000,
|
|
35
36
|
enableKeepAlive: true,
|
|
36
37
|
keepAliveInitialDelay: 10000,
|
|
37
|
-
namedPlaceholders: true,
|
|
38
38
|
decimalNumbers: true
|
|
39
39
|
});
|
|
40
40
|
}
|
|
@@ -67,12 +67,16 @@ class MySQLDriver {
|
|
|
67
67
|
* @param {string} sql - Raw SQL string
|
|
68
68
|
* @returns {Promise<Array>} Query result
|
|
69
69
|
*/
|
|
70
|
-
async raw(sql) {
|
|
70
|
+
async raw(sql, bindings = []) {
|
|
71
71
|
try {
|
|
72
|
+
if (bindings.length > 0) {
|
|
73
|
+
return await this.#pool.execute(sql, bindings);
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
return await this.#pool.query(sql);
|
|
73
77
|
}
|
|
74
78
|
catch (error) {
|
|
75
|
-
throw this.#handleError(error, sql);
|
|
79
|
+
throw this.#handleError(error, sql, bindings.length > 0 ? bindings : null);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
package/lib/Database/Model.js
CHANGED
|
@@ -175,18 +175,30 @@ class Model {
|
|
|
175
175
|
*/
|
|
176
176
|
async save() {
|
|
177
177
|
const table = this.constructor.table;
|
|
178
|
-
const data = {};
|
|
179
178
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
if (this._exists) {
|
|
180
|
+
const dirty = {};
|
|
181
|
+
|
|
182
|
+
for (const [key, value] of Object.entries(this._attributes)) {
|
|
183
|
+
if (key === 'id') continue;
|
|
184
|
+
if (value !== this._original[key]) {
|
|
185
|
+
dirty[key] = value;
|
|
186
|
+
}
|
|
183
187
|
}
|
|
184
|
-
}
|
|
185
188
|
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
if (Object.keys(dirty).length > 0) {
|
|
190
|
+
await DB.table(table).where('id', this._attributes.id).update(dirty);
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
193
|
else {
|
|
194
|
+
const data = {};
|
|
195
|
+
|
|
196
|
+
for (const [key, value] of Object.entries(this._attributes)) {
|
|
197
|
+
if (value !== undefined) {
|
|
198
|
+
data[key] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
190
202
|
const id = await DB.table(table).insert(data);
|
|
191
203
|
this._attributes.id = id;
|
|
192
204
|
this._exists = true;
|
|
@@ -264,10 +276,10 @@ function hydrate(modelClass, row) {
|
|
|
264
276
|
|
|
265
277
|
const QUERY_METHODS = [
|
|
266
278
|
// Chain methods (return QueryBuilder)
|
|
267
|
-
'distinct', 'orWhere', 'whereIn', 'whereNotIn',
|
|
279
|
+
'selectRaw', 'distinct', 'orWhere', 'whereIn', 'whereNotIn',
|
|
268
280
|
'whereBetween', 'whereNot', 'join', 'groupBy', 'offset',
|
|
269
281
|
// Terminal methods (return results)
|
|
270
|
-
'count', 'max', 'min', 'sum', 'avg', 'delete'
|
|
282
|
+
'count', 'countDistinct', 'max', 'min', 'sum', 'avg', 'delete'
|
|
271
283
|
];
|
|
272
284
|
|
|
273
285
|
for (const method of QUERY_METHODS) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
|
|
1
|
+
import { validateDirection, validateIdentifier, validateRawExpression, validateWhereOperator } from "./QueryValidation.js";
|
|
2
2
|
import Environment from "../Core/Environment.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -22,7 +22,6 @@ class QueryBuilder {
|
|
|
22
22
|
#joins = [];
|
|
23
23
|
#orders = [];
|
|
24
24
|
#groups = [];
|
|
25
|
-
#havings = [];
|
|
26
25
|
#limitValue = null;
|
|
27
26
|
#offsetValue = null;
|
|
28
27
|
#distinctFlag = false;
|
|
@@ -90,19 +89,21 @@ class QueryBuilder {
|
|
|
90
89
|
return operator;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
#validateInteger(value) {
|
|
92
|
+
#validateInteger(value, maxAllowed = 10000000) {
|
|
94
93
|
const stringValue = String(value).trim();
|
|
94
|
+
|
|
95
95
|
if (!/^\d+$/.test(stringValue)) {
|
|
96
96
|
throw new Error(`Invalid integer: "${value}". Must be a pure positive number.`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const num = parseInt(stringValue, 10);
|
|
100
|
+
|
|
100
101
|
if (isNaN(num) || num < 0) {
|
|
101
102
|
throw new Error(`Invalid integer: "${value}". Must be a non-negative number.`);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
if (num >
|
|
105
|
-
throw new Error(`Invalid integer: "${value}". Maximum allowed value is
|
|
105
|
+
if (num > maxAllowed) {
|
|
106
|
+
throw new Error(`Invalid integer: "${value}". Maximum allowed value is ${maxAllowed}.`);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
return num;
|
|
@@ -157,7 +158,6 @@ class QueryBuilder {
|
|
|
157
158
|
this.#joins = [];
|
|
158
159
|
this.#orders = [];
|
|
159
160
|
this.#groups = [];
|
|
160
|
-
this.#havings = [];
|
|
161
161
|
this.#limitValue = null;
|
|
162
162
|
this.#offsetValue = null;
|
|
163
163
|
this.#distinctFlag = false;
|
|
@@ -179,6 +179,45 @@ class QueryBuilder {
|
|
|
179
179
|
return this;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Add a raw expression to the SELECT clause.
|
|
184
|
+
* If the current selection is the default '*', replaces it.
|
|
185
|
+
* Otherwise appends to existing selections.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} expression - Raw SQL expression
|
|
188
|
+
* @returns {QueryBuilder}
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* // Standalone
|
|
192
|
+
* DB.table("orders").selectRaw("COUNT(*) as total").first();
|
|
193
|
+
*
|
|
194
|
+
* // Combined with select
|
|
195
|
+
* DB.table("orders")
|
|
196
|
+
* .select("status")
|
|
197
|
+
* .selectRaw("COUNT(*) as total")
|
|
198
|
+
* .selectRaw("AVG(amount) as avg_amount")
|
|
199
|
+
* .groupBy("status")
|
|
200
|
+
* .get();
|
|
201
|
+
*/
|
|
202
|
+
selectRaw(expression) {
|
|
203
|
+
if (typeof expression !== 'string' || expression.trim() === '') {
|
|
204
|
+
throw new Error('selectRaw() requires a non-empty string expression.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
validateRawExpression(expression);
|
|
208
|
+
|
|
209
|
+
const raw = new RawExpression(expression);
|
|
210
|
+
|
|
211
|
+
if (this.#selectColumns.length === 1 && this.#selectColumns[0] === '*') {
|
|
212
|
+
this.#selectColumns = [raw];
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
this.#selectColumns.push(raw);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
distinct() {
|
|
183
222
|
this.#distinctFlag = true;
|
|
184
223
|
|
|
@@ -206,6 +245,19 @@ class QueryBuilder {
|
|
|
206
245
|
operator = this.#validateWhereOperator(operator);
|
|
207
246
|
value = this.#validateWhereValue(value);
|
|
208
247
|
|
|
248
|
+
// where("column", null) → IS NULL, where("column", "!=", null) → IS NOT NULL
|
|
249
|
+
if (value === null || value === undefined) {
|
|
250
|
+
const isNot = operator === '!=' || operator === '<>' || operator === 'IS NOT';
|
|
251
|
+
|
|
252
|
+
this.#wheres.push({
|
|
253
|
+
type: isNot ? 'notNull' : 'null',
|
|
254
|
+
column: this.#validateIdentifier(column),
|
|
255
|
+
boolean: 'AND'
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
|
|
209
261
|
this.#wheres.push({
|
|
210
262
|
type: 'basic',
|
|
211
263
|
column: this.#validateIdentifier(column),
|
|
@@ -228,6 +280,18 @@ class QueryBuilder {
|
|
|
228
280
|
operator = this.#validateWhereOperator(operator);
|
|
229
281
|
value = this.#validateWhereValue(value);
|
|
230
282
|
|
|
283
|
+
if (value === null || value === undefined) {
|
|
284
|
+
const isNot = operator === '!=' || operator === '<>' || operator === 'IS NOT';
|
|
285
|
+
|
|
286
|
+
this.#wheres.push({
|
|
287
|
+
type: isNot ? 'notNull' : 'null',
|
|
288
|
+
column: this.#validateIdentifier(column),
|
|
289
|
+
boolean: 'OR'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
|
|
231
295
|
this.#wheres.push({
|
|
232
296
|
type: 'basic',
|
|
233
297
|
column: this.#validateIdentifier(column),
|
|
@@ -363,7 +427,7 @@ class QueryBuilder {
|
|
|
363
427
|
}
|
|
364
428
|
|
|
365
429
|
limit(value) {
|
|
366
|
-
this.#limitValue = this.#validateInteger(value);
|
|
430
|
+
this.#limitValue = this.#validateInteger(value, 100000);
|
|
367
431
|
|
|
368
432
|
return this;
|
|
369
433
|
}
|
|
@@ -453,6 +517,40 @@ class QueryBuilder {
|
|
|
453
517
|
return parseInt(result) || 0;
|
|
454
518
|
}
|
|
455
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Count distinct values of a column.
|
|
522
|
+
* @param {string} column - Column name to count distinct values of
|
|
523
|
+
* @returns {Promise<number>}
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* const uniqueVisitors = await Log_View
|
|
527
|
+
* .where("created_at", ">=", "2026-01-01")
|
|
528
|
+
* .countDistinct("ip_address");
|
|
529
|
+
*/
|
|
530
|
+
async countDistinct(column) {
|
|
531
|
+
const connection = await this.#getConnection();
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const validatedColumn = this.#validateIdentifier(column);
|
|
535
|
+
const originalSelect = this.#selectColumns;
|
|
536
|
+
this.#selectColumns = [new RawExpression(`COUNT(DISTINCT ${validatedColumn}) as count_distinct`)];
|
|
537
|
+
this.#limitValue = 1;
|
|
538
|
+
|
|
539
|
+
const sql = this.#toSql();
|
|
540
|
+
const [rows] = await connection.query(sql, this.#bindings);
|
|
541
|
+
|
|
542
|
+
this.#selectColumns = originalSelect;
|
|
543
|
+
|
|
544
|
+
return rows[0] ? parseInt(rows[0].count_distinct) || 0 : 0;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
throw this.#sanitizeError(error);
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
this.#reset();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
456
554
|
async max(column) {
|
|
457
555
|
return await this.#aggregate('MAX', column);
|
|
458
556
|
}
|
|
@@ -474,20 +572,24 @@ class QueryBuilder {
|
|
|
474
572
|
}
|
|
475
573
|
|
|
476
574
|
async #aggregate(func, column) {
|
|
575
|
+
const connection = await this.#getConnection();
|
|
477
576
|
const alias = func.toLowerCase();
|
|
478
|
-
const originalSelect = this.#selectColumns;
|
|
479
577
|
|
|
480
578
|
try {
|
|
481
579
|
const validatedColumn = column === '*' ? '*' : this.#validateIdentifier(column);
|
|
580
|
+
const originalSelect = this.#selectColumns;
|
|
482
581
|
this.#selectColumns = [new RawExpression(`${func}(${validatedColumn}) as ${alias}`)];
|
|
582
|
+
this.#limitValue = 1;
|
|
483
583
|
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
return row ? row[alias] : null;
|
|
487
|
-
}
|
|
584
|
+
const sql = this.#toSql();
|
|
585
|
+
const [rows] = await connection.query(sql, this.#bindings);
|
|
488
586
|
|
|
489
|
-
finally {
|
|
490
587
|
this.#selectColumns = originalSelect;
|
|
588
|
+
|
|
589
|
+
return rows[0] ? rows[0][alias] : null;
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
throw this.#sanitizeError(error);
|
|
491
593
|
}
|
|
492
594
|
}
|
|
493
595
|
|
|
@@ -585,11 +687,10 @@ class QueryBuilder {
|
|
|
585
687
|
const joins = this.#compileJoins();
|
|
586
688
|
const wheres = this.#compileWheres();
|
|
587
689
|
const groups = this.#compileGroupBy();
|
|
588
|
-
const havings = this.#compileHaving();
|
|
589
690
|
const orders = this.#compileOrders();
|
|
590
691
|
const limit = this.#compileLimit();
|
|
591
692
|
|
|
592
|
-
return `SELECT ${distinct}${columns} FROM ${this.#quoteIdentifier(this.#table)}${joins}${wheres}${groups}${
|
|
693
|
+
return `SELECT ${distinct}${columns} FROM ${this.#quoteIdentifier(this.#table)}${joins}${wheres}${groups}${orders}${limit}`;
|
|
593
694
|
}
|
|
594
695
|
|
|
595
696
|
#compileJoins() {
|
|
@@ -647,19 +748,7 @@ class QueryBuilder {
|
|
|
647
748
|
return ` GROUP BY ${this.#groups.join(', ')}`;
|
|
648
749
|
}
|
|
649
750
|
|
|
650
|
-
#compileHaving() {
|
|
651
|
-
if (this.#havings.length === 0) {
|
|
652
|
-
return '';
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const compiled = this.#havings.map((having, index) => {
|
|
656
|
-
const boolean = index === 0 ? '' : ' AND ';
|
|
657
|
-
|
|
658
|
-
return `${boolean}${having.column} ${having.operator} ?`;
|
|
659
|
-
}).join('');
|
|
660
751
|
|
|
661
|
-
return ` HAVING ${compiled}`;
|
|
662
|
-
}
|
|
663
752
|
|
|
664
753
|
#compileOrders() {
|
|
665
754
|
if (this.#orders.length === 0) {
|
|
@@ -74,6 +74,38 @@ export function validateDirection(direction) {
|
|
|
74
74
|
return upper;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Validates a raw SQL expression for use in selectRaw().
|
|
79
|
+
* Blocks dangerous patterns while allowing legitimate aggregate/function expressions.
|
|
80
|
+
*
|
|
81
|
+
* Allowed: COUNT(*), AVG(col), DISTINCT, AS aliases, DATE(), CASE WHEN, math operators
|
|
82
|
+
* Blocked: Semicolons, DDL (DROP/ALTER/CREATE/TRUNCATE), DML writes (INSERT/UPDATE/DELETE),
|
|
83
|
+
* UNION, SQL comments, system functions (LOAD_FILE, INTO OUTFILE), subqueries
|
|
84
|
+
*
|
|
85
|
+
* @param {string} expression - Raw SQL expression to validate
|
|
86
|
+
* @throws {Error} If expression contains dangerous patterns
|
|
87
|
+
*/
|
|
88
|
+
export function validateRawExpression(expression) {
|
|
89
|
+
if (typeof expression !== 'string' || expression.trim() === '') {
|
|
90
|
+
throw new Error('Raw expression must be a non-empty string.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (/;/.test(expression)) {
|
|
94
|
+
throw new Error(`Dangerous raw expression: semicolons are not allowed.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (/--|#|\/\*|\*\//.test(expression)) {
|
|
98
|
+
throw new Error(`Dangerous raw expression: SQL comments are not allowed.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dangerous = /\b(DROP|ALTER|CREATE|TRUNCATE|INSERT|UPDATE|DELETE|REPLACE|GRANT|REVOKE|UNION|INTO\s+OUTFILE|INTO\s+DUMPFILE|LOAD_FILE|BENCHMARK|SLEEP|INFORMATION_SCHEMA)\b/i;
|
|
102
|
+
|
|
103
|
+
if (dangerous.test(expression)) {
|
|
104
|
+
const match = expression.match(dangerous);
|
|
105
|
+
throw new Error(`Dangerous raw expression: "${match[0].toUpperCase()}" is not allowed in selectRaw().`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
110
|
// Private Functions
|
|
79
111
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
class Blueprint {
|
|
2
2
|
#tableName;
|
|
3
3
|
#columns = [];
|
|
4
|
+
#indexes = [];
|
|
5
|
+
#dropIndexes = [];
|
|
4
6
|
|
|
5
7
|
constructor(tableName) {
|
|
6
8
|
this.#tableName = tableName;
|
|
@@ -18,6 +20,24 @@ class Blueprint {
|
|
|
18
20
|
return this.#columns;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
getIndexes() {
|
|
24
|
+
return this.#indexes;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getDropIndexes() {
|
|
28
|
+
return this.#dropIndexes;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
index(columns) {
|
|
32
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
33
|
+
const name = `idx_${this.#tableName}_${cols.join('_')}`;
|
|
34
|
+
this.#indexes.push({ name, columns: cols });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dropIndex(name) {
|
|
38
|
+
this.#dropIndexes.push(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
id() {
|
|
22
42
|
this.#columns.push({ name: 'id', type: 'id', modifiers: {} });
|
|
23
43
|
return this;
|
|
@@ -64,11 +84,15 @@ class Blueprint {
|
|
|
64
84
|
};
|
|
65
85
|
this.#columns.push(column);
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
const blueprint = this;
|
|
88
|
+
const chain = {
|
|
89
|
+
nullable() { column.modifiers.nullable = true; return chain; },
|
|
90
|
+
default(value) { column.modifiers.default = value; return chain; },
|
|
91
|
+
unique() { column.modifiers.unique = true; return chain; },
|
|
92
|
+
index() { blueprint.index(name); return chain; },
|
|
71
93
|
};
|
|
94
|
+
|
|
95
|
+
return chain;
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
|
|
@@ -16,6 +16,17 @@ export default class Schema {
|
|
|
16
16
|
await DB.rawQuery(this.#buildCreateSQL(blueprint, true));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
static async table(tableName, callback) {
|
|
20
|
+
const blueprint = new (await import('./Blueprint.js')).default(tableName);
|
|
21
|
+
callback(blueprint);
|
|
22
|
+
|
|
23
|
+
const statements = this.#buildAlterSQL(blueprint);
|
|
24
|
+
|
|
25
|
+
for (const sql of statements) {
|
|
26
|
+
await DB.rawQuery(sql);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
static async dropIfExists(tableName) {
|
|
20
31
|
await DB.rawQuery(`DROP TABLE IF EXISTS \`${tableName}\``);
|
|
21
32
|
}
|
|
@@ -23,13 +34,36 @@ export default class Schema {
|
|
|
23
34
|
// Private Methods
|
|
24
35
|
static #buildCreateSQL(blueprint, ifNotExists = false) {
|
|
25
36
|
const columns = blueprint.getColumns().map(col => this.#buildColumnSQL(col));
|
|
37
|
+
const indexes = blueprint.getIndexes().map(idx => this.#buildIndexSQL(idx));
|
|
38
|
+
const definitions = [...columns, ...indexes];
|
|
26
39
|
const dbConfig = Config.all('database').connections.mysql;
|
|
27
40
|
|
|
28
41
|
const charset = dbConfig.charset || 'utf8mb4';
|
|
29
42
|
const collation = dbConfig.collation || 'utf8mb4_unicode_ci';
|
|
30
43
|
const ifNotExistsClause = ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
31
44
|
|
|
32
|
-
return `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n ${
|
|
45
|
+
return `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n ${definitions.join(',\n ')}\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static #buildAlterSQL(blueprint) {
|
|
49
|
+
const tableName = blueprint.getTableName();
|
|
50
|
+
const statements = [];
|
|
51
|
+
|
|
52
|
+
for (const idx of blueprint.getIndexes()) {
|
|
53
|
+
const cols = idx.columns.map(c => `\`${c}\``).join(', ');
|
|
54
|
+
statements.push(`ALTER TABLE \`${tableName}\` ADD INDEX \`${idx.name}\` (${cols})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const name of blueprint.getDropIndexes()) {
|
|
58
|
+
statements.push(`ALTER TABLE \`${tableName}\` DROP INDEX \`${name}\``);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return statements;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static #buildIndexSQL(idx) {
|
|
65
|
+
const cols = idx.columns.map(c => `\`${c}\``).join(', ');
|
|
66
|
+
return `INDEX \`${idx.name}\` (${cols})`;
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
static #buildColumnSQL(column) {
|
|
@@ -55,10 +89,18 @@ export default class Schema {
|
|
|
55
89
|
if (column.modifiers) {
|
|
56
90
|
sql += column.modifiers.nullable ? ' NULL' : ' NOT NULL';
|
|
57
91
|
if (column.modifiers.default !== null) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
const raw = column.modifiers.default;
|
|
93
|
+
|
|
94
|
+
if (typeof raw === 'string') {
|
|
95
|
+
const escaped = raw.replace(/'/g, "''");
|
|
96
|
+
sql += ` DEFAULT '${escaped}'`;
|
|
97
|
+
}
|
|
98
|
+
else if (typeof raw === 'boolean') {
|
|
99
|
+
sql += ` DEFAULT ${raw ? 1 : 0}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
sql += ` DEFAULT ${Number(raw)}`;
|
|
103
|
+
}
|
|
62
104
|
}
|
|
63
105
|
if (column.modifiers.unique) sql += ' UNIQUE';
|
|
64
106
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* AES-256-
|
|
4
|
+
* AES-256-GCM authenticated encryption and decryption utility.
|
|
5
5
|
* Uses APP_KEY from environment for secure encryption.
|
|
6
6
|
*/
|
|
7
7
|
class Encryption {
|
|
8
8
|
/**
|
|
9
|
-
* Encrypts a value using AES-256-
|
|
9
|
+
* Encrypts a value using AES-256-GCM with authentication tag.
|
|
10
10
|
* @param {string|Object} value - Value to encrypt (objects are JSON stringified)
|
|
11
|
-
* @returns {string} Encrypted string in format "iv:encryptedData"
|
|
11
|
+
* @returns {string} Encrypted string in format "iv:authTag:encryptedData"
|
|
12
12
|
*/
|
|
13
13
|
static encrypt(value) {
|
|
14
14
|
if (typeof value === "object") {
|
|
@@ -16,27 +16,35 @@ class Encryption {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
|
|
19
|
-
const iv = crypto.randomBytes(
|
|
20
|
-
const cipher = crypto.createCipheriv("aes-256-
|
|
19
|
+
const iv = crypto.randomBytes(12);
|
|
20
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", secretKey, iv);
|
|
21
21
|
|
|
22
22
|
let encrypted = cipher.update(value, "utf8", "hex");
|
|
23
23
|
encrypted += cipher.final("hex");
|
|
24
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
24
25
|
|
|
25
|
-
return iv.toString("hex") + ":" + encrypted;
|
|
26
|
+
return iv.toString("hex") + ":" + authTag + ":" + encrypted;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Decrypts an AES-256-
|
|
30
|
-
* @param {string} encryptedValue - Encrypted string in format "iv:encryptedData"
|
|
30
|
+
* Decrypts an AES-256-GCM encrypted value.
|
|
31
|
+
* @param {string} encryptedValue - Encrypted string in format "iv:authTag:encryptedData"
|
|
31
32
|
* @returns {string|false} Decrypted string or false if decryption fails
|
|
32
33
|
*/
|
|
33
34
|
static decrypt(encryptedValue) {
|
|
34
35
|
try {
|
|
35
36
|
const parts = encryptedValue.split(":");
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
if (parts.length < 3) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
43
|
+
const authTag = Buffer.from(parts[1], "hex");
|
|
44
|
+
const encryptedText = Buffer.from(parts.slice(2).join(":"), "hex");
|
|
38
45
|
const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
|
|
39
|
-
const decipher = crypto.createDecipheriv("aes-256-
|
|
46
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", secretKey, iv);
|
|
47
|
+
decipher.setAuthTag(authTag);
|
|
40
48
|
|
|
41
49
|
let decrypted = decipher.update(encryptedText, "hex", "utf8");
|
|
42
50
|
decrypted += decipher.final("utf8");
|
|
@@ -62,7 +62,9 @@ class Storage {
|
|
|
62
62
|
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
63
63
|
const fullPath = this.#validatePath(base, filePath);
|
|
64
64
|
|
|
65
|
-
await fs.promises.unlink(fullPath)
|
|
65
|
+
await fs.promises.unlink(fullPath).catch(err => {
|
|
66
|
+
if (err.code !== "ENOENT") throw err;
|
|
67
|
+
});
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
package/lib/Http/Server.js
CHANGED
|
@@ -42,6 +42,11 @@ class Server {
|
|
|
42
42
|
dotenv.config({ quiet: true });
|
|
43
43
|
await Config.initialize();
|
|
44
44
|
|
|
45
|
+
if (!process.env.APP_KEY || process.env.APP_KEY.trim() === "") {
|
|
46
|
+
console.error("\x1b[31m✕ APP_KEY is not set. Please set APP_KEY in your .env file before starting the server.\x1b[0m");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
this.#config = Config.all("server");
|
|
46
51
|
const { web_server = {}, cors = {} } = this.#config;
|
|
47
52
|
|
|
@@ -335,7 +340,7 @@ class Server {
|
|
|
335
340
|
|
|
336
341
|
// Private Methods
|
|
337
342
|
static #printBanner({ success, address, host, port, error }) {
|
|
338
|
-
if (this.#config.log.channel
|
|
343
|
+
if (this.#config.log.channel === "console") {
|
|
339
344
|
return;
|
|
340
345
|
}
|
|
341
346
|
|
package/lib/Logging/Log.js
CHANGED
|
@@ -9,6 +9,7 @@ import Config from "../Core/Config.js";
|
|
|
9
9
|
class Log {
|
|
10
10
|
static #levels = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
|
|
11
11
|
static #levelLabels = { debug: "DEBUG", info: "INFO", warn: "WARN", error: "ERROR", fatal: "FATAL" };
|
|
12
|
+
static #dirEnsured = false;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Logs a debug message.
|
|
@@ -158,10 +159,15 @@ class Log {
|
|
|
158
159
|
/** @private */
|
|
159
160
|
static #logToFile(level, message, context, timestamp, config) {
|
|
160
161
|
const filePath = path.resolve(process.cwd(), config.file);
|
|
161
|
-
const dir = path.dirname(filePath);
|
|
162
162
|
|
|
163
|
-
if (!
|
|
164
|
-
|
|
163
|
+
if (!this.#dirEnsured) {
|
|
164
|
+
const dir = path.dirname(filePath);
|
|
165
|
+
|
|
166
|
+
if (!fs.existsSync(dir)) {
|
|
167
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.#dirEnsured = true;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
const time = this.#formatTimestamp(timestamp);
|
package/lib/Route/Router.js
CHANGED
|
@@ -54,11 +54,25 @@ class HotReloadRegistry {
|
|
|
54
54
|
if (!info) return handler;
|
|
55
55
|
|
|
56
56
|
const { filePath, methodName } = info;
|
|
57
|
+
let cachedModule = null;
|
|
58
|
+
let cachedMtime = 0;
|
|
57
59
|
|
|
58
60
|
return async (request, response, param) => {
|
|
59
|
-
|
|
61
|
+
let mtime = 0;
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
try {
|
|
64
|
+
mtime = fs.statSync(filePath).mtimeMs;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
mtime = Date.now();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!cachedModule || mtime !== cachedMtime) {
|
|
71
|
+
cachedModule = await import(`file://${filePath}?t=${mtime}`);
|
|
72
|
+
cachedMtime = mtime;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return cachedModule.default[methodName](request, response, param);
|
|
62
76
|
};
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -391,25 +405,30 @@ class RouteGroup {
|
|
|
391
405
|
|
|
392
406
|
group(callback) {
|
|
393
407
|
const beforeCount = this.routes.length;
|
|
394
|
-
callback();
|
|
395
|
-
const newRoutes = this.routes.slice(beforeCount);
|
|
396
|
-
|
|
397
|
-
for (const route of newRoutes) {
|
|
398
|
-
// Apply prefix
|
|
399
|
-
if (this.options.prefix) {
|
|
400
|
-
route.url = route.url === "/"
|
|
401
|
-
? this.options.prefix
|
|
402
|
-
: this.options.prefix + route.url;
|
|
403
|
-
}
|
|
404
408
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
409
|
+
const applyGroup = () => {
|
|
410
|
+
const newRoutes = this.routes.slice(beforeCount);
|
|
411
|
+
for (const route of newRoutes) {
|
|
412
|
+
if (this.options.prefix) {
|
|
413
|
+
route.url = route.url === "/"
|
|
414
|
+
? this.options.prefix
|
|
415
|
+
: this.options.prefix + route.url;
|
|
416
|
+
}
|
|
417
|
+
if (route.name && this.options.name) {
|
|
418
|
+
route.name = this.options.name + route.name;
|
|
419
|
+
}
|
|
420
|
+
route.middlewares.unshift(...this.options.middlewares);
|
|
408
421
|
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const result = callback();
|
|
409
425
|
|
|
410
|
-
|
|
411
|
-
|
|
426
|
+
// Sync callback: apply immediately. Async callback: apply after resolution.
|
|
427
|
+
if (result && typeof result.then === "function") {
|
|
428
|
+
return result.then(() => applyGroup());
|
|
412
429
|
}
|
|
430
|
+
|
|
431
|
+
applyGroup();
|
|
413
432
|
}
|
|
414
433
|
}
|
|
415
434
|
|
package/lib/Session/Manager.js
CHANGED
|
@@ -19,13 +19,24 @@ class SessionManager {
|
|
|
19
19
|
* Gets the singleton instance.
|
|
20
20
|
* @returns {Promise<SessionManager>}
|
|
21
21
|
*/
|
|
22
|
+
static #initPromise = null;
|
|
23
|
+
|
|
22
24
|
static async getInstance() {
|
|
23
|
-
if (
|
|
24
|
-
this.#instance
|
|
25
|
-
|
|
25
|
+
if (this.#instance) {
|
|
26
|
+
return this.#instance;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!this.#initPromise) {
|
|
30
|
+
this.#initPromise = (async () => {
|
|
31
|
+
const manager = new SessionManager();
|
|
32
|
+
await manager.#initialize();
|
|
33
|
+
this.#instance = manager;
|
|
34
|
+
|
|
35
|
+
return manager;
|
|
36
|
+
})();
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
return this.#
|
|
39
|
+
return this.#initPromise;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/** @private */
|
package/lib/View/View.js
CHANGED
|
@@ -175,23 +175,24 @@ class View {
|
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const url = req.query.url;
|
|
181
|
-
if (!url) {
|
|
182
|
-
return res.code(400).send({ error: "Missing url" });
|
|
183
|
-
}
|
|
178
|
+
server.get("/__nitron/navigate", async (req, res) => {
|
|
179
|
+
const url = req.query.url;
|
|
184
180
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return res
|
|
190
|
-
.code(result.status || 200)
|
|
191
|
-
.header("X-Content-Type-Options", "nosniff")
|
|
192
|
-
.send(result);
|
|
193
|
-
});
|
|
181
|
+
if (!url) {
|
|
182
|
+
return res.code(400).send({ error: "Missing url" });
|
|
183
|
+
}
|
|
194
184
|
|
|
185
|
+
const result = await View.#renderPartial(url, req, res);
|
|
186
|
+
|
|
187
|
+
if (result.handled) return;
|
|
188
|
+
|
|
189
|
+
return res
|
|
190
|
+
.code(result.status || 200)
|
|
191
|
+
.header("X-Content-Type-Options", "nosniff")
|
|
192
|
+
.send(result);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (View.#isDev) {
|
|
195
196
|
server.get("/__nitron/icon.png", async (req, res) => {
|
|
196
197
|
const iconPath = path.join(__dirname, "Client", "nitronjs-icon.png");
|
|
197
198
|
const iconBuffer = readFileSync(iconPath);
|
package/lib/index.d.ts
CHANGED
|
@@ -56,40 +56,84 @@ export class Model {
|
|
|
56
56
|
static hidden: string[];
|
|
57
57
|
static casts: Record<string, string>;
|
|
58
58
|
|
|
59
|
+
// Direct static methods
|
|
59
60
|
static get<T extends Model>(this: new () => T): Promise<T[]>;
|
|
60
61
|
static find<T extends Model>(this: new () => T, id: number | string): Promise<T | null>;
|
|
61
62
|
static first<T extends Model>(this: new () => T): Promise<T | null>;
|
|
62
|
-
static where<T extends Model>(this: new () => T, column: string,
|
|
63
|
+
static where<T extends Model>(this: new () => T, column: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
64
|
+
static where<T extends Model>(this: new () => T, column: string, operator: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
65
|
+
static where<T extends Model>(this: new () => T, column: Record<string, any>): QueryBuilder<T>;
|
|
63
66
|
static select<T extends Model>(this: new () => T, ...columns: string[]): QueryBuilder<T>;
|
|
64
67
|
static orderBy<T extends Model>(this: new () => T, column: string, direction?: 'asc' | 'desc'): QueryBuilder<T>;
|
|
65
68
|
static limit<T extends Model>(this: new () => T, count: number): QueryBuilder<T>;
|
|
66
|
-
static create<T extends Model>(this: new () => T, data: Record<string, any>): Promise<T>;
|
|
67
69
|
|
|
70
|
+
// Forwarded from QueryBuilder (QUERY_METHODS)
|
|
71
|
+
static selectRaw<T extends Model>(this: new () => T, expression: string): QueryBuilder<T>;
|
|
72
|
+
static distinct<T extends Model>(this: new () => T): QueryBuilder<T>;
|
|
73
|
+
static orWhere<T extends Model>(this: new () => T, column: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
74
|
+
static orWhere<T extends Model>(this: new () => T, column: string, operator: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
75
|
+
static whereIn<T extends Model>(this: new () => T, column: string, values: any[]): QueryBuilder<T>;
|
|
76
|
+
static whereNotIn<T extends Model>(this: new () => T, column: string, values: any[]): QueryBuilder<T>;
|
|
77
|
+
static whereBetween<T extends Model>(this: new () => T, column: string, range: [any, any]): QueryBuilder<T>;
|
|
78
|
+
static whereNot<T extends Model>(this: new () => T, column: string, value: any): QueryBuilder<T>;
|
|
79
|
+
static join<T extends Model>(this: new () => T, table: string, col1: string, operator: string, col2: string): QueryBuilder<T>;
|
|
80
|
+
static groupBy<T extends Model>(this: new () => T, ...columns: string[]): QueryBuilder<T>;
|
|
81
|
+
static offset<T extends Model>(this: new () => T, count: number): QueryBuilder<T>;
|
|
82
|
+
static count<T extends Model>(this: new () => T, column?: string): Promise<number>;
|
|
83
|
+
static countDistinct<T extends Model>(this: new () => T, column: string): Promise<number>;
|
|
84
|
+
static max<T extends Model>(this: new () => T, column: string): Promise<number | null>;
|
|
85
|
+
static min<T extends Model>(this: new () => T, column: string): Promise<number | null>;
|
|
86
|
+
static sum<T extends Model>(this: new () => T, column: string): Promise<number>;
|
|
87
|
+
static avg<T extends Model>(this: new () => T, column: string): Promise<number | null>;
|
|
88
|
+
|
|
89
|
+
// Instance methods
|
|
68
90
|
save(): Promise<this>;
|
|
69
91
|
delete(): Promise<boolean>;
|
|
70
|
-
|
|
71
|
-
refresh(): Promise<this>;
|
|
72
|
-
toJSON(): Record<string, any>;
|
|
92
|
+
toObject(): Record<string, any>;
|
|
73
93
|
|
|
74
94
|
[key: string]: any;
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
export interface QueryBuilder<T> {
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
// Where clauses
|
|
99
|
+
where(column: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
100
|
+
where(column: string, operator: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
101
|
+
where(column: Record<string, any>): QueryBuilder<T>;
|
|
102
|
+
orWhere(column: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
103
|
+
orWhere(column: string, operator: string, value: string | number | boolean | null): QueryBuilder<T>;
|
|
80
104
|
whereIn(column: string, values: any[]): QueryBuilder<T>;
|
|
81
105
|
whereNotIn(column: string, values: any[]): QueryBuilder<T>;
|
|
82
|
-
|
|
83
|
-
whereNotNull(column: string): QueryBuilder<T>;
|
|
106
|
+
whereNot(column: string, value: any): QueryBuilder<T>;
|
|
84
107
|
whereBetween(column: string, range: [any, any]): QueryBuilder<T>;
|
|
108
|
+
|
|
109
|
+
// Selection
|
|
85
110
|
select(...columns: string[]): QueryBuilder<T>;
|
|
111
|
+
selectRaw(expression: string): QueryBuilder<T>;
|
|
112
|
+
distinct(): QueryBuilder<T>;
|
|
113
|
+
|
|
114
|
+
// Joins
|
|
115
|
+
join(table: string, col1: string, operator: string, col2: string): QueryBuilder<T>;
|
|
116
|
+
|
|
117
|
+
// Ordering & Grouping
|
|
86
118
|
orderBy(column: string, direction?: 'asc' | 'desc'): QueryBuilder<T>;
|
|
119
|
+
groupBy(...columns: string[]): QueryBuilder<T>;
|
|
87
120
|
limit(count: number): QueryBuilder<T>;
|
|
88
121
|
offset(count: number): QueryBuilder<T>;
|
|
122
|
+
|
|
123
|
+
// Terminal — fetch
|
|
89
124
|
get(): Promise<T[]>;
|
|
90
125
|
first(): Promise<T | null>;
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
|
|
127
|
+
// Terminal — aggregates
|
|
128
|
+
count(column?: string): Promise<number>;
|
|
129
|
+
countDistinct(column: string): Promise<number>;
|
|
130
|
+
max(column: string): Promise<number | null>;
|
|
131
|
+
min(column: string): Promise<number | null>;
|
|
132
|
+
sum(column: string): Promise<number>;
|
|
133
|
+
avg(column: string): Promise<number | null>;
|
|
134
|
+
|
|
135
|
+
// Terminal — mutation
|
|
136
|
+
insert(data: Record<string, any>): Promise<number>;
|
|
93
137
|
update(data: Record<string, any>): Promise<number>;
|
|
94
138
|
delete(): Promise<number>;
|
|
95
139
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"njs": "./cli/njs.js"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Config } from "@nitronjs/framework";
|
|
2
2
|
|
|
3
3
|
class VerifyCsrf {
|
|
4
4
|
static async handler(req, res) {
|
|
5
|
-
const csrf =
|
|
5
|
+
const csrf = Config.all("session").csrf;
|
|
6
6
|
const token = req.body?.[csrf.tokenField] || req.headers[csrf.headerField];
|
|
7
7
|
|
|
8
8
|
if (!token) {
|