@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.
- package/bin/fx.js +12 -0
- package/index.js +64 -0
- package/lib/core/AppError.js +46 -0
- package/lib/core/Application.js +553 -0
- package/lib/core/AutoLoader.js +162 -0
- package/lib/core/Base.js +64 -0
- package/lib/core/Config.js +122 -0
- package/lib/core/Context.js +429 -0
- package/lib/database/ConnectionManager.js +192 -0
- package/lib/database/MariaModel.js +29 -0
- package/lib/database/Model.js +247 -0
- package/lib/database/ModelRegistry.js +72 -0
- package/lib/database/MongoModel.js +232 -0
- package/lib/database/Pagination.js +37 -0
- package/lib/database/PostgreModel.js +29 -0
- package/lib/database/QueryBuilder.js +172 -0
- package/lib/database/SQLiteModel.js +27 -0
- package/lib/database/SqlModel.js +252 -0
- package/lib/database/SqlQueryBuilder.js +309 -0
- package/lib/helpers/CryptoHelper.js +48 -0
- package/lib/helpers/FileHelper.js +61 -0
- package/lib/helpers/HashHelper.js +39 -0
- package/lib/helpers/I18nHelper.js +170 -0
- package/lib/helpers/Logger.js +105 -0
- package/lib/helpers/MediaHelper.js +38 -0
- package/lib/http/Controller.js +34 -0
- package/lib/http/ErrorHandler.js +135 -0
- package/lib/http/Middleware.js +43 -0
- package/lib/http/Router.js +109 -0
- package/lib/http/Validation.js +124 -0
- package/lib/middleware/index.js +286 -0
- package/lib/realtime/RoomManager.js +85 -0
- package/lib/realtime/WsHandler.js +107 -0
- package/lib/schedule/Job.js +34 -0
- package/lib/schedule/Queue.js +90 -0
- package/lib/schedule/Scheduler.js +161 -0
- package/lib/schedule/Task.js +39 -0
- package/lib/schedule/WorkerPool.js +225 -0
- package/lib/services/EventBus.js +94 -0
- package/lib/services/Service.js +261 -0
- package/lib/services/Storage.js +112 -0
- package/lib/view/OpenAPI.js +231 -0
- package/lib/view/View.js +72 -0
- package/package.json +52 -0
- package/testing/index.js +232 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model — 데이터베이스 테이블/컬렉션 추상화
|
|
3
|
+
*
|
|
4
|
+
* @see docs/framework/02-database-orm.md
|
|
5
|
+
* @see docs/framework/class-design.mm.md (Model)
|
|
6
|
+
*/
|
|
7
|
+
import QueryBuilder from './QueryBuilder.js';
|
|
8
|
+
import { ServiceError } from '../core/AppError.js';
|
|
9
|
+
|
|
10
|
+
export default class Model {
|
|
11
|
+
// ── Static 멤버 (서브클래스에서 오버라이드) ──
|
|
12
|
+
static table = ''; // 테이블명 / 컬렉션명
|
|
13
|
+
static primaryKey = 'id'; // 기본키
|
|
14
|
+
static connection = 'main'; // DB 연결명
|
|
15
|
+
static timestamps = true; // created_at, updated_at 자동
|
|
16
|
+
static softDelete = false; // deleted_at 소프트 삭제
|
|
17
|
+
static hidden = []; // JSON 직렬화 시 제외 필드
|
|
18
|
+
static columns = {}; // SQL 스키마 정의 (fx db:sync 용)
|
|
19
|
+
static indexes = []; // 인덱스 정의 [{ columns: ['role'], unique? }]
|
|
20
|
+
static schema = {}; // MongoDB 스키마
|
|
21
|
+
|
|
22
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
23
|
+
// 인스턴스 (레코드 래퍼)
|
|
24
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
|
|
26
|
+
constructor(attributes = {}) {
|
|
27
|
+
this._attributes = { ...attributes };
|
|
28
|
+
this._original = { ...attributes };
|
|
29
|
+
// 속성을 인스턴스 프로퍼티로도 노출 (backward compat)
|
|
30
|
+
Object.assign(this, attributes);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 속성 접근 */
|
|
34
|
+
get(key) { return key ? this._attributes[key] : { ...this._attributes }; }
|
|
35
|
+
set(key, value) { this._attributes[key] = value; }
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 뮤테이터용 내부 설정 (02-database-orm.md)
|
|
39
|
+
* set password(v) { this._set('password', app.hash.argon2(v)); }
|
|
40
|
+
*/
|
|
41
|
+
_set(key, value) {
|
|
42
|
+
this._attributes[key] = value;
|
|
43
|
+
this[key] = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 인스턴스 수정 — DB update + 로컬 반영
|
|
48
|
+
* @param {object} data
|
|
49
|
+
* @returns {Promise<this>}
|
|
50
|
+
*/
|
|
51
|
+
async update(data) {
|
|
52
|
+
Object.assign(this._attributes, data);
|
|
53
|
+
// Model 서브클래스(Maria/Postgre/Mongo)에서 오버라이드하여 실제 DB 쿼리 실행
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 인스턴스 삭제
|
|
59
|
+
* softDelete가 true면 deleted_at 설정, 아니면 실제 삭제.
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async delete() {
|
|
63
|
+
if (this.constructor.softDelete) {
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
this._attributes.deleted_at = now;
|
|
66
|
+
this.deleted_at = now;
|
|
67
|
+
await this.update({ deleted_at: now });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// 서브클래스에서 오버라이드하여 실제 DB 삭제
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 소프트 삭제 복원 — deleted_at = null
|
|
75
|
+
* @returns {Promise<this>}
|
|
76
|
+
*/
|
|
77
|
+
async restore() {
|
|
78
|
+
this._attributes.deleted_at = null;
|
|
79
|
+
this.deleted_at = null;
|
|
80
|
+
await this.update({ deleted_at: null });
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 강제 삭제 — softDelete 무시하고 실제 삭제
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
async forceDelete() {
|
|
89
|
+
// 서브클래스에서 오버라이드하여 실제 DB 삭제
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* JSON 직렬화 — hidden 필드 제외
|
|
94
|
+
* @returns {object}
|
|
95
|
+
*/
|
|
96
|
+
toJSON() {
|
|
97
|
+
const obj = { ...this._attributes };
|
|
98
|
+
const hidden = this.constructor.hidden || [];
|
|
99
|
+
for (const key of hidden) {
|
|
100
|
+
delete obj[key];
|
|
101
|
+
}
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
106
|
+
// 관계 (스텁 — 서브클래스에서 오버라이드)
|
|
107
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
108
|
+
|
|
109
|
+
/** 1:N 관계 */
|
|
110
|
+
hasMany(model, fk) {
|
|
111
|
+
return { type: 'hasMany', model, fk: fk || `${this.constructor.table.replace(/s$/, '')}_id` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** N:1 관계 */
|
|
115
|
+
belongsTo(model, fk) {
|
|
116
|
+
return { type: 'belongsTo', model, fk: fk || `${model.toLowerCase()}_id` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 1:1 관계 */
|
|
120
|
+
hasOne(model, fk) {
|
|
121
|
+
return { type: 'hasOne', model, fk: fk || `${this.constructor.table.replace(/s$/, '')}_id` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** N:N 관계 */
|
|
125
|
+
belongsToMany(model, pivot) {
|
|
126
|
+
return { type: 'belongsToMany', model, pivot };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
130
|
+
// Static 메서드 (쿼리 진입점)
|
|
131
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
132
|
+
|
|
133
|
+
/** 쿼리 빌더 시작 */
|
|
134
|
+
static query() {
|
|
135
|
+
return new QueryBuilder(this);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** where 체이닝 시작 */
|
|
139
|
+
static where(key, op, value) {
|
|
140
|
+
return this.query().where(key, op, value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** limit 체이닝 시작 */
|
|
144
|
+
static limit(n) {
|
|
145
|
+
return this.query().limit(n);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 관계 eager-load 진입점 */
|
|
149
|
+
static with(...relations) {
|
|
150
|
+
return this.query().with(...relations);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 페이지네이션 진입점 */
|
|
154
|
+
static paginate(page, perPage) {
|
|
155
|
+
return this.query().paginate(page, perPage);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ID로 조회
|
|
160
|
+
* @param {*} id
|
|
161
|
+
* @returns {Promise<Model|null>}
|
|
162
|
+
*/
|
|
163
|
+
static async find(id) {
|
|
164
|
+
return this.query().where(this.primaryKey, id).first();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ID로 조회 (없으면 404 에러)
|
|
169
|
+
* @param {*} id
|
|
170
|
+
* @returns {Promise<Model>}
|
|
171
|
+
*/
|
|
172
|
+
static async findOrFail(id) {
|
|
173
|
+
const result = await this.find(id);
|
|
174
|
+
if (!result) {
|
|
175
|
+
throw new ServiceError(`${this.name} not found`, 404);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 전체 조회
|
|
182
|
+
* @returns {Promise<Array>}
|
|
183
|
+
*/
|
|
184
|
+
static async all() {
|
|
185
|
+
return this.query().get();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** all() alias */
|
|
189
|
+
static async findAll() {
|
|
190
|
+
return this.all();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 여러 ID 조회
|
|
195
|
+
* @param {Array} ids
|
|
196
|
+
* @returns {Promise<Array>}
|
|
197
|
+
*/
|
|
198
|
+
static async findMany(ids) {
|
|
199
|
+
return this.query().whereIn(this.primaryKey, ids).get();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 카운트
|
|
204
|
+
* @param {object} [where] - 간단 조건
|
|
205
|
+
* @returns {Promise<number>}
|
|
206
|
+
*/
|
|
207
|
+
static async count(where) {
|
|
208
|
+
let qb = this.query();
|
|
209
|
+
if (where) {
|
|
210
|
+
for (const [key, value] of Object.entries(where)) {
|
|
211
|
+
qb = qb.where(key, value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return qb.count();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 생성
|
|
219
|
+
* @param {object} data
|
|
220
|
+
* @returns {Promise<Model>}
|
|
221
|
+
*/
|
|
222
|
+
static async create(data) {
|
|
223
|
+
// Model 서브클래스에서 오버라이드하여 실제 DB insert
|
|
224
|
+
const instance = new this({ ...data, [this.primaryKey]: Date.now() });
|
|
225
|
+
return instance;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Raw 쿼리 (DB 고유 기능)
|
|
230
|
+
* @param {string|Array} query - SQL 문자열 또는 MongoDB pipeline
|
|
231
|
+
* @param {Array} [bindings] - SQL 바인딩
|
|
232
|
+
* @returns {Promise<Array>}
|
|
233
|
+
*/
|
|
234
|
+
static async raw(query, bindings) {
|
|
235
|
+
// 서브클래스에서 오버라이드 (Knex.raw / Mongoose.aggregate)
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 네이티브 드라이버 접근
|
|
241
|
+
* @returns {object} - Knex 인스턴스 | Mongoose 인스턴스
|
|
242
|
+
*/
|
|
243
|
+
static nativeConnection() {
|
|
244
|
+
// 서브클래스에서 오버라이드
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelRegistry — Proxy 기반 db.User 접근
|
|
3
|
+
*
|
|
4
|
+
* app.db.User → ModelRegistry에서 'User' 모델 반환.
|
|
5
|
+
* Proxy로 동적 프로퍼티 접근 지원.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/class-design.mm.md (ModelRegistry)
|
|
8
|
+
*/
|
|
9
|
+
export default class ModelRegistry {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._models = new Map();
|
|
12
|
+
this._knex = null;
|
|
13
|
+
this._mongoConnections = null;
|
|
14
|
+
|
|
15
|
+
// Proxy로 동적 접근 지원 (db.User → _models.get('User'))
|
|
16
|
+
return new Proxy(this, {
|
|
17
|
+
get(target, prop) {
|
|
18
|
+
// 내부 메서드/프로퍼티는 직접 접근
|
|
19
|
+
if (prop in target || typeof prop === 'symbol') {
|
|
20
|
+
return target[prop];
|
|
21
|
+
}
|
|
22
|
+
// 모델 이름으로 접근
|
|
23
|
+
if (target._models.has(prop)) {
|
|
24
|
+
return target._models.get(prop);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 모델 등록
|
|
33
|
+
* @param {string} name - 모델 이름 (e.g. 'User')
|
|
34
|
+
* @param {typeof import('./Model.js').default} ModelClass
|
|
35
|
+
*/
|
|
36
|
+
register(name, ModelClass) {
|
|
37
|
+
this._models.set(name, ModelClass);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 모델 조회
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @returns {typeof import('./Model.js').default}
|
|
44
|
+
*/
|
|
45
|
+
get(name) {
|
|
46
|
+
return this._models.get(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 등록된 모델 이름 목록
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
names() {
|
|
54
|
+
return [...this._models.keys()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Knex 연결 설정 (SQL)
|
|
59
|
+
* @param {object} knex
|
|
60
|
+
*/
|
|
61
|
+
setKnex(knex) {
|
|
62
|
+
this._knex = knex;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* MongoDB 연결 설정
|
|
67
|
+
* @param {object} connections
|
|
68
|
+
*/
|
|
69
|
+
setMongoConnections(connections) {
|
|
70
|
+
this._mongoConnections = connections;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoModel — MongoDB 모델 서브클래스
|
|
3
|
+
*
|
|
4
|
+
* Mongoose 기반. SqlModel과 별도 계층.
|
|
5
|
+
* MongoDB 전용 API (aggregate, populate 등)를 별도 구현.
|
|
6
|
+
*
|
|
7
|
+
* @see docs/framework/02-database-orm.md
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { MongoModel } from '@fuzionx/framework';
|
|
11
|
+
*
|
|
12
|
+
* export default class AccessLog extends MongoModel {
|
|
13
|
+
* static table = 'access_logs';
|
|
14
|
+
* static connection = 'mongo';
|
|
15
|
+
* static timestamps = true;
|
|
16
|
+
* static schema = {
|
|
17
|
+
* level: { type: 'string', required: true },
|
|
18
|
+
* message: { type: 'string' },
|
|
19
|
+
* meta: { type: 'object' },
|
|
20
|
+
* };
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
import Model from './Model.js';
|
|
24
|
+
import QueryBuilder from './QueryBuilder.js';
|
|
25
|
+
|
|
26
|
+
export default class MongoModel extends Model {
|
|
27
|
+
static driver = 'mongodb';
|
|
28
|
+
static connection = 'mongo';
|
|
29
|
+
|
|
30
|
+
/** @type {import('./ConnectionManager.js').default|null} */
|
|
31
|
+
static _connectionManager = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ConnectionManager를 MongoModel에 주입
|
|
35
|
+
* @param {import('./ConnectionManager.js').default} cm
|
|
36
|
+
*/
|
|
37
|
+
static setConnectionManager(cm) {
|
|
38
|
+
MongoModel._connectionManager = cm;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DB 연결 가져오기
|
|
43
|
+
* @returns {object}
|
|
44
|
+
*/
|
|
45
|
+
static getConnection() {
|
|
46
|
+
if (!MongoModel._connectionManager) {
|
|
47
|
+
throw new Error('DB not initialized. Ensure Application.boot() has been called.');
|
|
48
|
+
}
|
|
49
|
+
return MongoModel._connectionManager.get(this.connection);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mongoose 모델 가져오기 (lazy 생성)
|
|
54
|
+
* @returns {import('mongoose').Model}
|
|
55
|
+
*/
|
|
56
|
+
static _getMongooseModel() {
|
|
57
|
+
if (this._mongooseModel) return this._mongooseModel;
|
|
58
|
+
|
|
59
|
+
const conn = this.getConnection();
|
|
60
|
+
if (!conn.mongoose) {
|
|
61
|
+
throw new Error('MongoDB requires mongoose. Install: npm install mongoose');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// schema 정의 → Mongoose 모델 생성
|
|
65
|
+
const schemaFields = {};
|
|
66
|
+
for (const [key, def] of Object.entries(this.schema || {})) {
|
|
67
|
+
const mongoType = this._toMongooseType(def.type);
|
|
68
|
+
schemaFields[key] = {
|
|
69
|
+
type: mongoType,
|
|
70
|
+
required: def.required || false,
|
|
71
|
+
default: def.default,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const options = { collection: this.table };
|
|
76
|
+
if (this.timestamps) {
|
|
77
|
+
options.timestamps = { createdAt: 'created_at', updatedAt: 'updated_at' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const schema = new conn.mongoose.Schema(schemaFields, options);
|
|
81
|
+
this._mongooseModel = conn.mongoose.model(this.name, schema);
|
|
82
|
+
return this._mongooseModel;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* FuzionX 타입 → Mongoose 타입 변환
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
static _toMongooseType(type) {
|
|
90
|
+
const mongoose = this.getConnection().mongoose;
|
|
91
|
+
if (!mongoose) return String;
|
|
92
|
+
|
|
93
|
+
const map = {
|
|
94
|
+
'string': String,
|
|
95
|
+
'integer': Number,
|
|
96
|
+
'bigInteger': Number,
|
|
97
|
+
'float': Number,
|
|
98
|
+
'decimal': Number,
|
|
99
|
+
'boolean': Boolean,
|
|
100
|
+
'date': Date,
|
|
101
|
+
'datetime': Date,
|
|
102
|
+
'timestamp': Date,
|
|
103
|
+
'object': Object,
|
|
104
|
+
'json': Object,
|
|
105
|
+
'array': Array,
|
|
106
|
+
'binary': Buffer,
|
|
107
|
+
};
|
|
108
|
+
return map[type] || String;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 레코드 생성
|
|
113
|
+
* @param {object} data
|
|
114
|
+
* @returns {Promise<MongoModel>}
|
|
115
|
+
*/
|
|
116
|
+
static async create(data) {
|
|
117
|
+
try {
|
|
118
|
+
const MongooseModel = this._getMongooseModel();
|
|
119
|
+
const doc = await MongooseModel.create(data);
|
|
120
|
+
return new this(doc.toObject());
|
|
121
|
+
} catch {
|
|
122
|
+
// Mongoose 미설치 → stub
|
|
123
|
+
return new this({ ...data, _id: Date.now().toString(36) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* ID로 조회
|
|
129
|
+
* @param {*} id
|
|
130
|
+
* @returns {Promise<MongoModel|null>}
|
|
131
|
+
*/
|
|
132
|
+
static async find(id) {
|
|
133
|
+
try {
|
|
134
|
+
const MongooseModel = this._getMongooseModel();
|
|
135
|
+
const doc = await MongooseModel.findById(id);
|
|
136
|
+
return doc ? new this(doc.toObject()) : null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 전체 조회
|
|
144
|
+
* @returns {Promise<Array>}
|
|
145
|
+
*/
|
|
146
|
+
static async all() {
|
|
147
|
+
try {
|
|
148
|
+
const MongooseModel = this._getMongooseModel();
|
|
149
|
+
const docs = await MongooseModel.find({});
|
|
150
|
+
return docs.map(d => new this(d.toObject()));
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async findAll() { return this.all(); }
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Raw aggregation pipeline
|
|
160
|
+
* @param {Array} pipeline
|
|
161
|
+
* @returns {Promise<Array>}
|
|
162
|
+
*/
|
|
163
|
+
static async raw(pipeline) {
|
|
164
|
+
try {
|
|
165
|
+
const MongooseModel = this._getMongooseModel();
|
|
166
|
+
return MongooseModel.aggregate(pipeline);
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 네이티브 Mongoose 접근
|
|
174
|
+
* @returns {object|null}
|
|
175
|
+
*/
|
|
176
|
+
static nativeConnection() {
|
|
177
|
+
try {
|
|
178
|
+
return this.getConnection().mongoose;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
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
|
+
const id = this._attributes._id || this._attributes[this.constructor.primaryKey];
|
|
196
|
+
if (!id) return this;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const MongooseModel = this.constructor._getMongooseModel();
|
|
200
|
+
await MongooseModel.updateOne({ _id: id }, { $set: data });
|
|
201
|
+
} catch {}
|
|
202
|
+
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 삭제
|
|
208
|
+
*/
|
|
209
|
+
async delete() {
|
|
210
|
+
if (this.constructor.softDelete) {
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
this._attributes.deleted_at = now;
|
|
213
|
+
this.deleted_at = now;
|
|
214
|
+
await this.update({ deleted_at: now });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await this.forceDelete();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 실제 삭제
|
|
222
|
+
*/
|
|
223
|
+
async forceDelete() {
|
|
224
|
+
const id = this._attributes._id || this._attributes[this.constructor.primaryKey];
|
|
225
|
+
if (!id) return;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const MongooseModel = this.constructor._getMongooseModel();
|
|
229
|
+
await MongooseModel.deleteOne({ _id: id });
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination — 페이지네이션 결과 객체
|
|
3
|
+
*
|
|
4
|
+
* @see docs/framework/class-design.mm.md (Pagination)
|
|
5
|
+
*/
|
|
6
|
+
export default class Pagination {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Array} data - 현재 페이지 데이터
|
|
9
|
+
* @param {number} total - 전체 레코드 수
|
|
10
|
+
* @param {number} page - 현재 페이지
|
|
11
|
+
* @param {number} perPage - 페이지당 수
|
|
12
|
+
*/
|
|
13
|
+
constructor(data, total, page, perPage) {
|
|
14
|
+
this.data = data;
|
|
15
|
+
this.total = total;
|
|
16
|
+
this.page = page;
|
|
17
|
+
this.perPage = perPage;
|
|
18
|
+
this.lastPage = Math.ceil(total / perPage) || 1;
|
|
19
|
+
this.hasMore = page < this.lastPage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* JSON 직렬화 형태
|
|
24
|
+
*/
|
|
25
|
+
toJSON() {
|
|
26
|
+
return {
|
|
27
|
+
data: this.data,
|
|
28
|
+
meta: {
|
|
29
|
+
total: this.total,
|
|
30
|
+
page: this.page,
|
|
31
|
+
perPage: this.perPage,
|
|
32
|
+
lastPage: this.lastPage,
|
|
33
|
+
hasMore: this.hasMore,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreModel — PostgreSQL 모델 서브클래스
|
|
3
|
+
*
|
|
4
|
+
* Knex + pg 기반.
|
|
5
|
+
* SqlModel의 공통 SQL 로직을 상속하며,
|
|
6
|
+
* PostgreSQL 고유 기능(JSONB, ARRAY, CTE 등)은 raw()로 접근.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/framework/02-database-orm.md
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { PostgreModel } from '@fuzionx/framework';
|
|
12
|
+
*
|
|
13
|
+
* export default class Analytics extends PostgreModel {
|
|
14
|
+
* static table = 'analytics';
|
|
15
|
+
* static connection = 'analytics_db';
|
|
16
|
+
* static columns = {
|
|
17
|
+
* id: { type: 'increments' },
|
|
18
|
+
* event: { type: 'string', length: 100 },
|
|
19
|
+
* meta: { type: 'json' },
|
|
20
|
+
* created: { type: 'timestamp' },
|
|
21
|
+
* };
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
import SqlModel from './SqlModel.js';
|
|
25
|
+
|
|
26
|
+
export default class PostgreModel extends SqlModel {
|
|
27
|
+
static driver = 'postgres';
|
|
28
|
+
static connection = 'main';
|
|
29
|
+
}
|