@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.
Files changed (45) hide show
  1. package/bin/fx.js +12 -0
  2. package/index.js +64 -0
  3. package/lib/core/AppError.js +46 -0
  4. package/lib/core/Application.js +553 -0
  5. package/lib/core/AutoLoader.js +162 -0
  6. package/lib/core/Base.js +64 -0
  7. package/lib/core/Config.js +122 -0
  8. package/lib/core/Context.js +429 -0
  9. package/lib/database/ConnectionManager.js +192 -0
  10. package/lib/database/MariaModel.js +29 -0
  11. package/lib/database/Model.js +247 -0
  12. package/lib/database/ModelRegistry.js +72 -0
  13. package/lib/database/MongoModel.js +232 -0
  14. package/lib/database/Pagination.js +37 -0
  15. package/lib/database/PostgreModel.js +29 -0
  16. package/lib/database/QueryBuilder.js +172 -0
  17. package/lib/database/SQLiteModel.js +27 -0
  18. package/lib/database/SqlModel.js +252 -0
  19. package/lib/database/SqlQueryBuilder.js +309 -0
  20. package/lib/helpers/CryptoHelper.js +48 -0
  21. package/lib/helpers/FileHelper.js +61 -0
  22. package/lib/helpers/HashHelper.js +39 -0
  23. package/lib/helpers/I18nHelper.js +170 -0
  24. package/lib/helpers/Logger.js +105 -0
  25. package/lib/helpers/MediaHelper.js +38 -0
  26. package/lib/http/Controller.js +34 -0
  27. package/lib/http/ErrorHandler.js +135 -0
  28. package/lib/http/Middleware.js +43 -0
  29. package/lib/http/Router.js +109 -0
  30. package/lib/http/Validation.js +124 -0
  31. package/lib/middleware/index.js +286 -0
  32. package/lib/realtime/RoomManager.js +85 -0
  33. package/lib/realtime/WsHandler.js +107 -0
  34. package/lib/schedule/Job.js +34 -0
  35. package/lib/schedule/Queue.js +90 -0
  36. package/lib/schedule/Scheduler.js +161 -0
  37. package/lib/schedule/Task.js +39 -0
  38. package/lib/schedule/WorkerPool.js +225 -0
  39. package/lib/services/EventBus.js +94 -0
  40. package/lib/services/Service.js +261 -0
  41. package/lib/services/Storage.js +112 -0
  42. package/lib/view/OpenAPI.js +231 -0
  43. package/lib/view/View.js +72 -0
  44. package/package.json +52 -0
  45. package/testing/index.js +232 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * QueryBuilder — SQL/MongoDB 쿼리 빌더 (체이닝)
