@fuzionx/framework 0.1.61 → 0.1.63
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/index.js +517 -59
- package/cli/templates/make/app-spa/views/default/spa/package.json +1 -1
- package/index.js +2 -1
- package/lib/cache/CacheManager.js +183 -0
- package/lib/cache/drivers/FileDriver.js +228 -0
- package/lib/cache/drivers/RedisDriver.js +166 -0
- package/lib/core/Application.js +26 -1
- package/lib/core/Base.js +3 -0
- package/lib/database/ConnectionManager.js +22 -3
- package/lib/database/Model.js +15 -1
- package/lib/database/SqlModel.js +14 -0
- package/lib/database/SqlQueryBuilder.js +91 -6
- package/lib/middleware/index.js +1 -0
- package/lib/middleware/roleGuard.js +49 -0
- package/lib/middleware/theme.js +56 -3
- package/lib/services/Service.js +23 -5
- package/package.json +10 -2
|
@@ -8,15 +8,22 @@
|
|
|
8
8
|
* @see docs/framework/17-config.md
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import path from 'node:path';
|
|
11
13
|
|
|
12
14
|
const _require = createRequire(import.meta.url);
|
|
15
|
+
/** CWD 기반 require — 프로젝트 node_modules에서 resolve */
|
|
16
|
+
const _cwdRequire = createRequire(pathToFileURL(path.join(process.cwd(), '_resolve.js')).href);
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* optional dependency 안전 로드
|
|
20
|
+
* 프레임워크 경로 → CWD 경로 순으로 시도
|
|
16
21
|
* @private
|
|
17
22
|
*/
|
|
18
23
|
function tryRequire(name) {
|
|
19
|
-
try { return _require(name); } catch {
|
|
24
|
+
try { return _require(name); } catch {
|
|
25
|
+
try { return _cwdRequire(name); } catch { return null; }
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export default class ConnectionManager {
|
|
@@ -144,8 +151,20 @@ export default class ConnectionManager {
|
|
|
144
151
|
conn.connect = () => {
|
|
145
152
|
if (conn._connected) return Promise.resolve();
|
|
146
153
|
if (conn._connectPromise) return conn._connectPromise;
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
// user/password가 있으면 URI에 인증 정보 포함
|
|
155
|
+
let uri = config.uri || config.url;
|
|
156
|
+
if (!uri) {
|
|
157
|
+
const host = config.host || '127.0.0.1';
|
|
158
|
+
const port = config.port || 27017;
|
|
159
|
+
const db = config.database || '';
|
|
160
|
+
if (config.user && config.password) {
|
|
161
|
+
// 인증 정보 포함 URI + authSource 지정
|
|
162
|
+
const authSource = config.authSource || config.database || 'admin';
|
|
163
|
+
uri = `mongodb://${encodeURIComponent(config.user)}:${encodeURIComponent(config.password)}@${host}:${port}/${db}?authSource=${authSource}`;
|
|
164
|
+
} else {
|
|
165
|
+
uri = `mongodb://${host}:${port}/${db}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
149
168
|
conn._connectPromise = mongoose.connect(uri, config.options || {}).then(() => {
|
|
150
169
|
conn._connected = true;
|
|
151
170
|
});
|
package/lib/database/Model.js
CHANGED
|
@@ -16,9 +16,23 @@ export default class Model {
|
|
|
16
16
|
static softDelete = false; // deleted_at 소프트 삭제
|
|
17
17
|
static hidden = []; // JSON 직렬화 시 제외 필드
|
|
18
18
|
static columns = {}; // SQL 스키마 정의 (fx db:sync 용)
|
|
19
|
-
static indexes = []; // 인덱스 정의 [{ columns: ['
|
|
19
|
+
static indexes = []; // 인덱스 정의 [{ columns: ['role_code'], unique?: boolean, name?: string }]
|
|
20
20
|
static schema = {}; // MongoDB 스키마
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 관계 정의 — 서브클래스에서 오버라이드
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* static relations = {
|
|
27
|
+
* wallet: { type: 'hasOne', model: 'Wallet', foreignKey: 'user_id' },
|
|
28
|
+
* posts: { type: 'hasMany', model: 'Post', foreignKey: 'user_id' },
|
|
29
|
+
* role: { type: 'belongsTo', model: 'Role', foreignKey: 'role_code', ownerKey: 'code' },
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* @type {Object<string, { type: string, model: string, foreignKey: string, ownerKey?: string }>}
|
|
33
|
+
*/
|
|
34
|
+
static relations = {};
|
|
35
|
+
|
|
22
36
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
23
37
|
// 인스턴스 (레코드 래퍼)
|
|
24
38
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
package/lib/database/SqlModel.js
CHANGED
|
@@ -31,6 +31,12 @@ export default class SqlModel extends Model {
|
|
|
31
31
|
*/
|
|
32
32
|
static _connectionManager = null;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* ModelRegistry 인스턴스 (Application에서 주입 — relation resolve용)
|
|
36
|
+
* @type {import('./ModelRegistry.js').default|null}
|
|
37
|
+
*/
|
|
38
|
+
static _modelRegistry = null;
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* ConnectionManager를 모든 SqlModel 서브클래스에 주입
|
|
36
42
|
* @param {import('./ConnectionManager.js').default} cm
|
|
@@ -39,6 +45,14 @@ export default class SqlModel extends Model {
|
|
|
39
45
|
SqlModel._connectionManager = cm;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/**
|
|
49
|
+
* ModelRegistry를 모든 SqlModel 서브클래스에 주입 (relation eager-loading용)
|
|
50
|
+
* @param {import('./ModelRegistry.js').default} registry
|
|
51
|
+
*/
|
|
52
|
+
static setModelRegistry(registry) {
|
|
53
|
+
SqlModel._modelRegistry = registry;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
/**
|
|
43
57
|
* DB 연결 가져오기
|
|
44
58
|
* @returns {object} connection object
|
|
@@ -33,17 +33,22 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
33
33
|
*/
|
|
34
34
|
async get() {
|
|
35
35
|
const conn = this._model.getConnection();
|
|
36
|
+
let results;
|
|
36
37
|
|
|
37
38
|
if (conn.type === 'sqlite' && conn.db) {
|
|
38
|
-
|
|
39
|
+
results = this._executeSqlite(conn.db, 'select');
|
|
40
|
+
} else if (conn.type === 'knex' && conn.db) {
|
|
41
|
+
results = await this._executeKnex(conn.db, 'select');
|
|
42
|
+
} else {
|
|
43
|
+
results = [];
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
// eager-loading — with()로 지정된 관계 로드
|
|
47
|
+
if (this._withs.length > 0 && results.length > 0) {
|
|
48
|
+
await this._loadRelations(results);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
return [];
|
|
51
|
+
return results;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
@@ -155,7 +160,7 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
155
160
|
const total = await this.count();
|
|
156
161
|
this._limit = perPage;
|
|
157
162
|
this._offset = (page - 1) * perPage;
|
|
158
|
-
const data = await this.get();
|
|
163
|
+
const data = await this.get(); // get() 내부에서 relation 자동 로드
|
|
159
164
|
return new Pagination(data, total, page, perPage);
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -329,4 +334,84 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
329
334
|
}
|
|
330
335
|
return query;
|
|
331
336
|
}
|
|
337
|
+
|
|
338
|
+
// ━━━━━━━━━━ Relation Eager-Loading ━━━━━━━━━━
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* with()로 지정된 관계를 일괄 조회하여 결과에 첨부 (N+1 방지)
|
|
342
|
+
*
|
|
343
|
+
* 지원 관계 타입:
|
|
344
|
+
* - hasOne: 1:1 (현재 PK → 대상 FK)
|
|
345
|
+
* - hasMany: 1:N (현재 PK → 대상 FK)
|
|
346
|
+
* - belongsTo: N:1 (현재 FK → 대상 PK/ownerKey)
|
|
347
|
+
*
|
|
348
|
+
* @param {Array<Model>} results - 메인 조회 결과
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
async _loadRelations(results) {
|
|
352
|
+
const modelRelations = this._model.relations || {};
|
|
353
|
+
|
|
354
|
+
for (const relName of this._withs) {
|
|
355
|
+
const rel = modelRelations[relName];
|
|
356
|
+
if (!rel) continue; // 정의되지 않은 관계는 무시
|
|
357
|
+
|
|
358
|
+
// 관계 대상 모델 클래스 resolve (문자열 → ModelRegistry에서 찾기)
|
|
359
|
+
let RelatedModel = rel.model;
|
|
360
|
+
if (typeof RelatedModel === 'string') {
|
|
361
|
+
// SqlModel._modelRegistry를 통해 모델 클래스 resolve
|
|
362
|
+
const registry = this._model._modelRegistry || this._model.constructor._modelRegistry;
|
|
363
|
+
if (registry) {
|
|
364
|
+
RelatedModel = registry.get(rel.model);
|
|
365
|
+
}
|
|
366
|
+
if (!RelatedModel || typeof RelatedModel === 'string') {
|
|
367
|
+
continue; // resolve 실패 시 스킵
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const foreignKey = rel.foreignKey;
|
|
372
|
+
const ownerKey = rel.ownerKey || this._model.primaryKey; // belongsTo의 대상 키
|
|
373
|
+
const localKey = rel.localKey || this._model.primaryKey; // hasOne/hasMany의 로컬 키
|
|
374
|
+
|
|
375
|
+
if (rel.type === 'hasOne' || rel.type === 'hasMany') {
|
|
376
|
+
// hasOne/hasMany: 현재 모델의 localKey 값 수집 → 대상 모델의 foreignKey로 조회
|
|
377
|
+
const localValues = [...new Set(results.map(r => r._attributes[localKey]).filter(v => v != null))];
|
|
378
|
+
if (localValues.length === 0) continue;
|
|
379
|
+
|
|
380
|
+
const related = await RelatedModel.query().whereIn(foreignKey, localValues).get();
|
|
381
|
+
|
|
382
|
+
// 결과 매핑 (FK → 로컬키 기준)
|
|
383
|
+
if (rel.type === 'hasOne') {
|
|
384
|
+
const map = new Map();
|
|
385
|
+
for (const r of related) map.set(r._attributes[foreignKey], r);
|
|
386
|
+
for (const row of results) {
|
|
387
|
+
row[relName] = map.get(row._attributes[localKey]) || null;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
// hasMany: FK별로 그룹화
|
|
391
|
+
const map = new Map();
|
|
392
|
+
for (const r of related) {
|
|
393
|
+
const key = r._attributes[foreignKey];
|
|
394
|
+
if (!map.has(key)) map.set(key, []);
|
|
395
|
+
map.get(key).push(r);
|
|
396
|
+
}
|
|
397
|
+
for (const row of results) {
|
|
398
|
+
row[relName] = map.get(row._attributes[localKey]) || [];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
} else if (rel.type === 'belongsTo') {
|
|
403
|
+
// belongsTo: 현재 모델의 foreignKey 값 수집 → 대상 모델의 ownerKey로 조회
|
|
404
|
+
const fkValues = [...new Set(results.map(r => r._attributes[foreignKey]).filter(v => v != null))];
|
|
405
|
+
if (fkValues.length === 0) continue;
|
|
406
|
+
|
|
407
|
+
const related = await RelatedModel.query().whereIn(ownerKey, fkValues).get();
|
|
408
|
+
const map = new Map();
|
|
409
|
+
for (const r of related) map.set(r._attributes[ownerKey], r);
|
|
410
|
+
|
|
411
|
+
for (const row of results) {
|
|
412
|
+
row[relName] = map.get(row._attributes[foreignKey]) || null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
332
417
|
}
|
package/lib/middleware/index.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roleGuard — 역할 기반 접근 제어 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* `auth()` 미들웨어 이후에 사용.
|
|
5
|
+
* ctx.user.role_code가 허용된 역할 목록에 포함되는지 검사.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 관리자 백오피스: developer, admin만 허용
|
|
9
|
+
* r.group('/api', { middleware: [auth(), roleGuard(['developer', 'admin'])] }, (r) => { ... });
|
|
10
|
+
*
|
|
11
|
+
* // 리셀러 전용: reseller만 허용
|
|
12
|
+
* r.group('/api', { middleware: [auth(), roleGuard(['developer', 'admin', 'reseller'])] }, (r) => { ... });
|
|
13
|
+
*
|
|
14
|
+
* @see docs/framework/14-authentication.md
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} allowedRoles - 접근 허용 역할 코드 배열
|
|
17
|
+
* @param {object} [opts]
|
|
18
|
+
* @param {string} [opts.roleField='role_code'] - user 객체에서 역할을 읽을 필드명
|
|
19
|
+
* @param {string} [opts.message='auth.role_denied'] - 거부 시 에러 메시지 키
|
|
20
|
+
* @returns {Function} 미들웨어 함수
|
|
21
|
+
*/
|
|
22
|
+
export function roleGuard(allowedRoles, opts = {}) {
|
|
23
|
+
const roleField = opts.roleField || 'role_code';
|
|
24
|
+
const message = opts.message || 'auth.role_denied';
|
|
25
|
+
|
|
26
|
+
return async (ctx, next) => {
|
|
27
|
+
/** 인증되지 않은 사용자 */
|
|
28
|
+
if (!ctx.user) {
|
|
29
|
+
ctx.status(401).json({ error: { message: 'Unauthorized', status: 401 } });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 역할 코드 추출 */
|
|
34
|
+
const userRole = ctx.user[roleField];
|
|
35
|
+
|
|
36
|
+
/** 허용된 역할인지 확인 */
|
|
37
|
+
if (!userRole || !allowedRoles.includes(userRole)) {
|
|
38
|
+
ctx.status(403).json({
|
|
39
|
+
error: {
|
|
40
|
+
message: ctx.t ? ctx.t(message) : message,
|
|
41
|
+
status: 403,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await next();
|
|
48
|
+
};
|
|
49
|
+
}
|
package/lib/middleware/theme.js
CHANGED
|
@@ -1,19 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* theme — 도메인 → 테마 매핑 미들웨어
|
|
2
|
+
* theme — 도메인 → 템플릿(테마) 매핑 미들웨어
|
|
3
|
+
*
|
|
4
|
+
* DB에 Domain 모델이 등록되어 있으면 요청 호스트로 조회하여
|
|
5
|
+
* ctx.theme을 해당 템플릿 코드로 설정.
|
|
6
|
+
* DB 없거나 매칭 도메인이 없으면 fuzionx.yaml의 기본 테마 사용.
|
|
7
|
+
*
|
|
8
|
+
* 성능: 캐시를 통해 매 요청마다 DB 조회를 방지.
|
|
3
9
|
*
|
|
4
10
|
* @see docs/framework/03-views-templates.md
|
|
5
11
|
* @see docs/framework/12-middleware.md — "theme: 도메인 → 테마 매핑"
|
|
6
12
|
*
|
|
7
13
|
* @param {object} [opts]
|
|
8
14
|
* @param {string} [opts.default='default'] - 기본 테마
|
|
9
|
-
* @param {object} [opts.mapping] - { 'domain.com': 'theme1' }
|
|
10
15
|
*/
|
|
11
16
|
export function theme(opts = {}) {
|
|
12
17
|
const defaultTheme = opts.default || 'default';
|
|
13
18
|
|
|
14
19
|
return async (ctx, next) => {
|
|
15
20
|
const configDefault = ctx.app?.config?.get('app.themes.default') || defaultTheme;
|
|
16
|
-
|
|
21
|
+
|
|
22
|
+
// DB의 Domain 모델이 등록되어 있으면 호스트로 조회
|
|
23
|
+
const DomainModel = ctx.app?.db?.Domain;
|
|
24
|
+
if (DomainModel) {
|
|
25
|
+
const host = ctx.headers?.host || ''; // '127.0.0.1:49080' 또는 'example.com'
|
|
26
|
+
|
|
27
|
+
// 캐시 키: 도메인 호스트별
|
|
28
|
+
const cacheKey = `theme:${host}`;
|
|
29
|
+
let templateCode = null;
|
|
30
|
+
|
|
31
|
+
// CacheManager가 있으면 캐시 사용 (5분)
|
|
32
|
+
if (ctx.app._cacheManager) {
|
|
33
|
+
templateCode = await ctx.app._cacheManager.remember(cacheKey, 300, async () => {
|
|
34
|
+
const domainRecord = await DomainModel
|
|
35
|
+
.where('domain', host)
|
|
36
|
+
.where('is_active', true)
|
|
37
|
+
.first();
|
|
38
|
+
|
|
39
|
+
if (domainRecord) {
|
|
40
|
+
// 관계 로드하여 template.code 가져오기
|
|
41
|
+
const TemplateModel = ctx.app.db.Template;
|
|
42
|
+
if (TemplateModel) {
|
|
43
|
+
const template = await TemplateModel.find(domainRecord.template_id);
|
|
44
|
+
if (template?.is_active) return template.code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null; // null도 캐시 (DB 미스 반복 방지)
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
// 캐시 없으면 직접 조회
|
|
51
|
+
const domainRecord = await DomainModel
|
|
52
|
+
.where('domain', host)
|
|
53
|
+
.where('is_active', true)
|
|
54
|
+
.first();
|
|
55
|
+
|
|
56
|
+
if (domainRecord) {
|
|
57
|
+
const TemplateModel = ctx.app.db.Template;
|
|
58
|
+
if (TemplateModel) {
|
|
59
|
+
const template = await TemplateModel.find(domainRecord.template_id);
|
|
60
|
+
if (template?.is_active) templateCode = template.code;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.theme = templateCode || configDefault;
|
|
66
|
+
} else {
|
|
67
|
+
// Domain 모델 미등록 시 YAML 기본값 사용
|
|
68
|
+
ctx.theme = configDefault;
|
|
69
|
+
}
|
|
17
70
|
|
|
18
71
|
await next();
|
|
19
72
|
};
|
package/lib/services/Service.js
CHANGED
|
@@ -72,7 +72,10 @@ export default class Service extends Base {
|
|
|
72
72
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* TTL 기반
|
|
75
|
+
* TTL 기반 캐시로 감싸기
|
|
76
|
+
*
|
|
77
|
+
* CacheManager가 초기화되어 있으면 Redis/File 캐시 사용.
|
|
78
|
+
* 없으면 인메모리 Map 폴백 (워커 간 공유 안됨).
|
|
76
79
|
*
|
|
77
80
|
* @param {string} key - 캐시 키
|
|
78
81
|
* @param {number} ttl - TTL (초)
|
|
@@ -85,6 +88,12 @@ export default class Service extends Base {
|
|
|
85
88
|
* });
|
|
86
89
|
*/
|
|
87
90
|
async withCache(key, ttl, fn) {
|
|
91
|
+
// CacheManager가 있으면 Redis/File 캐시 사용
|
|
92
|
+
if (this.cache) {
|
|
93
|
+
return this.cache.remember(key, ttl, fn);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 폴백: 인메모리 Map (워커 간 공유 불가)
|
|
88
97
|
const cached = _cache.get(key);
|
|
89
98
|
if (cached && cached.expires > Date.now()) {
|
|
90
99
|
return cached.value;
|
|
@@ -102,23 +111,32 @@ export default class Service extends Base {
|
|
|
102
111
|
* 캐시 키 무효화
|
|
103
112
|
* @param {string} key
|
|
104
113
|
*/
|
|
105
|
-
invalidateCache(key) {
|
|
114
|
+
async invalidateCache(key) {
|
|
115
|
+
if (this.cache) {
|
|
116
|
+
await this.cache.delete(key);
|
|
117
|
+
}
|
|
106
118
|
_cache.delete(key);
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/**
|
|
110
122
|
* 전체 캐시 초기화
|
|
111
123
|
*/
|
|
112
|
-
clearCache() {
|
|
124
|
+
async clearCache() {
|
|
125
|
+
if (this.cache) {
|
|
126
|
+
await this.cache.flush();
|
|
127
|
+
}
|
|
113
128
|
_cache.clear();
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
/**
|
|
117
132
|
* 캐시 존재 여부
|
|
118
133
|
* @param {string} key
|
|
119
|
-
* @returns {boolean}
|
|
134
|
+
* @returns {Promise<boolean>}
|
|
120
135
|
*/
|
|
121
|
-
hasCache(key) {
|
|
136
|
+
async hasCache(key) {
|
|
137
|
+
if (this.cache) {
|
|
138
|
+
return this.cache.has(key);
|
|
139
|
+
}
|
|
122
140
|
const cached = _cache.get(key);
|
|
123
141
|
return cached ? cached.expires > Date.now() : false;
|
|
124
142
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
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,13 +34,21 @@
|
|
|
34
34
|
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fuzionx/core": "^0.1.
|
|
37
|
+
"@fuzionx/core": "^0.1.63",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|
|
41
41
|
"mysql2": "^3.20.0",
|
|
42
42
|
"pg": "^8.20.0"
|
|
43
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"ioredis": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"ioredis": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
44
52
|
"devDependencies": {
|
|
45
53
|
"vitest": "^3.0.0"
|
|
46
54
|
},
|