@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
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fuzionx/framework",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"fx": "./bin/fx.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.js",
|
|
12
|
+
"./testing": "./testing/index.js",
|
|
13
|
+
"./cli": "./cli/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:coverage": "vitest run --coverage"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"framework",
|
|
22
|
+
"mvc",
|
|
23
|
+
"controller",
|
|
24
|
+
"service",
|
|
25
|
+
"model",
|
|
26
|
+
"fuzionx"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@fuzionx/core": "^0.1.2",
|
|
38
|
+
"better-sqlite3": "^12.8.0",
|
|
39
|
+
"knex": "^3.2.5",
|
|
40
|
+
"mongoose": "^9.3.2",
|
|
41
|
+
"mysql2": "^3.20.0",
|
|
42
|
+
"pg": "^8.20.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vitest": "^3.0.0"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"index.js",
|
|
49
|
+
"lib/",
|
|
50
|
+
"testing/"
|
|
51
|
+
]
|
|
52
|
+
}
|
package/testing/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fuzionx/testing — 테스트 헬퍼
|
|
3
|
+
*
|
|
4
|
+
* in-process HTTP 클라이언트, 인증 헬퍼, DB 어설션.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/framework/20-testing.md
|
|
7
|
+
*/
|
|
8
|
+
import Application from '../lib/Application.js';
|
|
9
|
+
import Context from '../lib/Context.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 테스트용 앱 생성 (서버 미시작)
|
|
13
|
+
* @param {object} [opts] - Application 옵션
|
|
14
|
+
* @returns {Promise<TestApp>}
|
|
15
|
+
*/
|
|
16
|
+
export async function createApp(opts = {}) {
|
|
17
|
+
const app = new Application(opts);
|
|
18
|
+
await app.boot();
|
|
19
|
+
return new TestApp(app);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class TestApp {
|
|
23
|
+
constructor(app) {
|
|
24
|
+
this.app = app;
|
|
25
|
+
this.config = app.config;
|
|
26
|
+
this.db = app.db;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* HTTP 클라이언트 생성
|
|
31
|
+
* @returns {TestClient}
|
|
32
|
+
*/
|
|
33
|
+
client() {
|
|
34
|
+
return new TestClient(this.app);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* DB 어설션 헬퍼
|
|
39
|
+
*/
|
|
40
|
+
get assert() {
|
|
41
|
+
return new DbAssert(this.app);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async cleanup() {
|
|
45
|
+
// DB 연결 해제 등
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
50
|
+
// TestClient — in-process HTTP
|
|
51
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
|
|
53
|
+
export class TestClient {
|
|
54
|
+
constructor(app) {
|
|
55
|
+
this._app = app;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(url) { return new RequestBuilder(this._app, 'GET', url); }
|
|
59
|
+
post(url) { return new RequestBuilder(this._app, 'POST', url); }
|
|
60
|
+
put(url) { return new RequestBuilder(this._app, 'PUT', url); }
|
|
61
|
+
patch(url) { return new RequestBuilder(this._app, 'PATCH', url); }
|
|
62
|
+
delete(url) { return new RequestBuilder(this._app, 'DELETE', url); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class RequestBuilder {
|
|
66
|
+
constructor(app, method, url) {
|
|
67
|
+
this._app = app;
|
|
68
|
+
this._method = method;
|
|
69
|
+
this._url = url;
|
|
70
|
+
this._headers = {};
|
|
71
|
+
this._body = null;
|
|
72
|
+
this._query = {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
json(data) {
|
|
76
|
+
this._body = data;
|
|
77
|
+
this._headers['content-type'] = 'application/json';
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
form(data) {
|
|
82
|
+
this._body = data;
|
|
83
|
+
this._headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
header(key, value) {
|
|
88
|
+
this._headers[key.toLowerCase()] = value;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
query(params) {
|
|
93
|
+
Object.assign(this._query, params);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cookie(name, value) {
|
|
98
|
+
const existing = this._headers['cookie'] || '';
|
|
99
|
+
this._headers['cookie'] = existing ? `${existing}; ${name}=${value}` : `${name}=${value}`;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
loginAs(user) {
|
|
104
|
+
// 세션에 userId 설정 (시뮬레이션)
|
|
105
|
+
this._headers['x-test-user-id'] = String(user.id || user);
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
withToken(token) {
|
|
110
|
+
this._headers['authorization'] = `Bearer ${token}`;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 요청 실행
|
|
116
|
+
* @returns {Promise<TestResponse>}
|
|
117
|
+
*/
|
|
118
|
+
async then(resolve, reject) {
|
|
119
|
+
try {
|
|
120
|
+
const rawReq = {
|
|
121
|
+
method: this._method,
|
|
122
|
+
url: this._url,
|
|
123
|
+
path: this._url.split('?')[0],
|
|
124
|
+
query: this._query,
|
|
125
|
+
params: {},
|
|
126
|
+
headers: this._headers,
|
|
127
|
+
body: this._body,
|
|
128
|
+
remoteIp: '127.0.0.1',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const ctx = new Context(rawReq, this._app);
|
|
132
|
+
|
|
133
|
+
// TODO: 실제 라우터 매칭 + 미들웨어 체인 실행
|
|
134
|
+
// 지금은 ctx를 반환하여 기본 응답 구조만 테스트 가능
|
|
135
|
+
|
|
136
|
+
const response = new TestResponse(ctx);
|
|
137
|
+
resolve(response);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (reject) reject(err); else throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
145
|
+
// TestResponse — 응답 어설션
|
|
146
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
|
|
148
|
+
export class TestResponse {
|
|
149
|
+
constructor(ctx) {
|
|
150
|
+
this._ctx = ctx;
|
|
151
|
+
this.status = ctx._statusCode;
|
|
152
|
+
this.headers = ctx._headers;
|
|
153
|
+
this.body = ctx._body ? tryParseJSON(ctx._body) : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
assertStatus(code) {
|
|
157
|
+
if (this.status !== code) {
|
|
158
|
+
throw new Error(`Expected status ${code}, got ${this.status}`);
|
|
159
|
+
}
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
assertBody(expected) {
|
|
164
|
+
const actual = JSON.stringify(this.body);
|
|
165
|
+
const exp = JSON.stringify(expected);
|
|
166
|
+
if (actual !== exp) {
|
|
167
|
+
throw new Error(`Body mismatch:\nExpected: ${exp}\nActual: ${actual}`);
|
|
168
|
+
}
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
assertBodyContains(subset) {
|
|
173
|
+
for (const [key, value] of Object.entries(subset)) {
|
|
174
|
+
if (JSON.stringify(this.body?.[key]) !== JSON.stringify(value)) {
|
|
175
|
+
throw new Error(`Body missing ${key}: ${value}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
assertBodyHas(key) {
|
|
182
|
+
if (!(key in (this.body || {}))) {
|
|
183
|
+
throw new Error(`Body missing key: ${key}`);
|
|
184
|
+
}
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
assertHeader(key, value) {
|
|
189
|
+
const actual = this.headers[key];
|
|
190
|
+
if (value !== undefined && actual !== value) {
|
|
191
|
+
throw new Error(`Header '${key}' expected '${value}', got '${actual}'`);
|
|
192
|
+
}
|
|
193
|
+
if (value === undefined && actual === undefined) {
|
|
194
|
+
throw new Error(`Header '${key}' not present`);
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
assertRedirect(url) {
|
|
200
|
+
if (this.status < 300 || this.status >= 400) {
|
|
201
|
+
throw new Error(`Expected redirect, got ${this.status}`);
|
|
202
|
+
}
|
|
203
|
+
if (url && this.headers['Location'] !== url) {
|
|
204
|
+
throw new Error(`Expected redirect to ${url}, got ${this.headers['Location']}`);
|
|
205
|
+
}
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
211
|
+
// DB Assert (스텁)
|
|
212
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
|
|
214
|
+
export class DbAssert {
|
|
215
|
+
constructor(app) { this._app = app; }
|
|
216
|
+
|
|
217
|
+
async hasInDatabase(table, where) {
|
|
218
|
+
// Phase 5+ DB 연동 후 구현
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async missingFromDatabase(table, where) {
|
|
222
|
+
// Phase 5+ DB 연동 후 구현
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async countInDatabase(table, count, where) {
|
|
226
|
+
// Phase 5+ DB 연동 후 구현
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function tryParseJSON(str) {
|
|
231
|
+
try { return JSON.parse(str); } catch { return str; }
|
|
232
|
+
}
|