@fuzionx/framework 0.1.38 → 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.
- package/README.md +501 -501
- package/bin/fx.js +12 -12
- package/cli/db-sync.js +99 -99
- package/cli/index.js +493 -493
- package/cli/templates/make/app/controllers/HomeController.js +14 -14
- package/cli/templates/make/app/routes/api.js +7 -7
- package/cli/templates/make/app/routes/web.js +5 -5
- package/cli/templates/make/app/views/default/errors/404.html +11 -11
- package/cli/templates/make/app/views/default/errors/500.html +14 -14
- package/cli/templates/make/app/views/default/layouts/main.html +22 -22
- package/cli/templates/make/app/views/default/pages/home.html +11 -11
- package/cli/templates/make/controller.js.tpl +40 -40
- package/cli/templates/make/event.js.tpl +8 -8
- package/cli/templates/make/job.js.tpl +10 -10
- package/cli/templates/make/middleware.js.tpl +10 -10
- package/cli/templates/make/model.js.tpl +15 -15
- package/cli/templates/make/service.js.tpl +15 -15
- package/cli/templates/make/task.js.tpl +15 -15
- package/cli/templates/make/test.js.tpl +7 -7
- package/cli/templates/make/worker.js.tpl +14 -14
- package/cli/templates/make/ws.js.tpl +18 -18
- package/index.js +67 -67
- package/lib/core/AppError.js +46 -46
- package/lib/core/Application.js +1006 -998
- package/lib/core/AutoLoader.js +226 -226
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +228 -228
- package/lib/core/Context.js +484 -460
- package/lib/database/ConnectionManager.js +208 -208
- package/lib/database/MariaModel.js +29 -29
- package/lib/database/Model.js +247 -247
- package/lib/database/ModelRegistry.js +72 -72
- package/lib/database/MongoModel.js +232 -232
- package/lib/database/Pagination.js +37 -37
- package/lib/database/PostgreModel.js +29 -29
- package/lib/database/QueryBuilder.js +172 -172
- package/lib/database/SQLiteModel.js +27 -27
- package/lib/database/SqlModel.js +257 -257
- package/lib/database/SqlQueryBuilder.js +332 -321
- package/lib/helpers/CryptoHelper.js +48 -48
- package/lib/helpers/FileHelper.js +61 -61
- package/lib/helpers/HashHelper.js +39 -39
- package/lib/helpers/I18nHelper.js +174 -174
- package/lib/helpers/Logger.js +108 -105
- package/lib/helpers/MediaHelper.js +84 -84
- package/lib/http/Controller.js +34 -34
- package/lib/http/ErrorHandler.js +136 -136
- package/lib/http/Middleware.js +43 -43
- package/lib/http/Router.js +109 -109
- package/lib/http/Validation.js +125 -124
- package/lib/middleware/apiAuth.js +79 -79
- package/lib/middleware/auth.js +42 -42
- package/lib/middleware/bodyParser.js +19 -19
- package/lib/middleware/cors.js +47 -47
- package/lib/middleware/csrf.js +32 -32
- package/lib/middleware/index.js +13 -13
- package/lib/middleware/session.js +27 -27
- package/lib/middleware/theme.js +20 -20
- package/lib/realtime/RoomManager.js +85 -85
- package/lib/realtime/WsHandler.js +107 -107
- package/lib/schedule/Job.js +38 -38
- package/lib/schedule/Queue.js +103 -102
- package/lib/schedule/Scheduler.js +171 -170
- package/lib/schedule/Task.js +39 -39
- package/lib/schedule/WorkerPool.js +225 -225
- package/lib/services/EventBus.js +94 -94
- package/lib/services/Service.js +261 -261
- package/lib/services/Storage.js +112 -112
- package/lib/utilities/ArrUtil.js +112 -112
- package/lib/utilities/DateUtil.js +98 -98
- package/lib/utilities/FunctionUtil.js +119 -119
- package/lib/utilities/NumUtil.js +75 -75
- package/lib/utilities/ObjectUtil.js +170 -170
- package/lib/utilities/PaginationUtil.js +81 -81
- package/lib/utilities/StrUtil.js +105 -105
- package/lib/utilities/index.js +18 -18
- package/lib/view/OpenAPI.js +231 -231
- package/lib/view/View.js +83 -83
- package/package.json +2 -2
- package/testing/index.js +232 -232
- package/cli/fx.js +0 -3
package/lib/database/SqlModel.js
CHANGED
|
@@ -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
|
+
}
|