@fuzionx/framework 0.1.39 → 0.1.42

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.
Files changed (80) hide show
  1. package/README.md +501 -501
  2. package/bin/fx.js +12 -12
  3. package/cli/db-sync.js +100 -99
  4. package/cli/index.js +494 -493
  5. package/cli/templates/make/app/controllers/HomeController.js +14 -14
  6. package/cli/templates/make/app/routes/api.js +7 -7
  7. package/cli/templates/make/app/routes/web.js +5 -5
  8. package/cli/templates/make/app/views/default/errors/404.html +11 -11
  9. package/cli/templates/make/app/views/default/errors/500.html +14 -14
  10. package/cli/templates/make/app/views/default/layouts/main.html +22 -22
  11. package/cli/templates/make/app/views/default/pages/home.html +11 -11
  12. package/cli/templates/make/controller.js.tpl +40 -40
  13. package/cli/templates/make/event.js.tpl +8 -8
  14. package/cli/templates/make/job.js.tpl +10 -10
  15. package/cli/templates/make/middleware.js.tpl +10 -10
  16. package/cli/templates/make/model.js.tpl +15 -15
  17. package/cli/templates/make/service.js.tpl +15 -15
  18. package/cli/templates/make/task.js.tpl +15 -15
  19. package/cli/templates/make/test.js.tpl +7 -7
  20. package/cli/templates/make/worker.js.tpl +14 -14
  21. package/cli/templates/make/ws.js.tpl +18 -18
  22. package/index.js +67 -67
  23. package/lib/core/AppError.js +46 -46
  24. package/lib/core/Application.js +1006 -1006
  25. package/lib/core/AutoLoader.js +227 -226
  26. package/lib/core/Base.js +64 -64
  27. package/lib/core/Config.js +228 -228
  28. package/lib/core/Context.js +484 -484
  29. package/lib/database/ConnectionManager.js +208 -208
  30. package/lib/database/MariaModel.js +29 -29
  31. package/lib/database/Model.js +247 -247
  32. package/lib/database/ModelRegistry.js +72 -72
  33. package/lib/database/MongoModel.js +232 -232
  34. package/lib/database/Pagination.js +37 -37
  35. package/lib/database/PostgreModel.js +29 -29
  36. package/lib/database/QueryBuilder.js +172 -172
  37. package/lib/database/SQLiteModel.js +27 -27
  38. package/lib/database/SqlModel.js +257 -257
  39. package/lib/database/SqlQueryBuilder.js +332 -332
  40. package/lib/helpers/CryptoHelper.js +48 -48
  41. package/lib/helpers/FileHelper.js +61 -61
  42. package/lib/helpers/HashHelper.js +39 -39
  43. package/lib/helpers/I18nHelper.js +174 -174
  44. package/lib/helpers/Logger.js +108 -108
  45. package/lib/helpers/MediaHelper.js +84 -84
  46. package/lib/http/Controller.js +34 -34
  47. package/lib/http/ErrorHandler.js +136 -136
  48. package/lib/http/Middleware.js +43 -43
  49. package/lib/http/Router.js +109 -109
  50. package/lib/http/Validation.js +125 -125
  51. package/lib/middleware/apiAuth.js +79 -79
  52. package/lib/middleware/auth.js +42 -42
  53. package/lib/middleware/bodyParser.js +19 -19
  54. package/lib/middleware/cors.js +47 -47
  55. package/lib/middleware/csrf.js +32 -32
  56. package/lib/middleware/index.js +13 -13
  57. package/lib/middleware/session.js +27 -27
  58. package/lib/middleware/theme.js +20 -20
  59. package/lib/realtime/RoomManager.js +85 -85
  60. package/lib/realtime/WsHandler.js +107 -107
  61. package/lib/schedule/Job.js +38 -38
  62. package/lib/schedule/Queue.js +103 -103
  63. package/lib/schedule/Scheduler.js +171 -171
  64. package/lib/schedule/Task.js +39 -39
  65. package/lib/schedule/WorkerPool.js +225 -225
  66. package/lib/services/EventBus.js +94 -94
  67. package/lib/services/Service.js +261 -261
  68. package/lib/services/Storage.js +112 -112
  69. package/lib/utilities/ArrUtil.js +112 -112
  70. package/lib/utilities/DateUtil.js +98 -98
  71. package/lib/utilities/FunctionUtil.js +119 -119
  72. package/lib/utilities/NumUtil.js +75 -75
  73. package/lib/utilities/ObjectUtil.js +170 -170
  74. package/lib/utilities/PaginationUtil.js +81 -81
  75. package/lib/utilities/StrUtil.js +105 -105
  76. package/lib/utilities/index.js +18 -18
  77. package/lib/view/OpenAPI.js +231 -231
  78. package/lib/view/View.js +83 -83
  79. package/package.json +2 -2
  80. package/testing/index.js +232 -232
package/testing/index.js CHANGED
@@ -1,232 +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
- }
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
+ }