@fuzionx/framework 0.1.2
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/bin/fx.js +12 -0
- package/index.js +64 -0
- package/lib/core/AppError.js +46 -0
- package/lib/core/Application.js +553 -0
- package/lib/core/AutoLoader.js +162 -0
- package/lib/core/Base.js +64 -0
- package/lib/core/Config.js +122 -0
- package/lib/core/Context.js +429 -0
- package/lib/database/ConnectionManager.js +192 -0
- package/lib/database/MariaModel.js +29 -0
- package/lib/database/Model.js +247 -0
- package/lib/database/ModelRegistry.js +72 -0
- package/lib/database/MongoModel.js +232 -0
- package/lib/database/Pagination.js +37 -0
- package/lib/database/PostgreModel.js +29 -0
- package/lib/database/QueryBuilder.js +172 -0
- package/lib/database/SQLiteModel.js +27 -0
- package/lib/database/SqlModel.js +252 -0
- package/lib/database/SqlQueryBuilder.js +309 -0
- package/lib/helpers/CryptoHelper.js +48 -0
- package/lib/helpers/FileHelper.js +61 -0
- package/lib/helpers/HashHelper.js +39 -0
- package/lib/helpers/I18nHelper.js +170 -0
- package/lib/helpers/Logger.js +105 -0
- package/lib/helpers/MediaHelper.js +38 -0
- package/lib/http/Controller.js +34 -0
- package/lib/http/ErrorHandler.js +135 -0
- package/lib/http/Middleware.js +43 -0
- package/lib/http/Router.js +109 -0
- package/lib/http/Validation.js +124 -0
- package/lib/middleware/index.js +286 -0
- package/lib/realtime/RoomManager.js +85 -0
- package/lib/realtime/WsHandler.js +107 -0
- package/lib/schedule/Job.js +34 -0
- package/lib/schedule/Queue.js +90 -0
- package/lib/schedule/Scheduler.js +161 -0
- package/lib/schedule/Task.js +39 -0
- package/lib/schedule/WorkerPool.js +225 -0
- package/lib/services/EventBus.js +94 -0
- package/lib/services/Service.js +261 -0
- package/lib/services/Storage.js +112 -0
- package/lib/view/OpenAPI.js +231 -0
- package/lib/view/View.js +72 -0
- package/package.json +52 -0
- package/testing/index.js +232 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoLoader — 앱 디렉토리 자동 스캔 + 등록
|
|
3
|
+
*
|
|
4
|
+
* Application.boot() 시 호출.
|
|
5
|
+
* models/, services/, middleware/ 등 자동 로드.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/04-bootstrap-lifecycle.md
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 디렉토리에서 .js 파일 목록 반환
|
|
14
|
+
* @param {string} dir
|
|
15
|
+
* @returns {Promise<string[]>}
|
|
16
|
+
*/
|
|
17
|
+
async function scanDir(dir) {
|
|
18
|
+
try {
|
|
19
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
20
|
+
return entries
|
|
21
|
+
.filter(e => e.isFile() && e.name.endsWith('.js') && !e.name.startsWith('.'))
|
|
22
|
+
.map(e => path.join(dir, e.name));
|
|
23
|
+
} catch {
|
|
24
|
+
return []; // 디렉토리 없으면 빈 배열
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 파일명에서 PascalCase 이름 추출
|
|
30
|
+
* 'UserController.js' → 'User'
|
|
31
|
+
* 'User.js' → 'User'
|
|
32
|
+
*/
|
|
33
|
+
function extractName(filePath, suffix = '') {
|
|
34
|
+
const base = path.basename(filePath, '.js');
|
|
35
|
+
return suffix && base.endsWith(suffix) ? base.slice(0, -suffix.length) : base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default class AutoLoader {
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('./Application.js').default} app
|
|
41
|
+
* @param {string} baseDir - 프로젝트 루트
|
|
42
|
+
*/
|
|
43
|
+
constructor(app, baseDir) {
|
|
44
|
+
this.app = app;
|
|
45
|
+
this.baseDir = baseDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 전체 스캔 + 등록 실행
|
|
50
|
+
*/
|
|
51
|
+
async load() {
|
|
52
|
+
await this.loadModels();
|
|
53
|
+
await this.loadServices();
|
|
54
|
+
await this.loadMiddleware();
|
|
55
|
+
await this.loadEvents();
|
|
56
|
+
await this.loadJobs();
|
|
57
|
+
await this.loadWsHandlers();
|
|
58
|
+
await this.loadRoutes();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** models/*.js → ModelRegistry */
|
|
62
|
+
async loadModels() {
|
|
63
|
+
const files = await scanDir(path.join(this.baseDir, 'models'));
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const mod = await import(file);
|
|
66
|
+
const ModelClass = mod.default;
|
|
67
|
+
if (!ModelClass) continue;
|
|
68
|
+
const name = extractName(file);
|
|
69
|
+
if (this.app.db) {
|
|
70
|
+
this.app.db.register(name, ModelClass);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** services/*.js → DI register */
|
|
76
|
+
async loadServices() {
|
|
77
|
+
const files = await scanDir(path.join(this.baseDir, 'services'));
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const mod = await import(file);
|
|
80
|
+
const ServiceClass = mod.default;
|
|
81
|
+
if (!ServiceClass) continue;
|
|
82
|
+
const name = extractName(file, 'Service');
|
|
83
|
+
this.app.register(`${name}Service`, (app) => new ServiceClass(app));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** middleware/*.js → 이름별 미들웨어 맵 */
|
|
88
|
+
async loadMiddleware() {
|
|
89
|
+
if (!this.app._middlewareRegistry) {
|
|
90
|
+
this.app._middlewareRegistry = new Map();
|
|
91
|
+
}
|
|
92
|
+
const files = await scanDir(path.join(this.baseDir, 'middleware'));
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const mod = await import(file);
|
|
95
|
+
const MwClass = mod.default;
|
|
96
|
+
if (!MwClass) continue;
|
|
97
|
+
const mwName = MwClass.name || extractName(file, 'Middleware').toLowerCase();
|
|
98
|
+
this.app._middlewareRegistry.set(mwName, MwClass);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** events/*.js → EventBus.on() */
|
|
103
|
+
async loadEvents() {
|
|
104
|
+
const files = await scanDir(path.join(this.baseDir, 'events'));
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const mod = await import(file);
|
|
107
|
+
// events/*.js 는 export default (app) => { app.on('...', handler) }
|
|
108
|
+
if (typeof mod.default === 'function') {
|
|
109
|
+
mod.default(this.app);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** jobs/*.js → Scheduler + Queue */
|
|
115
|
+
async loadJobs() {
|
|
116
|
+
const files = await scanDir(path.join(this.baseDir, 'jobs'));
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const mod = await import(file);
|
|
119
|
+
const JobClass = mod.default;
|
|
120
|
+
if (!JobClass) continue;
|
|
121
|
+
|
|
122
|
+
// schedule이 있으면 cron Job → Scheduler
|
|
123
|
+
if (JobClass.schedule && this.app._scheduler) {
|
|
124
|
+
this.app._scheduler.register(JobClass);
|
|
125
|
+
}
|
|
126
|
+
// queue이 있으면 Task → Queue
|
|
127
|
+
if (JobClass.queue && this.app._queue) {
|
|
128
|
+
const name = JobClass.name;
|
|
129
|
+
this.app._queue.register(name, JobClass);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** ws/*.js → WsHandler 등록 */
|
|
135
|
+
async loadWsHandlers() {
|
|
136
|
+
if (!this.app._wsHandlers) {
|
|
137
|
+
this.app._wsHandlers = new Map();
|
|
138
|
+
}
|
|
139
|
+
const files = await scanDir(path.join(this.baseDir, 'ws'));
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const mod = await import(file);
|
|
142
|
+
const HandlerClass = mod.default;
|
|
143
|
+
if (!HandlerClass) continue;
|
|
144
|
+
const ns = HandlerClass.namespace || '/';
|
|
145
|
+
this.app._wsHandlers.set(ns, HandlerClass);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** routes/*.js → Router.load() */
|
|
150
|
+
async loadRoutes() {
|
|
151
|
+
const files = await scanDir(path.join(this.baseDir, 'routes'));
|
|
152
|
+
if (!this.app._router) return;
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
const mod = await import(file);
|
|
155
|
+
if (typeof mod.default === 'function') {
|
|
156
|
+
this.app._router.load(mod.default);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { scanDir, extractName };
|
package/lib/core/Base.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base — 공통 기본 클래스
|
|
3
|
+
*
|
|
4
|
+
* Controller, Service, Middleware, Job, Task, WsHandler가 상속.
|
|
5
|
+
* 생성자에서 app을 받아 바로가기 프로퍼티 설정.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/class-design.mm.md (DI 주입 맵)
|
|
8
|
+
*/
|
|
9
|
+
export default class Base {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('./Application.js').default} app
|
|
12
|
+
*/
|
|
13
|
+
constructor(app) {
|
|
14
|
+
/** @type {import('./Application.js').default} */
|
|
15
|
+
this.app = app;
|
|
16
|
+
/** @type {object} ModelRegistry */
|
|
17
|
+
this.db = app.db;
|
|
18
|
+
/** @type {import('./Config.js').default} */
|
|
19
|
+
this.config = app.config;
|
|
20
|
+
/** @type {object} Logger */
|
|
21
|
+
this.logger = app.logger;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 서비스 조회 (DI)
|
|
26
|
+
* @param {string} name
|
|
27
|
+
* @returns {*}
|
|
28
|
+
*/
|
|
29
|
+
service(name) {
|
|
30
|
+
return this.app.make(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 이벤트 발행
|
|
35
|
+
* @param {string} event
|
|
36
|
+
* @param {*} [data]
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
*/
|
|
39
|
+
async emit(event, data, opts) {
|
|
40
|
+
await this.app.emit(event, data, opts);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 큐 Task 디스패치
|
|
45
|
+
* @param {string|Function} taskOrName
|
|
46
|
+
* @param {object} data
|
|
47
|
+
* @param {object} [opts]
|
|
48
|
+
*/
|
|
49
|
+
dispatch(taskOrName, data, opts) {
|
|
50
|
+
this.app.dispatch(taskOrName, data, opts);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @returns {object} WebSocket 매니저 */
|
|
54
|
+
get ws() { return this.app.ws; }
|
|
55
|
+
|
|
56
|
+
/** @returns {import('../schedule/WorkerPool.js').default} */
|
|
57
|
+
get worker() { return this.app.worker; }
|
|
58
|
+
|
|
59
|
+
/** @returns {import('./CryptoHelper.js').default} */
|
|
60
|
+
get crypto() { return this.app.crypto; }
|
|
61
|
+
|
|
62
|
+
/** @returns {import('./I18nHelper.js').default} */
|
|
63
|
+
get i18n() { return this.app.i18n; }
|
|
64
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config — 통합 설정 접근
|
|
3
|
+
*
|
|
4
|
+
* fuzionx.yaml의 3개 섹션(bridge, database, app)을 통합 관리.
|
|
5
|
+
* dot-notation으로 접근: config.get('app.auth.secret')
|
|
6
|
+
*
|
|
7
|
+
* .env 자동 로드 + ${VAR:default} 환경변수 치환.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/framework/17-config.md
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
export default class Config {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} raw - 파싱된 YAML 객체 전체
|
|
17
|
+
* raw.bridge — Bridge(Rust)가 파싱한 bridge: 섹션
|
|
18
|
+
* raw.database — JS가 파싱한 database: 섹션
|
|
19
|
+
* raw.app — JS가 파싱한 app: 섹션
|
|
20
|
+
*/
|
|
21
|
+
constructor(raw = {}) {
|
|
22
|
+
this.bridge = raw.bridge || {};
|
|
23
|
+
this.database = raw.database || {};
|
|
24
|
+
this.app = raw.app || {};
|
|
25
|
+
this._raw = raw;
|
|
26
|
+
this._cache = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* .env 파일 로드 (17-config.md)
|
|
31
|
+
* 부트 시 자동 호출. 이미 설정된 환경변수는 덮어쓰지 않음.
|
|
32
|
+
* @param {string} [baseDir='.']
|
|
33
|
+
*/
|
|
34
|
+
loadEnv(baseDir = '.') {
|
|
35
|
+
const envPath = path.resolve(baseDir, '.env');
|
|
36
|
+
if (!existsSync(envPath)) return;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
40
|
+
for (const line of content.split('\n')) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
43
|
+
const eqIdx = trimmed.indexOf('=');
|
|
44
|
+
if (eqIdx === -1) continue;
|
|
45
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
46
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
47
|
+
// .env 인용부호 제거 ("value" → value, 'value' → value)
|
|
48
|
+
value = value.replace(/^(['"])(.*)\1$/, '$2');
|
|
49
|
+
// 시스템 환경변수 우선 (17-config.md)
|
|
50
|
+
if (!(key in process.env)) {
|
|
51
|
+
process.env[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {} // .env 읽기 실패 시 무시
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* YAML 객체의 ${VAR} / ${VAR:default} 패턴 치환 (17-config.md)
|
|
59
|
+
* @param {object} obj
|
|
60
|
+
* @returns {object}
|
|
61
|
+
*/
|
|
62
|
+
static resolveEnvVars(obj) {
|
|
63
|
+
if (typeof obj === 'string') {
|
|
64
|
+
return obj.replace(/\$\{(\w+)(?::([^}]*))?\}/g, (_, varName, defaultVal) => {
|
|
65
|
+
const envVal = process.env[varName];
|
|
66
|
+
if (envVal !== undefined) return envVal;
|
|
67
|
+
if (defaultVal !== undefined) return defaultVal;
|
|
68
|
+
throw new Error(`Required environment variable '${varName}' is not set`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(obj)) return obj.map(item => Config.resolveEnvVars(item));
|
|
72
|
+
if (obj !== null && typeof obj === 'object') {
|
|
73
|
+
const result = {};
|
|
74
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
75
|
+
result[key] = Config.resolveEnvVars(value);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
return obj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* dot-notation으로 설정값 접근
|
|
84
|
+
* @param {string} path - 'app.auth.secret', 'database.main.host' 등
|
|
85
|
+
* @param {*} [defaultValue] - 키가 없을 때 반환값
|
|
86
|
+
* @returns {*}
|
|
87
|
+
*/
|
|
88
|
+
get(path, defaultValue = undefined) {
|
|
89
|
+
if (this._cache.has(path)) return this._cache.get(path);
|
|
90
|
+
|
|
91
|
+
const parts = path.split('.');
|
|
92
|
+
let current = this._raw;
|
|
93
|
+
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (current == null || typeof current !== 'object') {
|
|
96
|
+
return defaultValue;
|
|
97
|
+
}
|
|
98
|
+
current = current[part];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (current === undefined) return defaultValue;
|
|
102
|
+
this._cache.set(path, current);
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 설정값 존재 여부
|
|
108
|
+
* @param {string} path
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
has(path) {
|
|
112
|
+
return this.get(path) !== undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 전체 설정 반환 (읽기 전용)
|
|
117
|
+
* @returns {object}
|
|
118
|
+
*/
|
|
119
|
+
all() {
|
|
120
|
+
return this._raw;
|
|
121
|
+
}
|
|
122
|
+
}
|