@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
@@ -1,257 +1,257 @@
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).map(c => SqlQueryBuilder._sanitizeName(c));
98
- const placeholders = columns.map(() => '?').join(', ');
99
- const safeTable = SqlQueryBuilder._sanitizeName(this.table);
100
- const sql = `INSERT INTO ${safeTable} (${columns.join(', ')}) VALUES (${placeholders})`;
101
- const result = conn.db.prepare(sql).run(...Object.values(insertData));
102
- const id = result.lastInsertRowid;
103
- return new this({ ...insertData, [this.primaryKey]: Number(id) });
104
- }
105
-
106
- if (conn.type === 'knex' && conn.db) {
107
- // MySQL: insert returns [insertId], PostgreSQL: needs .returning()
108
- if (conn.driver === 'postgres') {
109
- const [row] = await conn.db(this.table).insert(insertData).returning(this.primaryKey);
110
- const id = typeof row === 'object' ? row[this.primaryKey] : row;
111
- return new this({ ...insertData, [this.primaryKey]: id });
112
- } else {
113
- const [id] = await conn.db(this.table).insert(insertData);
114
- return new this({ ...insertData, [this.primaryKey]: id });
115
- }
116
- }
117
-
118
- // Stub: no DB connection
119
- return new this({ ...insertData, [this.primaryKey]: Date.now() });
120
- }
121
-
122
- /**
123
- * ID로 조회
124
- * @param {*} id
125
- * @returns {Promise<Model|null>}
126
- */
127
- static async find(id) {
128
- return this.query().where(this.primaryKey, id).first();
129
- }
130
-
131
- /**
132
- * 전체 조회
133
- * @returns {Promise<Array>}
134
- */
135
- static async all() {
136
- return this.query().get();
137
- }
138
-
139
- static async findAll() { return this.all(); }
140
-
141
- /**
142
- * 여러 ID 조회
143
- * @param {Array} ids
144
- */
145
- static async findMany(ids) {
146
- return this.query().whereIn(this.primaryKey, ids).get();
147
- }
148
-
149
- /**
150
- * Raw SQL 실행
151
- * @param {string} sql
152
- * @param {Array} [bindings]
153
- * @returns {Promise<Array>}
154
- */
155
- static async raw(sql, bindings = []) {
156
- const conn = this.getConnection();
157
-
158
- if (conn.type === 'sqlite' && conn.db) {
159
- return conn.db.prepare(sql).all(...bindings);
160
- }
161
-
162
- if (conn.type === 'knex' && conn.db) {
163
- const result = await conn.db.raw(sql, bindings);
164
- // MySQL: [[rows], fields], PostgreSQL: { rows: [...] }
165
- if (Array.isArray(result)) {
166
- return Array.isArray(result[0]) ? result[0] : result;
167
- }
168
- return result.rows || [];
169
- }
170
-
171
- return [];
172
- }
173
-
174
- /**
175
- * 네이티브 드라이버 접근
176
- * @returns {object}
177
- */
178
- static nativeConnection() {
179
- const conn = this.getConnection();
180
- if (conn.type === 'sqlite') return conn.db;
181
- if (conn.type === 'knex') return conn.db;
182
- return null;
183
- }
184
-
185
- // ━━━━━━━━━━ 인스턴스 메서드 ━━━━━━━━━━
186
-
187
- /**
188
- * 인스턴스 수정
189
- * @param {object} data
190
- * @returns {Promise<this>}
191
- */
192
- async update(data) {
193
- Object.assign(this._attributes, data);
194
- Object.assign(this, data);
195
-
196
- if (this.constructor.timestamps && !data.updated_at) {
197
- const conn = this.constructor.getConnection();
198
- const now = dbNow(conn);
199
- this._attributes.updated_at = now;
200
- this.updated_at = now;
201
- data.updated_at = now;
202
- }
203
-
204
- const pk = this.constructor.primaryKey;
205
- const id = this._attributes[pk];
206
- if (id == null) return this;
207
-
208
- const conn = this.constructor.getConnection();
209
-
210
- if (conn.type === 'sqlite' && conn.db) {
211
- const columns = Object.keys(data).map(c => SqlQueryBuilder._sanitizeName(c));
212
- const pk = SqlQueryBuilder._sanitizeName(this.constructor.primaryKey);
213
- const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
214
- const sets = columns.map(c => `${c} = ?`).join(', ');
215
- const sql = `UPDATE ${safeTable} SET ${sets} WHERE ${pk} = ?`;
216
- conn.db.prepare(sql).run(...Object.values(data), id);
217
- } else if (conn.type === 'knex' && conn.db) {
218
- await conn.db(this.constructor.table).where(pk, id).update(data);
219
- }
220
-
221
- return this;
222
- }
223
-
224
- /**
225
- * 삭제 — softDelete 지원
226
- */
227
- async delete() {
228
- if (this.constructor.softDelete) {
229
- const conn = this.constructor.getConnection();
230
- const now = dbNow(conn);
231
- this._attributes.deleted_at = now;
232
- this.deleted_at = now;
233
- await this.update({ deleted_at: now });
234
- return;
235
- }
236
- await this.forceDelete();
237
- }
238
-
239
- /**
240
- * 실제 삭제 (softDelete 무시)
241
- */
242
- async forceDelete() {
243
- const pk = this.constructor.primaryKey;
244
- const id = this._attributes[pk];
245
- if (id == null) return;
246
-
247
- const conn = this.constructor.getConnection();
248
-
249
- if (conn.type === 'sqlite' && conn.db) {
250
- const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
251
- const safePk = SqlQueryBuilder._sanitizeName(pk);
252
- conn.db.prepare(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`).run(id);
253
- } else if (conn.type === 'knex' && conn.db) {
254
- await conn.db(this.constructor.table).where(pk, id).delete();
255
- }
256
- }
257
- }
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).map(c => SqlQueryBuilder._sanitizeName(c));
98
+ const placeholders = columns.map(() => '?').join(', ');
99
+ const safeTable = SqlQueryBuilder._sanitizeName(this.table);
100
+ const sql = `INSERT INTO ${safeTable} (${columns.join(', ')}) VALUES (${placeholders})`;
101
+ const result = conn.db.prepare(sql).run(...Object.values(insertData));
102
+ const id = result.lastInsertRowid;
103
+ return new this({ ...insertData, [this.primaryKey]: Number(id) });
104
+ }
105
+
106
+ if (conn.type === 'knex' && conn.db) {
107
+ // MySQL: insert returns [insertId], PostgreSQL: needs .returning()
108
+ if (conn.driver === 'postgres') {
109
+ const [row] = await conn.db(this.table).insert(insertData).returning(this.primaryKey);
110
+ const id = typeof row === 'object' ? row[this.primaryKey] : row;
111
+ return new this({ ...insertData, [this.primaryKey]: id });
112
+ } else {
113
+ const [id] = await conn.db(this.table).insert(insertData);
114
+ return new this({ ...insertData, [this.primaryKey]: id });
115
+ }
116
+ }
117
+
118
+ // Stub: no DB connection
119
+ return new this({ ...insertData, [this.primaryKey]: Date.now() });
120
+ }
121
+
122
+ /**
123
+ * ID로 조회
124
+ * @param {*} id
125
+ * @returns {Promise<Model|null>}
126
+ */
127
+ static async find(id) {
128
+ return this.query().where(this.primaryKey, id).first();
129
+ }
130
+
131
+ /**
132
+ * 전체 조회
133
+ * @returns {Promise<Array>}
134
+ */
135
+ static async all() {
136
+ return this.query().get();
137
+ }
138
+
139
+ static async findAll() { return this.all(); }
140
+
141
+ /**
142
+ * 여러 ID 조회
143
+ * @param {Array} ids
144
+ */
145
+ static async findMany(ids) {
146
+ return this.query().whereIn(this.primaryKey, ids).get();
147
+ }
148
+
149
+ /**
150
+ * Raw SQL 실행
151
+ * @param {string} sql
152
+ * @param {Array} [bindings]
153
+ * @returns {Promise<Array>}
154
+ */
155
+ static async raw(sql, bindings = []) {
156
+ const conn = this.getConnection();
157
+
158
+ if (conn.type === 'sqlite' && conn.db) {
159
+ return conn.db.prepare(sql).all(...bindings);
160
+ }
161
+
162
+ if (conn.type === 'knex' && conn.db) {
163
+ const result = await conn.db.raw(sql, bindings);
164
+ // MySQL: [[rows], fields], PostgreSQL: { rows: [...] }
165
+ if (Array.isArray(result)) {
166
+ return Array.isArray(result[0]) ? result[0] : result;
167
+ }
168
+ return result.rows || [];
169
+ }
170
+
171
+ return [];
172
+ }
173
+
174
+ /**
175
+ * 네이티브 드라이버 접근
176
+ * @returns {object}
177
+ */
178
+ static nativeConnection() {
179
+ const conn = this.getConnection();
180
+ if (conn.type === 'sqlite') return conn.db;
181
+ if (conn.type === 'knex') return conn.db;
182
+ return null;
183
+ }
184
+
185
+ // ━━━━━━━━━━ 인스턴스 메서드 ━━━━━━━━━━
186
+
187
+ /**
188
+ * 인스턴스 수정
189
+ * @param {object} data
190
+ * @returns {Promise<this>}
191
+ */
192
+ async update(data) {
193
+ Object.assign(this._attributes, data);
194
+ Object.assign(this, data);
195
+
196
+ if (this.constructor.timestamps && !data.updated_at) {
197
+ const conn = this.constructor.getConnection();
198
+ const now = dbNow(conn);
199
+ this._attributes.updated_at = now;
200
+ this.updated_at = now;
201
+ data.updated_at = now;
202
+ }
203
+
204
+ const pk = this.constructor.primaryKey;
205
+ const id = this._attributes[pk];
206
+ if (id == null) return this;
207
+
208
+ const conn = this.constructor.getConnection();
209
+
210
+ if (conn.type === 'sqlite' && conn.db) {
211
+ const columns = Object.keys(data).map(c => SqlQueryBuilder._sanitizeName(c));
212
+ const pk = SqlQueryBuilder._sanitizeName(this.constructor.primaryKey);
213
+ const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
214
+ const sets = columns.map(c => `${c} = ?`).join(', ');
215
+ const sql = `UPDATE ${safeTable} SET ${sets} WHERE ${pk} = ?`;
216
+ conn.db.prepare(sql).run(...Object.values(data), id);
217
+ } else if (conn.type === 'knex' && conn.db) {
218
+ await conn.db(this.constructor.table).where(pk, id).update(data);
219
+ }
220
+
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * 삭제 — softDelete 지원
226
+ */
227
+ async delete() {
228
+ if (this.constructor.softDelete) {
229
+ const conn = this.constructor.getConnection();
230
+ const now = dbNow(conn);
231
+ this._attributes.deleted_at = now;
232
+ this.deleted_at = now;
233
+ await this.update({ deleted_at: now });
234
+ return;
235
+ }
236
+ await this.forceDelete();
237
+ }
238
+
239
+ /**
240
+ * 실제 삭제 (softDelete 무시)
241
+ */
242
+ async forceDelete() {
243
+ const pk = this.constructor.primaryKey;
244
+ const id = this._attributes[pk];
245
+ if (id == null) return;
246
+
247
+ const conn = this.constructor.getConnection();
248
+
249
+ if (conn.type === 'sqlite' && conn.db) {
250
+ const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
251
+ const safePk = SqlQueryBuilder._sanitizeName(pk);
252
+ conn.db.prepare(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`).run(id);
253
+ } else if (conn.type === 'knex' && conn.db) {
254
+ await conn.db(this.constructor.table).where(pk, id).delete();
255
+ }
256
+ }
257
+ }