@fuzionx/framework 0.1.39 → 0.1.41

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 +99 -99
  4. package/cli/index.js +493 -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 +226 -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
@@ -1,261 +1,261 @@
1
- /**
2
- * Service — 비즈니스 로직 기본 클래스
3
- *
4
- * 컨트롤러는 얇게, 비즈니스 로직은 서비스에.
5
- * 트랜잭션, 캐싱, 병렬 실행, 재시도, 뮤텍스 지원.
6
- *
7
- * @see docs/framework/07-service-layer.md
8
- * @see docs/framework/class-design.mm.md (Service)
9
- */
10
- import Base from '../core/Base.js';
11
- import { ServiceError } from '../core/AppError.js';
12
-
13
- /** @type {Map<string, { value: *, expires: number }>} 전역 캐시 스토어 */
14
- const _cache = new Map();
15
-
16
- /** @type {Map<string, Promise>} 전역 뮤텍스 락 */
17
- const _locks = new Map();
18
-
19
- export default class Service extends Base {
20
-
21
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
- // 트랜잭션
23
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
-
25
- /**
26
- * SQL 트랜잭션 실행
27
- * @param {Function} callback - (trx) => Promise<T>
28
- * @returns {Promise<T>}
29
- */
30
- async transaction(callback) {
31
- const cm = this.app?._connectionManager;
32
- if (!cm) throw new Error('Database not configured for transactions');
33
-
34
- const conn = cm.get();
35
- if (conn.type === 'knex' && conn.db) {
36
- return conn.db.transaction(callback);
37
- }
38
- throw new Error('SQL database not configured for transactions');
39
- }
40
-
41
- /**
42
- * MongoDB 트랜잭션 실행
43
- * @param {string} connName - MongoDB 연결명
44
- * @param {Function} callback - (session) => Promise<T>
45
- * @returns {Promise<T>}
46
- */
47
- async mongoTransaction(connName, callback) {
48
- const cm = this.app?._connectionManager;
49
- if (!cm) throw new Error('Database not configured');
50
-
51
- const conn = cm.get(connName);
52
- if (conn.type !== 'mongo' || !conn.mongoose) {
53
- throw new Error(`MongoDB connection '${connName}' not found`);
54
- }
55
-
56
- const session = await conn.mongoose.startSession();
57
- try {
58
- session.startTransaction();
59
- const result = await callback(session);
60
- await session.commitTransaction();
61
- return result;
62
- } catch (err) {
63
- await session.abortTransaction();
64
- throw err;
65
- } finally {
66
- session.endSession();
67
- }
68
- }
69
-
70
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
- // 캐시
72
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
-
74
- /**
75
- * TTL 기반 인메모리 캐시로 감싸기
76
- *
77
- * @param {string} key - 캐시 키
78
- * @param {number} ttl - TTL (초)
79
- * @param {Function} fn - 캐시 미스 시 실행할 함수
80
- * @returns {Promise<*>}
81
- *
82
- * @example
83
- * const stats = await this.withCache('dashboard:stats', 300, async () => {
84
- * return this.db.Order.raw('SELECT SUM(total) ...');
85
- * });
86
- */
87
- async withCache(key, ttl, fn) {
88
- const cached = _cache.get(key);
89
- if (cached && cached.expires > Date.now()) {
90
- return cached.value;
91
- }
92
-
93
- const value = await fn();
94
- _cache.set(key, {
95
- value,
96
- expires: Date.now() + (ttl * 1000),
97
- });
98
- return value;
99
- }
100
-
101
- /**
102
- * 캐시 키 무효화
103
- * @param {string} key
104
- */
105
- invalidateCache(key) {
106
- _cache.delete(key);
107
- }
108
-
109
- /**
110
- * 전체 캐시 초기화
111
- */
112
- clearCache() {
113
- _cache.clear();
114
- }
115
-
116
- /**
117
- * 캐시 존재 여부
118
- * @param {string} key
119
- * @returns {boolean}
120
- */
121
- hasCache(key) {
122
- const cached = _cache.get(key);
123
- return cached ? cached.expires > Date.now() : false;
124
- }
125
-
126
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
127
- // 병렬 실행
128
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
129
-
130
- /**
131
- * 여러 함수를 병렬 실행
132
- *
133
- * @param {Array<Function>} tasks - () => Promise 형태의 함수 배열
134
- * @param {object} [opts]
135
- * @param {boolean} [opts.failFast=false] - true면 하나 실패 시 전체 reject
136
- * @returns {Promise<Array>} 결과 배열 (failFast=false 시 에러도 포함)
137
- *
138
- * @example
139
- * const [users, orders, stats] = await this.runParallel([
140
- * () => this.db.User.count(),
141
- * () => this.db.Order.where('status', 'active').get(),
142
- * () => this.calculateStats(),
143
- * ]);
144
- */
145
- async runParallel(tasks, opts = {}) {
146
- const promises = tasks.map(fn => fn());
147
-
148
- if (opts.failFast) {
149
- return Promise.all(promises);
150
- }
151
-
152
- const results = await Promise.allSettled(promises);
153
- return results.map(r => {
154
- if (r.status === 'fulfilled') return r.value;
155
- // 실패 시 에러 객체 반환 (throw하지 않음)
156
- return r.reason;
157
- });
158
- }
159
-
160
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
- // 재시도
162
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
163
-
164
- /**
165
- * 재시도 + 지수 백오프
166
- *
167
- * @param {Function} fn - 실행할 함수
168
- * @param {object} [opts]
169
- * @param {number} [opts.retries=3] - 재시도 횟수
170
- * @param {number} [opts.delay=1000] - 초기 지연 (ms)
171
- * @param {number} [opts.backoff=2] - 백오프 배수
172
- * @param {Function} [opts.shouldRetry] - (err, attempt) => boolean
173
- * @returns {Promise<*>}
174
- *
175
- * @example
176
- * const data = await this.retryable(
177
- * () => fetch('https://api.external.com/data'),
178
- * { retries: 3, delay: 1000, backoff: 2 }
179
- * );
180
- */
181
- async retryable(fn, opts = {}) {
182
- const { retries = 3, delay = 1000, backoff = 2, shouldRetry } = opts;
183
- let lastError;
184
- let currentDelay = delay;
185
-
186
- for (let attempt = 0; attempt <= retries; attempt++) {
187
- try {
188
- return await fn();
189
- } catch (err) {
190
- lastError = err;
191
-
192
- if (attempt === retries) break;
193
- if (shouldRetry && !shouldRetry(err, attempt)) break;
194
-
195
- await new Promise(r => setTimeout(r, currentDelay));
196
- currentDelay *= backoff;
197
- }
198
- }
199
-
200
- throw lastError;
201
- }
202
-
203
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
204
- // 뮤텍스
205
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
206
-
207
- /**
208
- * 동시 실행 방지 (in-process lock)
209
- *
210
- * 같은 key에 대해 동시에 1개만 실행.
211
- * 이전 실행 완료 후 다음 실행.
212
- *
213
- * @param {string} key - 락 키
214
- * @param {Function} fn - 실행할 함수
215
- * @returns {Promise<*>}
216
- *
217
- * @example
218
- * await this.mutex('order:process:' + orderId, async () => {
219
- * const order = await this.db.Order.findOrFail(orderId);
220
- * await order.update({ status: 'processing' });
221
- * });
222
- */
223
- async mutex(key, fn) {
224
- // 이전 락이 있으면 대기
225
- while (_locks.has(key)) {
226
- await _locks.get(key);
227
- }
228
-
229
- // 새 락 설정
230
- let releaseLock;
231
- const lockPromise = new Promise(resolve => { releaseLock = resolve; });
232
- _locks.set(key, lockPromise);
233
-
234
- try {
235
- return await fn();
236
- } finally {
237
- _locks.delete(key);
238
- releaseLock();
239
- }
240
- }
241
-
242
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
- // 에러
244
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
245
-
246
- /**
247
- * 에러 생성 헬퍼
248
- * @param {string} message
249
- * @param {number} [status=400]
250
- * @returns {ServiceError}
251
- */
252
- error(message, status = 400) {
253
- return new ServiceError(message, status);
254
- }
255
- }
256
-
257
- /**
258
- * 캐시 스토어 접근 (테스트용)
259
- * @returns {Map}
260
- */
261
- export { _cache, _locks };
1
+ /**
2
+ * Service — 비즈니스 로직 기본 클래스
3
+ *
4
+ * 컨트롤러는 얇게, 비즈니스 로직은 서비스에.
5
+ * 트랜잭션, 캐싱, 병렬 실행, 재시도, 뮤텍스 지원.
6
+ *
7
+ * @see docs/framework/07-service-layer.md
8
+ * @see docs/framework/class-design.mm.md (Service)
9
+ */
10
+ import Base from '../core/Base.js';
11
+ import { ServiceError } from '../core/AppError.js';
12
+
13
+ /** @type {Map<string, { value: *, expires: number }>} 전역 캐시 스토어 */
14
+ const _cache = new Map();
15
+
16
+ /** @type {Map<string, Promise>} 전역 뮤텍스 락 */
17
+ const _locks = new Map();
18
+
19
+ export default class Service extends Base {
20
+
21
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
+ // 트랜잭션
23
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
+
25
+ /**
26
+ * SQL 트랜잭션 실행
27
+ * @param {Function} callback - (trx) => Promise<T>
28
+ * @returns {Promise<T>}
29
+ */
30
+ async transaction(callback) {
31
+ const cm = this.app?._connectionManager;
32
+ if (!cm) throw new Error('Database not configured for transactions');
33
+
34
+ const conn = cm.get();
35
+ if (conn.type === 'knex' && conn.db) {
36
+ return conn.db.transaction(callback);
37
+ }
38
+ throw new Error('SQL database not configured for transactions');
39
+ }
40
+
41
+ /**
42
+ * MongoDB 트랜잭션 실행
43
+ * @param {string} connName - MongoDB 연결명
44
+ * @param {Function} callback - (session) => Promise<T>
45
+ * @returns {Promise<T>}
46
+ */
47
+ async mongoTransaction(connName, callback) {
48
+ const cm = this.app?._connectionManager;
49
+ if (!cm) throw new Error('Database not configured');
50
+
51
+ const conn = cm.get(connName);
52
+ if (conn.type !== 'mongo' || !conn.mongoose) {
53
+ throw new Error(`MongoDB connection '${connName}' not found`);
54
+ }
55
+
56
+ const session = await conn.mongoose.startSession();
57
+ try {
58
+ session.startTransaction();
59
+ const result = await callback(session);
60
+ await session.commitTransaction();
61
+ return result;
62
+ } catch (err) {
63
+ await session.abortTransaction();
64
+ throw err;
65
+ } finally {
66
+ session.endSession();
67
+ }
68
+ }
69
+
70
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
+ // 캐시
72
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+
74
+ /**
75
+ * TTL 기반 인메모리 캐시로 감싸기
76
+ *
77
+ * @param {string} key - 캐시 키
78
+ * @param {number} ttl - TTL (초)
79
+ * @param {Function} fn - 캐시 미스 시 실행할 함수
80
+ * @returns {Promise<*>}
81
+ *
82
+ * @example
83
+ * const stats = await this.withCache('dashboard:stats', 300, async () => {
84
+ * return this.db.Order.raw('SELECT SUM(total) ...');
85
+ * });
86
+ */
87
+ async withCache(key, ttl, fn) {
88
+ const cached = _cache.get(key);
89
+ if (cached && cached.expires > Date.now()) {
90
+ return cached.value;
91
+ }
92
+
93
+ const value = await fn();
94
+ _cache.set(key, {
95
+ value,
96
+ expires: Date.now() + (ttl * 1000),
97
+ });
98
+ return value;
99
+ }
100
+
101
+ /**
102
+ * 캐시 키 무효화
103
+ * @param {string} key
104
+ */
105
+ invalidateCache(key) {
106
+ _cache.delete(key);
107
+ }
108
+
109
+ /**
110
+ * 전체 캐시 초기화
111
+ */
112
+ clearCache() {
113
+ _cache.clear();
114
+ }
115
+
116
+ /**
117
+ * 캐시 존재 여부
118
+ * @param {string} key
119
+ * @returns {boolean}
120
+ */
121
+ hasCache(key) {
122
+ const cached = _cache.get(key);
123
+ return cached ? cached.expires > Date.now() : false;
124
+ }
125
+
126
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
127
+ // 병렬 실행
128
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
129
+
130
+ /**
131
+ * 여러 함수를 병렬 실행
132
+ *
133
+ * @param {Array<Function>} tasks - () => Promise 형태의 함수 배열
134
+ * @param {object} [opts]
135
+ * @param {boolean} [opts.failFast=false] - true면 하나 실패 시 전체 reject
136
+ * @returns {Promise<Array>} 결과 배열 (failFast=false 시 에러도 포함)
137
+ *
138
+ * @example
139
+ * const [users, orders, stats] = await this.runParallel([
140
+ * () => this.db.User.count(),
141
+ * () => this.db.Order.where('status', 'active').get(),
142
+ * () => this.calculateStats(),
143
+ * ]);
144
+ */
145
+ async runParallel(tasks, opts = {}) {
146
+ const promises = tasks.map(fn => fn());
147
+
148
+ if (opts.failFast) {
149
+ return Promise.all(promises);
150
+ }
151
+
152
+ const results = await Promise.allSettled(promises);
153
+ return results.map(r => {
154
+ if (r.status === 'fulfilled') return r.value;
155
+ // 실패 시 에러 객체 반환 (throw하지 않음)
156
+ return r.reason;
157
+ });
158
+ }
159
+
160
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
+ // 재시도
162
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
163
+
164
+ /**
165
+ * 재시도 + 지수 백오프
166
+ *
167
+ * @param {Function} fn - 실행할 함수
168
+ * @param {object} [opts]
169
+ * @param {number} [opts.retries=3] - 재시도 횟수
170
+ * @param {number} [opts.delay=1000] - 초기 지연 (ms)
171
+ * @param {number} [opts.backoff=2] - 백오프 배수
172
+ * @param {Function} [opts.shouldRetry] - (err, attempt) => boolean
173
+ * @returns {Promise<*>}
174
+ *
175
+ * @example
176
+ * const data = await this.retryable(
177
+ * () => fetch('https://api.external.com/data'),
178
+ * { retries: 3, delay: 1000, backoff: 2 }
179
+ * );
180
+ */
181
+ async retryable(fn, opts = {}) {
182
+ const { retries = 3, delay = 1000, backoff = 2, shouldRetry } = opts;
183
+ let lastError;
184
+ let currentDelay = delay;
185
+
186
+ for (let attempt = 0; attempt <= retries; attempt++) {
187
+ try {
188
+ return await fn();
189
+ } catch (err) {
190
+ lastError = err;
191
+
192
+ if (attempt === retries) break;
193
+ if (shouldRetry && !shouldRetry(err, attempt)) break;
194
+
195
+ await new Promise(r => setTimeout(r, currentDelay));
196
+ currentDelay *= backoff;
197
+ }
198
+ }
199
+
200
+ throw lastError;
201
+ }
202
+
203
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
204
+ // 뮤텍스
205
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
206
+
207
+ /**
208
+ * 동시 실행 방지 (in-process lock)
209
+ *
210
+ * 같은 key에 대해 동시에 1개만 실행.
211
+ * 이전 실행 완료 후 다음 실행.
212
+ *
213
+ * @param {string} key - 락 키
214
+ * @param {Function} fn - 실행할 함수
215
+ * @returns {Promise<*>}
216
+ *
217
+ * @example
218
+ * await this.mutex('order:process:' + orderId, async () => {
219
+ * const order = await this.db.Order.findOrFail(orderId);
220
+ * await order.update({ status: 'processing' });
221
+ * });
222
+ */
223
+ async mutex(key, fn) {
224
+ // 이전 락이 있으면 대기
225
+ while (_locks.has(key)) {
226
+ await _locks.get(key);
227
+ }
228
+
229
+ // 새 락 설정
230
+ let releaseLock;
231
+ const lockPromise = new Promise(resolve => { releaseLock = resolve; });
232
+ _locks.set(key, lockPromise);
233
+
234
+ try {
235
+ return await fn();
236
+ } finally {
237
+ _locks.delete(key);
238
+ releaseLock();
239
+ }
240
+ }
241
+
242
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
+ // 에러
244
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
245
+
246
+ /**
247
+ * 에러 생성 헬퍼
248
+ * @param {string} message
249
+ * @param {number} [status=400]
250
+ * @returns {ServiceError}
251
+ */
252
+ error(message, status = 400) {
253
+ return new ServiceError(message, status);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 캐시 스토어 접근 (테스트용)
259
+ * @returns {Map}
260
+ */
261
+ export { _cache, _locks };