@fuzionx/framework 0.1.21 β 0.1.23
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/templates/app/app.js.tpl +1 -9
- package/cli/templates/app/controllers/HomeController.js +1 -1
- package/cli/templates/app/fuzionx.yaml.tpl +32 -4
- package/cli/templates/app/routes/web.js.tpl +3 -3
- package/cli/templates/make/middleware.js.tpl +1 -1
- package/lib/core/Application.js +41 -40
- package/lib/core/AutoLoader.js +6 -1
- package/lib/core/Config.js +2 -1
- package/lib/core/Context.js +8 -3
- package/lib/database/ConnectionManager.js +17 -1
- package/lib/database/SqlModel.js +10 -5
- package/lib/database/SqlQueryBuilder.js +29 -17
- package/lib/http/Middleware.js +1 -1
- package/lib/middleware/index.js +1 -1
- package/package.json +2 -2
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { Application } from '@fuzionx/framework';
|
|
2
|
-
import webRoutes from './routes/web.js';
|
|
3
|
-
import apiRoutes from './routes/api.js';
|
|
4
2
|
|
|
5
3
|
const app = new Application({ configPath: './fuzionx.yaml' });
|
|
6
4
|
|
|
7
|
-
app.routes(webRoutes);
|
|
8
|
-
app.routes(apiRoutes);
|
|
9
|
-
|
|
10
5
|
await app.boot();
|
|
11
|
-
|
|
12
|
-
app.listen(49080, () => {
|
|
13
|
-
console.log('π FuzionX running on http://localhost:49080');
|
|
14
|
-
});
|
|
6
|
+
await app.listen();
|
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
# FuzionX Configuration
|
|
2
2
|
bridge:
|
|
3
3
|
port: 49080
|
|
4
|
-
workers:
|
|
4
|
+
workers: 0
|
|
5
5
|
worker_timeout: 30
|
|
6
6
|
|
|
7
|
+
cors:
|
|
8
|
+
enabled: false
|
|
9
|
+
origins:
|
|
10
|
+
- "*"
|
|
11
|
+
|
|
7
12
|
rate_limit:
|
|
8
13
|
enabled: true
|
|
9
14
|
per_ip: 1000
|
|
10
15
|
|
|
16
|
+
session:
|
|
17
|
+
enabled: false
|
|
18
|
+
store: memory
|
|
19
|
+
ttl: 3600
|
|
20
|
+
cookie_name: fuzionx.sid
|
|
21
|
+
|
|
22
|
+
websocket:
|
|
23
|
+
enabled: false
|
|
24
|
+
path: /ws
|
|
25
|
+
check_interval: 60
|
|
26
|
+
timeout: 60
|
|
27
|
+
|
|
28
|
+
static:
|
|
29
|
+
- url: /public
|
|
30
|
+
path: ./public
|
|
31
|
+
|
|
32
|
+
logging:
|
|
33
|
+
level: info
|
|
34
|
+
intercept_console: true
|
|
35
|
+
|
|
11
36
|
database:
|
|
12
37
|
default: main
|
|
13
38
|
connections:
|
|
@@ -18,15 +43,18 @@ database:
|
|
|
18
43
|
app:
|
|
19
44
|
name: '{{name}}'
|
|
20
45
|
environment: development
|
|
46
|
+
|
|
21
47
|
auth:
|
|
22
|
-
secret: 'change-me-in-production'
|
|
48
|
+
secret: '${JWT_SECRET:change-me-in-production}'
|
|
23
49
|
accessTtl: '15m'
|
|
50
|
+
|
|
24
51
|
i18n:
|
|
25
52
|
default_locale: 'ko'
|
|
26
53
|
fallback: 'en'
|
|
54
|
+
|
|
27
55
|
docs:
|
|
28
56
|
enabled: true
|
|
29
57
|
path: '/docs'
|
|
30
58
|
|
|
31
|
-
themes:
|
|
32
|
-
|
|
59
|
+
themes:
|
|
60
|
+
default: 'default'
|
package/lib/core/Application.js
CHANGED
|
@@ -245,6 +245,18 @@ export default class Application {
|
|
|
245
245
|
async boot() {
|
|
246
246
|
if (this._booted) return;
|
|
247
247
|
|
|
248
|
+
// 0. Bridge μ΄κΈ°ν β boot λ‘μ§ μ μ bridge ν보
|
|
249
|
+
// Helper(crypto/hash/media/file/i18n/logger/view)κ° boot μ€ bridge νμ
|
|
250
|
+
if (FuzionXApp && !this._coreApp) {
|
|
251
|
+
this._coreApp = new FuzionXApp({
|
|
252
|
+
config: this.configPath
|
|
253
|
+
? path.resolve(this.baseDir, this.configPath)
|
|
254
|
+
: undefined,
|
|
255
|
+
});
|
|
256
|
+
this._bridge = this._coreApp._bridge;
|
|
257
|
+
this._propagateBridge();
|
|
258
|
+
}
|
|
259
|
+
|
|
248
260
|
// 1. .env λ‘λ (17-config.md)
|
|
249
261
|
this.config.loadEnv(this.baseDir);
|
|
250
262
|
// .env λ‘λ ν μΊμ ν΄λ¦¬μ΄ (μλ‘μ΄ νκ²½λ³μ λ°μ)
|
|
@@ -307,6 +319,21 @@ export default class Application {
|
|
|
307
319
|
await this.emit('booted');
|
|
308
320
|
}
|
|
309
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Bridge μ°Έμ‘°λ₯Ό λͺ¨λ Helper μΈμ€ν΄μ€μ μ ν
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_propagateBridge() {
|
|
327
|
+
if (!this._bridge) return;
|
|
328
|
+
if (this._view) this._view._bridge = this._bridge;
|
|
329
|
+
if (this.logger) this.logger._bridge = this._bridge;
|
|
330
|
+
if (this.crypto) this.crypto._bridge = this._bridge;
|
|
331
|
+
if (this.hash) this.hash._bridge = this._bridge;
|
|
332
|
+
if (this.media) this.media._bridge = this._bridge;
|
|
333
|
+
if (this.file) this.file._bridge = this._bridge;
|
|
334
|
+
if (this.i18n) this.i18n._bridge = this._bridge;
|
|
335
|
+
}
|
|
336
|
+
|
|
310
337
|
// βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
311
338
|
// μλ² μμ β Bridge μ°κ²°
|
|
312
339
|
// βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -331,29 +358,14 @@ export default class Application {
|
|
|
331
358
|
|
|
332
359
|
await this.emit('ready');
|
|
333
360
|
|
|
334
|
-
// Bridge μ°λ
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
// FuzionXApp μμ± ν Bridge μ ν β boot() μμ μλ nullμ΄μμ
|
|
344
|
-
this._bridge = this._coreApp._bridge;
|
|
345
|
-
if (this._bridge) {
|
|
346
|
-
if (this._view) this._view._bridge = this._bridge;
|
|
347
|
-
if (this.logger) this.logger._bridge = this._bridge;
|
|
348
|
-
if (this.crypto) this.crypto._bridge = this._bridge;
|
|
349
|
-
if (this.hash) this.hash._bridge = this._bridge;
|
|
350
|
-
if (this.media) this.media._bridge = this._bridge;
|
|
351
|
-
if (this.file) this.file._bridge = this._bridge;
|
|
352
|
-
if (this.i18n) this.i18n._bridge = this._bridge;
|
|
353
|
-
}
|
|
354
|
-
// WS proxy μ°κ²° (WsHandlerμμ this.app.ws μ¬μ©)
|
|
355
|
-
this.ws = this._coreApp.ws;
|
|
356
|
-
}
|
|
361
|
+
// Bridge μ°λ β _coreAppμ boot()μμ μ΄λ―Έ μμ±λ¨
|
|
362
|
+
if (this._coreApp) {
|
|
363
|
+
// boot()μμ port μμ΄ μμ±λμΌλ―λ‘ port μ€μ
|
|
364
|
+
this._coreApp._port = port;
|
|
365
|
+
|
|
366
|
+
// WS proxy μ°κ²° (WsHandlerμμ this.app.ws μ¬μ©)
|
|
367
|
+
if (!this.ws) this.ws = this._coreApp.ws;
|
|
368
|
+
|
|
357
369
|
this._registerBridgeRoutes(this._coreApp);
|
|
358
370
|
|
|
359
371
|
// WsHandler β Bridge WS μ΄λ²€νΈ μ°κ²°
|
|
@@ -527,14 +539,15 @@ export default class Application {
|
|
|
527
539
|
const eventMap = HandlerClass.buildEventMap();
|
|
528
540
|
const wsNs = coreApp.ws(namespace);
|
|
529
541
|
|
|
542
|
+
// μ±κΈν€ μΈμ€ν΄μ€ β μ°κ²°/λ©μμ§/ν΄μ μμ λμΌ μΈμ€ν΄μ€ μ¬μ©
|
|
543
|
+
const inst = new HandlerClass(this);
|
|
544
|
+
|
|
530
545
|
wsNs.on('connect', (socket) => {
|
|
531
|
-
const inst = new HandlerClass(this);
|
|
532
546
|
console.log(`[WS] connect: ${namespace} sid=${socket.sessionId}`);
|
|
533
547
|
inst.onConnect(socket);
|
|
534
548
|
});
|
|
535
549
|
|
|
536
550
|
wsNs.on('message', (socket, rawMessage) => {
|
|
537
|
-
const inst = new HandlerClass(this);
|
|
538
551
|
let parsed;
|
|
539
552
|
try { parsed = typeof rawMessage === 'string' ? JSON.parse(rawMessage) : rawMessage; }
|
|
540
553
|
catch { parsed = { type: 'message', data: rawMessage }; }
|
|
@@ -556,7 +569,6 @@ export default class Application {
|
|
|
556
569
|
});
|
|
557
570
|
|
|
558
571
|
wsNs.on('disconnect', (socket) => {
|
|
559
|
-
const inst = new HandlerClass(this);
|
|
560
572
|
console.log(`[WS] disconnect: ${namespace} sid=${socket.sessionId}`);
|
|
561
573
|
inst.onDisconnect(socket);
|
|
562
574
|
});
|
|
@@ -605,7 +617,7 @@ export default class Application {
|
|
|
605
617
|
}, this);
|
|
606
618
|
|
|
607
619
|
// Framework β async μ²΄μΈ μ€ν ν μ§μ sendAsyncResponse
|
|
608
|
-
const promise = this._executeChain(middlewareFns, route, ctx
|
|
620
|
+
const promise = this._executeChain(middlewareFns, route, ctx);
|
|
609
621
|
|
|
610
622
|
promise.then(() => {
|
|
611
623
|
const response = ctx.toResponse();
|
|
@@ -646,7 +658,7 @@ export default class Application {
|
|
|
646
658
|
* λ―Έλ€μ¨μ΄ μ²΄μΈ + νΈλ€λ¬ μ€ν
|
|
647
659
|
* @private
|
|
648
660
|
*/
|
|
649
|
-
async _executeChain(middlewareFns, route, ctx
|
|
661
|
+
async _executeChain(middlewareFns, route, ctx) {
|
|
650
662
|
let idx = 0;
|
|
651
663
|
|
|
652
664
|
const next = async () => {
|
|
@@ -677,18 +689,7 @@ export default class Application {
|
|
|
677
689
|
}
|
|
678
690
|
if (!handled) this._errorHandler.handle(err, ctx);
|
|
679
691
|
}
|
|
680
|
-
|
|
681
|
-
// Context β Bridge res λ³ν (μ΄μ€ μλ΅ λ°©μ§ β Core resλ _sent μ¬μ©)
|
|
682
|
-
if (!res._sent) {
|
|
683
|
-
const response = ctx.toResponse();
|
|
684
|
-
res.status(response.status);
|
|
685
|
-
if (response.headers) {
|
|
686
|
-
for (const [k, v] of Object.entries(response.headers)) {
|
|
687
|
-
res.header(k, v);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
res.send(response.body);
|
|
691
|
-
}
|
|
692
|
+
// μλ΅μ _createBridgeHandlerμ .then()μμ sendAsyncResponseλ‘ μ μ‘
|
|
692
693
|
}
|
|
693
694
|
|
|
694
695
|
/**
|
package/lib/core/AutoLoader.js
CHANGED
|
@@ -106,8 +106,13 @@ export default class AutoLoader {
|
|
|
106
106
|
*/
|
|
107
107
|
static registerController(ControllerClass) {
|
|
108
108
|
const proto = ControllerClass.prototype;
|
|
109
|
+
// Base νλ‘ν νμ
λ©μλ μ€ν΅ (Controller.registerμ λμΌ λ‘μ§)
|
|
110
|
+
const baseNames = new Set(Object.getOwnPropertyNames(
|
|
111
|
+
Object.getPrototypeOf(proto) // Base.prototype (Controller extends Base)
|
|
112
|
+
));
|
|
109
113
|
for (const method of Object.getOwnPropertyNames(proto)) {
|
|
110
114
|
if (method === 'constructor') continue;
|
|
115
|
+
if (baseNames.has(method)) continue;
|
|
111
116
|
if (typeof proto[method] !== 'function') continue;
|
|
112
117
|
// static propertyλ‘ __handler__ λμ€ν¬λ¦½ν° λ±λ‘
|
|
113
118
|
ControllerClass[method] = {
|
|
@@ -140,7 +145,7 @@ export default class AutoLoader {
|
|
|
140
145
|
const mod = await import(file);
|
|
141
146
|
const MwClass = mod.default;
|
|
142
147
|
if (!MwClass) continue;
|
|
143
|
-
const mwName = MwClass.
|
|
148
|
+
const mwName = MwClass.alias || extractName(file, 'Middleware').toLowerCase();
|
|
144
149
|
this.app._middlewareRegistry.set(mwName, MwClass);
|
|
145
150
|
}
|
|
146
151
|
}
|
package/lib/core/Config.js
CHANGED
|
@@ -38,7 +38,8 @@ export default class Config {
|
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
const content = readFileSync(configPath, 'utf-8');
|
|
41
|
-
const
|
|
41
|
+
const raw = Config.parseYaml(content);
|
|
42
|
+
const parsed = Config.resolveEnvVars(raw);
|
|
42
43
|
|
|
43
44
|
// _rawμ λ³ν© (κΈ°μ‘΄ opts.config μ°μ )
|
|
44
45
|
for (const [key, value] of Object.entries(parsed)) {
|
package/lib/core/Context.js
CHANGED
|
@@ -175,9 +175,14 @@ export default class Context {
|
|
|
175
175
|
return this;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
send(
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
send(data) {
|
|
179
|
+
if (typeof data === 'object' && data !== null) {
|
|
180
|
+
return this.json(data);
|
|
181
|
+
}
|
|
182
|
+
this._body = String(data);
|
|
183
|
+
if (!this._headers['Content-Type']) {
|
|
184
|
+
this._headers['Content-Type'] = 'text/html; charset=utf-8';
|
|
185
|
+
}
|
|
181
186
|
this._sent = true;
|
|
182
187
|
return this;
|
|
183
188
|
}
|
|
@@ -139,7 +139,23 @@ export default class ConnectionManager {
|
|
|
139
139
|
case 'mongo': {
|
|
140
140
|
const mongoose = tryRequire('mongoose');
|
|
141
141
|
if (!mongoose) return this._createStub('mongo', config);
|
|
142
|
-
|
|
142
|
+
// Lazy connect β promise μΊμ±μΌλ‘ μ΄μ€ μ°κ²° λ°©μ§
|
|
143
|
+
const conn = { type: 'mongo', mongoose, config, _connected: false, _connectPromise: null };
|
|
144
|
+
conn.connect = () => {
|
|
145
|
+
if (conn._connected) return Promise.resolve();
|
|
146
|
+
if (conn._connectPromise) return conn._connectPromise;
|
|
147
|
+
const uri = config.uri || config.url
|
|
148
|
+
|| `mongodb://${config.host || '127.0.0.1'}:${config.port || 27017}/${config.database || ''}`;
|
|
149
|
+
conn._connectPromise = mongoose.connect(uri, config.options || {}).then(() => {
|
|
150
|
+
conn._connected = true;
|
|
151
|
+
});
|
|
152
|
+
return conn._connectPromise;
|
|
153
|
+
};
|
|
154
|
+
// μ¦μ μ°κ²° μμ (await μμ΄ β λ°±κ·ΈλΌμ΄λ)
|
|
155
|
+
conn.connect().catch(err => {
|
|
156
|
+
console.error(`[ConnectionManager] MongoDB μ°κ²° μ€ν¨: ${err.message}`);
|
|
157
|
+
});
|
|
158
|
+
return conn;
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
default:
|
package/lib/database/SqlModel.js
CHANGED
|
@@ -94,9 +94,10 @@ export default class SqlModel extends Model {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (conn.type === 'sqlite' && conn.db) {
|
|
97
|
-
const columns = Object.keys(insertData);
|
|
97
|
+
const columns = Object.keys(insertData).map(c => SqlQueryBuilder._sanitizeName(c));
|
|
98
98
|
const placeholders = columns.map(() => '?').join(', ');
|
|
99
|
-
const
|
|
99
|
+
const safeTable = SqlQueryBuilder._sanitizeName(this.table);
|
|
100
|
+
const sql = `INSERT INTO ${safeTable} (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
100
101
|
const result = conn.db.prepare(sql).run(...Object.values(insertData));
|
|
101
102
|
const id = result.lastInsertRowid;
|
|
102
103
|
return new this({ ...insertData, [this.primaryKey]: Number(id) });
|
|
@@ -207,9 +208,11 @@ export default class SqlModel extends Model {
|
|
|
207
208
|
const conn = this.constructor.getConnection();
|
|
208
209
|
|
|
209
210
|
if (conn.type === 'sqlite' && conn.db) {
|
|
210
|
-
const columns = Object.keys(data);
|
|
211
|
+
const columns = Object.keys(data).map(c => SqlQueryBuilder._sanitizeName(c));
|
|
212
|
+
const pk = SqlQueryBuilder._sanitizeName(this.constructor.primaryKey);
|
|
213
|
+
const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
|
|
211
214
|
const sets = columns.map(c => `${c} = ?`).join(', ');
|
|
212
|
-
const sql = `UPDATE ${
|
|
215
|
+
const sql = `UPDATE ${safeTable} SET ${sets} WHERE ${pk} = ?`;
|
|
213
216
|
conn.db.prepare(sql).run(...Object.values(data), id);
|
|
214
217
|
} else if (conn.type === 'knex' && conn.db) {
|
|
215
218
|
await conn.db(this.constructor.table).where(pk, id).update(data);
|
|
@@ -244,7 +247,9 @@ export default class SqlModel extends Model {
|
|
|
244
247
|
const conn = this.constructor.getConnection();
|
|
245
248
|
|
|
246
249
|
if (conn.type === 'sqlite' && conn.db) {
|
|
247
|
-
|
|
250
|
+
const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
|
|
251
|
+
const safePk = SqlQueryBuilder._sanitizeName(pk);
|
|
252
|
+
conn.db.prepare(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`).run(id);
|
|
248
253
|
} else if (conn.type === 'knex' && conn.db) {
|
|
249
254
|
await conn.db(this.constructor.table).where(pk, id).delete();
|
|
250
255
|
}
|
|
@@ -7,12 +7,26 @@
|
|
|
7
7
|
* @see docs/framework/02-database-orm.md
|
|
8
8
|
*/
|
|
9
9
|
import QueryBuilder from './QueryBuilder.js';
|
|
10
|
+
import Pagination from './Pagination.js';
|
|
10
11
|
|
|
11
12
|
export default class SqlQueryBuilder extends QueryBuilder {
|
|
12
13
|
constructor(model) {
|
|
13
14
|
super(model);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* SQL μλ³μ(컬λΌλͺ
/ν
μ΄λΈλͺ
) μ ν¨μ± κ²μ¦ β SQL Injection λ°©μ§
|
|
19
|
+
* @param {string} name
|
|
20
|
+
* @returns {string}
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
static _sanitizeName(name) {
|
|
24
|
+
if (typeof name !== 'string' || !/^[\w.]+$/.test(name)) {
|
|
25
|
+
throw new Error(`Invalid SQL identifier: '${name}'`);
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
/**
|
|
17
31
|
* κ²°κ³Ό μ‘°ν β SQL SELECT μ€ν
|
|
18
32
|
* @returns {Promise<Array<import('./Model.js').default>>}
|
|
@@ -117,7 +131,8 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
117
131
|
|
|
118
132
|
if (conn.type === 'sqlite' && conn.db) {
|
|
119
133
|
const { whereClause, whereParams } = this._buildWhereSql();
|
|
120
|
-
const
|
|
134
|
+
const safeTable = SqlQueryBuilder._sanitizeName(this._model.table);
|
|
135
|
+
const sql = `DELETE FROM ${safeTable}${whereClause}`;
|
|
121
136
|
const result = conn.db.prepare(sql).run(...whereParams);
|
|
122
137
|
return result.changes;
|
|
123
138
|
}
|
|
@@ -141,13 +156,7 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
141
156
|
this._limit = perPage;
|
|
142
157
|
this._offset = (page - 1) * perPage;
|
|
143
158
|
const data = await this.get();
|
|
144
|
-
return
|
|
145
|
-
data,
|
|
146
|
-
page,
|
|
147
|
-
perPage,
|
|
148
|
-
total,
|
|
149
|
-
lastPage: Math.ceil(total / perPage),
|
|
150
|
-
};
|
|
159
|
+
return new Pagination(data, total, page, perPage);
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
// ββββββββββ SQLite μ€ν ββββββββββ
|
|
@@ -161,13 +170,15 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
161
170
|
|
|
162
171
|
const selectColumns = mode === 'count'
|
|
163
172
|
? 'COUNT(*) as cnt'
|
|
164
|
-
: (this._selects ? this._selects.join(', ') : '*');
|
|
173
|
+
: (this._selects ? this._selects.map(c => SqlQueryBuilder._sanitizeName(c)).join(', ') : '*');
|
|
174
|
+
|
|
175
|
+
const safeTable = SqlQueryBuilder._sanitizeName(this._model.table);
|
|
165
176
|
|
|
166
|
-
let sql = `SELECT ${selectColumns} FROM ${
|
|
177
|
+
let sql = `SELECT ${selectColumns} FROM ${safeTable}${whereClause}`;
|
|
167
178
|
|
|
168
179
|
// ORDER BY
|
|
169
180
|
if (this._orders.length > 0 && mode !== 'count') {
|
|
170
|
-
const orderParts = this._orders.map(o => `${o.column} ${o.direction.toUpperCase()}`);
|
|
181
|
+
const orderParts = this._orders.map(o => `${SqlQueryBuilder._sanitizeName(o.column)} ${o.direction.toUpperCase()}`);
|
|
171
182
|
sql += ` ORDER BY ${orderParts.join(', ')}`;
|
|
172
183
|
}
|
|
173
184
|
|
|
@@ -204,20 +215,21 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
for (const w of this._wheres) {
|
|
218
|
+
const safeKey = SqlQueryBuilder._sanitizeName(w.key);
|
|
207
219
|
const prefix = parts.length > 0
|
|
208
220
|
? (w.type === 'or' ? ' OR ' : ' AND ')
|
|
209
221
|
: '';
|
|
210
222
|
|
|
211
223
|
if (w.op === 'IN') {
|
|
212
224
|
const placeholders = w.value.map(() => '?').join(', ');
|
|
213
|
-
parts.push(`${prefix}${
|
|
225
|
+
parts.push(`${prefix}${safeKey} IN (${placeholders})`);
|
|
214
226
|
params.push(...w.value);
|
|
215
227
|
} else if (w.op === 'IS NULL') {
|
|
216
|
-
parts.push(`${prefix}${
|
|
228
|
+
parts.push(`${prefix}${safeKey} IS NULL`);
|
|
217
229
|
} else if (w.op === 'IS NOT NULL') {
|
|
218
|
-
parts.push(`${prefix}${
|
|
230
|
+
parts.push(`${prefix}${safeKey} IS NOT NULL`);
|
|
219
231
|
} else {
|
|
220
|
-
parts.push(`${prefix}${
|
|
232
|
+
parts.push(`${prefix}${safeKey} ${w.op} ?`);
|
|
221
233
|
params.push(w.value);
|
|
222
234
|
}
|
|
223
235
|
}
|
|
@@ -233,8 +245,8 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
233
245
|
_buildUpdateSql(data) {
|
|
234
246
|
const { whereClause, whereParams } = this._buildWhereSql();
|
|
235
247
|
const columns = Object.keys(data);
|
|
236
|
-
const sets = columns.map(c => `${c} = ?`).join(', ');
|
|
237
|
-
const sql = `UPDATE ${this._model.table} SET ${sets}${whereClause}`;
|
|
248
|
+
const sets = columns.map(c => `${SqlQueryBuilder._sanitizeName(c)} = ?`).join(', ');
|
|
249
|
+
const sql = `UPDATE ${SqlQueryBuilder._sanitizeName(this._model.table)} SET ${sets}${whereClause}`;
|
|
238
250
|
const params = [...Object.values(data), ...whereParams];
|
|
239
251
|
return { sql, params };
|
|
240
252
|
}
|
package/lib/http/Middleware.js
CHANGED
|
@@ -8,7 +8,7 @@ import Base from '../core/Base.js';
|
|
|
8
8
|
|
|
9
9
|
export default class Middleware extends Base {
|
|
10
10
|
/** @type {string} λ―Έλ€μ¨μ΄ λ±λ‘ μ΄λ¦ (λΌμ°νΈμμ μ°Έμ‘°) */
|
|
11
|
-
static
|
|
11
|
+
static alias = '';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* λ―Έλ€μ¨μ΄ νΈλ€λ¬ (μλΈν΄λμ€μμ μ€λ²λΌμ΄λ)
|
package/lib/middleware/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @see docs/framework/12-middleware.md
|
|
5
5
|
* @see docs/framework/14-authentication.md
|
|
6
6
|
*/
|
|
7
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* JSON/Form body νμ± λ―Έλ€μ¨μ΄
|
|
@@ -200,7 +201,6 @@ function decodeJwtPayload(token, secret) {
|
|
|
200
201
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
201
202
|
|
|
202
203
|
// ββ HMAC-SHA256 μλͺ
κ²μ¦ ββ
|
|
203
|
-
const { createHmac, timingSafeEqual } = require('node:crypto');
|
|
204
204
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
205
205
|
const expectedSig = createHmac('sha256', secret)
|
|
206
206
|
.update(signingInput)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-stack MVC framework built on @fuzionx/core β Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fuzionx/core": "^0.1.
|
|
37
|
+
"@fuzionx/core": "^0.1.23",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|