3
+ *
4
+ * @see docs/framework/02-database-orm.md
5
+ * @see docs/framework/class-design.mm.md (QueryBuilder)
6
+ */
7
+ export default class QueryBuilder {
8
+ constructor(model) {
9
+ this._model = model;
10
+ this._wheres = [];
11
+ this._orders = [];
12
+ this._limit = null;
13
+ this._offset = null;
14
+ this._selects = null;
15
+ this._withs = [];
16
+ // softDelete 자동 적용
17
+ this._softDelete = model?.softDelete || false;
18
+ this._withTrashed = false;
19
+ this._onlyTrashed = false;
20
+ }
21
+
22
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
+ // 체이닝 메서드
24
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
+
26
+ where(key, op, value) {
27
+ if (value === undefined) { value = op; op = '='; }
28
+ this._wheres.push({ key, op, value, type: 'and' });
29
+ return this;
30
+ }
31
+
32
+ /** OR 조건 */
33
+ orWhere(key, op, value) {
34
+ if (value === undefined) { value = op; op = '='; }
35
+ this._wheres.push({ key, op, value, type: 'or' });
36
+ return this;
37
+ }
38
+
39
+ /** IN 조건 */
40
+ whereIn(key, values) {
41
+ this._wheres.push({ key, op: 'IN', value: values, type: 'and' });
42
+ return this;
43
+ }
44
+
45
+ /** NOT NULL 조건 */
46
+ whereNotNull(key) {
47
+ this._wheres.push({ key, op: 'IS NOT NULL', value: null, type: 'and' });
48
+ return this;
49
+ }
50
+
51
+ /** NULL 조건 */
52
+ whereNull(key) {
53
+ this._wheres.push({ key, op: 'IS NULL', value: null, type: 'and' });
54
+ return this;
55
+ }
56
+
57
+ orderBy(column, direction = 'asc') {
58
+ this._orders.push({ column, direction });
59
+ return this;
60
+ }
61
+
62
+ limit(n) { this._limit = n; return this; }
63
+ offset(n) { this._offset = n; return this; }
64
+
65
+ select(...columns) {
66
+ this._selects = columns.flat();
67
+ return this;
68
+ }
69
+
70
+ with(...relations) {
71
+ this._withs.push(...relations.flat());
72
+ return this;
73
+ }
74
+
75
+ /** softDelete 포함 (삭제된 레코드도 조회) */
76
+ withTrashed() {
77
+ this._withTrashed = true;
78
+ return this;
79
+ }
80
+
81
+ /** softDelete만 조회 (삭제된 레코드만) */
82
+ onlyTrashed() {
83
+ this._onlyTrashed = true;
84
+ return this;
85
+ }
86
+
87
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
+ // 터미널 메서드
89
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+
91
+ /**
92
+ * 페이지네이션
93
+ * @param {number} page
94
+ * @param {number} perPage
95
+ * @returns {Promise<Pagination>}
96
+ */
97
+ async paginate(page = 1, perPage = 20) {
98
+ // Model 서브클래스에서 오버라이드하여 실제 DB 쿼리 실행
99
+ return {
100
+ data: [],
101
+ page,
102
+ perPage,
103
+ total: 0,
104
+ lastPage: 0,
105
+ _query: this._buildQuery(),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * 전체 결과 반환
111
+ * @returns {Promise<Array>}
112
+ */
113
+ async get() {
114
+ // 서브클래스에서 오버라이드
115
+ return [];
116
+ }
117
+
118
+ /**
119
+ * 단일 결과
120
+ * @returns {Promise<object|null>}
121
+ */
122
+ async first() {
123
+ this._limit = 1;
124
+ const results = await this.get();
125
+ return results[0] || null;
126
+ }
127
+
128
+ /**
129
+ * 결과 수
130
+ * @returns {Promise<number>}
131
+ */
132
+ async count() {
133
+ // 서브클래스에서 오버라이드
134
+ return 0;
135
+ }
136
+
137
+ /**
138
+ * 조건부 수정
139
+ * @param {object} data
140
+ * @returns {Promise<number>} 수정된 행 수
141
+ */
142
+ async update(data) {
143
+ // 서브클래스에서 오버라이드
144
+ return 0;
145
+ }
146
+
147
+ /**
148
+ * 조건부 삭제
149
+ * @returns {Promise<number>} 삭제된 행 수
150
+ */
151
+ async delete() {
152
+ // 서브클래스에서 오버라이드
153
+ return 0;
154
+ }
155
+
156
+ /**
157
+ * 빌드된 쿼리 파라미터 (디버깅/테스트용)
158
+ */
159
+ _buildQuery() {
160
+ return {
161
+ wheres: this._wheres,
162
+ orders: this._orders,
163
+ limit: this._limit,
164
+ offset: this._offset,
165
+ selects: this._selects,
166
+ withs: this._withs,
167
+ softDelete: this._softDelete,
168
+ withTrashed: this._withTrashed,
169
+ onlyTrashed: this._onlyTrashed,
170
+ };
171
+ }
172
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SQLiteModel — SQLite 모델 서브클래스 (기본 드라이버)
3
+ *
4
+ * better-sqlite3 기반 동기 API.
5
+ * 개발 환경에서 별도 DB 서버 없이 즉시 사용 가능.
6
+ *
7
+ * @see docs/framework/02-database-orm.md
8
+ *
9
+ * @example
10
+ * import { SQLiteModel } from '@fuzionx/framework';
11
+ *
12
+ * export default class User extends SQLiteModel {
13
+ * static table = 'users';
14
+ * static timestamps = true;
15
+ * static columns = {
16
+ * id: { type: 'increments' },
17
+ * name: { type: 'string', length: 100 },
18
+ * email: { type: 'string', length: 150, unique: true },
19
+ * };
20
+ * }
21
+ */
22
+ import SqlModel from './SqlModel.js';
23
+
24
+ export default class SQLiteModel extends SqlModel {
25
+ static driver = 'sqlite';
26
+ static connection = 'main';
27
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * SqlModel — SQL 공통 로직 (SQLite / MariaDB / PostgreSQL 공유)
3
+ *
4
+ * Knex-style 쿼리 빌딩을 공통화한 중간 클래스.
5
+ * SQLiteModel, MariaModel, PostgreModel이 이 클래스를 상속.
6
+ *
7
+ * @see docs/framework/02-database-orm.md
8
+ */
9
+ import Model from './Model.js';
10
+ import SqlQueryBuilder from './SqlQueryBuilder.js';
11
+
12
+ /**
13
+ * DB 드라이버에 맞는 datetime 문자열 반환
14
+ * MySQL/MariaDB: 'YYYY-MM-DD HH:MM:SS.SSS' (T/Z 제거, 밀리초 유지 — DATETIME(3) 지원)
15
+ * SQLite/PostgreSQL: ISO 8601
16
+ */
17
+ function dbNow(conn) {
18
+ if (conn?.type === 'knex' && (conn.driver === 'mariadb' || conn.driver === 'mysql')) {
19
+ return new Date().toISOString().replace('T', ' ').replace('Z', '');
20
+ }
21
+ return new Date().toISOString();
22
+ }
23
+
24
+ export default class SqlModel extends Model {
25
+ /** @type {string} DB 드라이버 — 서브클래스에서 오버라이드 */
26
+ static driver = 'sql';
27
+
28
+ /**
29
+ * ConnectionManager 인스턴스 (Application에서 주입)
30
+ * @type {import('./ConnectionManager.js').default|null}
31
+ */
32
+ static _connectionManager = null;
33
+
34
+ /**
35
+ * ConnectionManager를 모든 SqlModel 서브클래스에 주입
36
+ * @param {import('./ConnectionManager.js').default} cm
37
+ */
38
+ static setConnectionManager(cm) {
39
+ SqlModel._connectionManager = cm;
40
+ }
41
+
42
+ /**
43
+ * DB 연결 가져오기
44
+ * @returns {object} connection object
45
+ */
46
+ static getConnection() {
47
+ if (!SqlModel._connectionManager) {
48
+ throw new Error(
49
+ `DB not initialized. Ensure Application.boot() has been called and database is configured.`
50
+ );
51
+ }
52
+ return SqlModel._connectionManager.get(this.connection);
53
+ }
54
+
55
+ /** 쿼리 빌더 시작 — SQL 전용 */
56
+ static query() {
57
+ return new SqlQueryBuilder(this);
58
+ }
59
+
60
+ /** where 체이닝 시작 */
61
+ static where(key, op, value) {
62
+ return this.query().where(key, op, value);
63
+ }
64
+
65
+ /** limit 체이닝 */
66
+ static limit(n) {
67
+ return this.query().limit(n);
68
+ }
69
+
70
+ /** with 체이닝 */
71
+ static with(...relations) {
72
+ return this.query().with(...relations);
73
+ }
74
+
75
+ /** 페이지네이션 */
76
+ static paginate(page, perPage) {
77
+ return this.query().paginate(page, perPage);
78
+ }
79
+
80
+ /**
81
+ * 레코드 생성
82
+ * @param {object} data
83
+ * @returns {Promise<Model>}
84
+ */
85
+ static async create(data) {
86
+ const conn = this.getConnection();
87
+ const insertData = { ...data };
88
+
89
+ // timestamps 자동 관리
90
+ if (this.timestamps) {
91
+ const now = dbNow(conn);
92
+ if (!insertData.created_at) insertData.created_at = now;
93
+ if (!insertData.updated_at) insertData.updated_at = now;
94
+ }
95
+
96
+ if (conn.type === 'sqlite' && conn.db) {
97
+ const columns = Object.keys(insertData);
98
+ const placeholders = columns.map(() => '?').join(', ');
99
+ const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES (${placeholders})`;
100
+ const result = conn.db.prepare(sql).run(...Object.values(insertData));
101
+ const id = result.lastInsertRowid;
102
+ return new this({ ...insertData, [this.primaryKey]: Number(id) });
103
+ }
104
+
105
+ if (conn.type === 'knex' && conn.db) {
106
+ // MySQL: insert returns [insertId], PostgreSQL: needs .returning()
107
+ if (conn.driver === 'postgres') {
108
+ const [row] = await conn.db(this.table).insert(insertData).returning(this.primaryKey);
109
+ const id = typeof row === 'object' ? row[this.primaryKey] : row;
110
+ return new this({ ...insertData, [this.primaryKey]: id });
111
+ } else {
112
+ const [id] = await conn.db(this.table).insert(insertData);
113
+ return new this({ ...insertData, [this.primaryKey]: id });
114
+ }
115
+ }
116
+
117
+ // Stub: no DB connection
118
+ return new this({ ...insertData, [this.primaryKey]: Date.now() });
119
+ }
120
+
121
+ /**
122
+ * ID로 조회
123
+ * @param {*} id
124
+ * @returns {Promise<Model|null>}
125
+ */
126
+ static async find(id) {
127
+ return this.query().where(this.primaryKey, id).first();
128
+ }
129
+
130
+ /**
131
+ * 전체 조회
132
+ * @returns {Promise<Array>}
133
+ */
134
+ static async all() {
135
+ return this.query().get();
136
+ }
137
+
138
+ static async findAll() { return this.all(); }
139
+
140
+ /**
141
+ * 여러 ID 조회
142
+ * @param {Array} ids
143
+ */
144
+ static async findMany(ids) {
145
+ return this.query().whereIn(this.primaryKey, ids).get();
146
+ }
147
+
148
+ /**
149
+ * Raw SQL 실행
150
+ * @param {string} sql
151
+ * @param {Array} [bindings]
152
+ * @returns {Promise<Array>}
153
+ */
154
+ static async raw(sql, bindings = []) {
155
+ const conn = this.getConnection();
156
+
157
+ if (conn.type === 'sqlite' && conn.db) {
158
+ return conn.db.prepare(sql).all(...bindings);
159
+ }
160
+
161
+ if (conn.type === 'knex' && conn.db) {
162
+ const result = await conn.db.raw(sql, bindings);
163
+ // MySQL: [[rows], fields], PostgreSQL: { rows: [...] }
164
+ if (Array.isArray(result)) {
165
+ return Array.isArray(result[0]) ? result[0] : result;
166
+ }
167
+ return result.rows || [];
168
+ }
169
+
170
+ return [];
171
+ }
172
+
173
+ /**
174
+ * 네이티브 드라이버 접근
175
+ * @returns {object}
176
+ */
177
+ static nativeConnection() {
178
+ const conn = this.getConnection();
179
+ if (conn.type === 'sqlite') return conn.db;
180
+ if (conn.type === 'knex') return conn.db;
181
+ return null;
182
+ }
183
+
184
+ // ━━━━━━━━━━ 인스턴스 메서드 ━━━━━━━━━━
185
+
186
+ /**
187
+ * 인스턴스 수정
188
+ * @param {object} data
189
+ * @returns {Promise<this>}
190
+ */
191
+ async update(data) {
192
+ Object.assign(this._attributes, data);
193
+ Object.assign(this, data);
194
+
195
+ if (this.constructor.timestamps && !data.updated_at) {
196
+ const conn = this.constructor.getConnection();
197
+ const now = dbNow(conn);
198
+ this._attributes.updated_at = now;
199
+ this.updated_at = now;
200
+ data.updated_at = now;
201
+ }
202
+
203
+ const pk = this.constructor.primaryKey;
204
+ const id = this._attributes[pk];
205
+ if (id == null) return this;
206
+
207
+ const conn = this.constructor.getConnection();
208
+
209
+ if (conn.type === 'sqlite' && conn.db) {
210
+ const columns = Object.keys(data);
211
+ const sets = columns.map(c => `${c} = ?`).join(', ');
212
+ const sql = `UPDATE ${this.constructor.table} SET ${sets} WHERE ${pk} = ?`;
213
+ conn.db.prepare(sql).run(...Object.values(data), id);
214
+ } else if (conn.type === 'knex' && conn.db) {
215
+ await conn.db(this.constructor.table).where(pk, id).update(data);
216
+ }
217
+
218
+ return this;
219
+ }
220
+
221
+ /**
222
+ * 삭제 — softDelete 지원
223
+ */
224
+ async delete() {
225
+ if (this.constructor.softDelete) {
226
+ const conn = this.constructor.getConnection();
227
+ const now = dbNow(conn);
228
+ this._attributes.deleted_at = now;
229
+ this.deleted_at = now;
230
+ await this.update({ deleted_at: now });
231
+ return;
232
+ }
233
+ await this.forceDelete();
234
+ }
235
+
236
+ /**
237
+ * 실제 삭제 (softDelete 무시)
238
+ */
239
+ async forceDelete() {
240
+ const pk = this.constructor.primaryKey;
241
+ const id = this._attributes[pk];
242
+ if (id == null) return;
243
+
244
+ const conn = this.constructor.getConnection();
245
+
246
+ if (conn.type === 'sqlite' && conn.db) {
247
+ conn.db.prepare(`DELETE FROM ${this.constructor.table} WHERE ${pk} = ?`).run(id);
248
+ } else if (conn.type === 'knex' && conn.db) {
249
+ await conn.db(this.constructor.table).where(pk, id).delete();
250
+ }
251
+ }
252
+ }