@fuzionx/framework 0.1.77 → 0.1.78
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/cli/templates/make/app-spa/views/default/spa/package.json +1 -1
- package/lib/database/MongoModel.js +39 -38
- package/lib/database/SqlQueryBuilder.js +25 -4
- package/lib/helpers/FileHelper.js +17 -1
- package/lib/schedule/drivers/MemoryQueueDriver.js +10 -1
- package/lib/schedule/drivers/RedisQueueDriver.js +10 -1
- package/lib/services/Storage.js +12 -6
- package/package.json +2 -2
|
@@ -110,63 +110,60 @@ export default class MongoModel extends Model {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* 레코드 생성
|
|
113
|
+
*
|
|
114
|
+
* 실패 시 예외 throw — silent stub 반환 X.
|
|
115
|
+
* 응용이 "INSERT 성공한 줄 알고" 진행하는 디버깅 지옥 차단.
|
|
113
116
|
* @param {object} data
|
|
114
117
|
* @returns {Promise<MongoModel>}
|
|
118
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 검증 실패 / 키 충돌 등
|
|
115
119
|
*/
|
|
116
120
|
static async create(data) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return new this(doc.toObject());
|
|
121
|
-
} catch {
|
|
122
|
-
// Mongoose 미설치 → stub
|
|
123
|
-
return new this({ ...data, _id: Date.now().toString(36) });
|
|
124
|
-
}
|
|
121
|
+
const MongooseModel = this._getMongooseModel();
|
|
122
|
+
const doc = await MongooseModel.create(data);
|
|
123
|
+
return new this(doc.toObject());
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
/**
|
|
128
127
|
* ID로 조회
|
|
128
|
+
*
|
|
129
|
+
* 레코드가 실제로 없으면 null 반환. 연결/쿼리 오류는 throw — silent null 반환 X.
|
|
130
|
+
* "USER_NOT_FOUND" 가 실제로 미존재인지 연결 장애인지 구분 가능.
|
|
129
131
|
* @param {*} id
|
|
130
132
|
* @returns {Promise<MongoModel|null>}
|
|
133
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 쿼리 오류 등
|
|
131
134
|
*/
|
|
132
135
|
static async find(id) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return doc ? new this(doc.toObject()) : null;
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
136
|
+
const MongooseModel = this._getMongooseModel();
|
|
137
|
+
const doc = await MongooseModel.findById(id);
|
|
138
|
+
return doc ? new this(doc.toObject()) : null;
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
/**
|
|
143
142
|
* 전체 조회
|
|
143
|
+
*
|
|
144
|
+
* 결과 없으면 빈 배열 반환. 연결/쿼리 오류는 throw — silent [] 반환 X.
|
|
144
145
|
* @returns {Promise<Array>}
|
|
146
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 쿼리 오류 등
|
|
145
147
|
*/
|
|
146
148
|
static async all() {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return docs.map(d => new this(d.toObject()));
|
|
151
|
-
} catch {
|
|
152
|
-
return [];
|
|
153
|
-
}
|
|
149
|
+
const MongooseModel = this._getMongooseModel();
|
|
150
|
+
const docs = await MongooseModel.find({});
|
|
151
|
+
return docs.map(d => new this(d.toObject()));
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
static async findAll() { return this.all(); }
|
|
157
155
|
|
|
158
156
|
/**
|
|
159
157
|
* Raw aggregation pipeline
|
|
158
|
+
*
|
|
159
|
+
* 파이프라인 실행 결과 반환. 오류 시 throw — silent [] 반환 X.
|
|
160
160
|
* @param {Array} pipeline
|
|
161
161
|
* @returns {Promise<Array>}
|
|
162
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 파이프라인 오류 등
|
|
162
163
|
*/
|
|
163
164
|
static async raw(pipeline) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return MongooseModel.aggregate(pipeline);
|
|
167
|
-
} catch {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
165
|
+
const MongooseModel = this._getMongooseModel();
|
|
166
|
+
return MongooseModel.aggregate(pipeline);
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
/**
|
|
@@ -185,8 +182,13 @@ export default class MongoModel extends Model {
|
|
|
185
182
|
|
|
186
183
|
/**
|
|
187
184
|
* 인스턴스 수정
|
|
185
|
+
*
|
|
186
|
+
* `_id` 가 없으면 no-op (메모리만 갱신). DB 수정 실패 시 throw —
|
|
187
|
+
* silent swallow 안 함. 응용이 "DB 저장됐다고 착각하는" 케이스 차단.
|
|
188
|
+
*
|
|
188
189
|
* @param {object} data
|
|
189
190
|
* @returns {Promise<this>}
|
|
191
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 검증 실패 등
|
|
190
192
|
*/
|
|
191
193
|
async update(data) {
|
|
192
194
|
Object.assign(this._attributes, data);
|
|
@@ -195,16 +197,13 @@ export default class MongoModel extends Model {
|
|
|
195
197
|
const id = this._attributes._id || this._attributes[this.constructor.primaryKey];
|
|
196
198
|
if (!id) return this;
|
|
197
199
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await MongooseModel.updateOne({ _id: id }, { $set: data });
|
|
201
|
-
} catch {}
|
|
202
|
-
|
|
200
|
+
const MongooseModel = this.constructor._getMongooseModel();
|
|
201
|
+
await MongooseModel.updateOne({ _id: id }, { $set: data });
|
|
203
202
|
return this;
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/**
|
|
207
|
-
* 삭제
|
|
206
|
+
* 삭제 (soft delete 지원)
|
|
208
207
|
*/
|
|
209
208
|
async delete() {
|
|
210
209
|
if (this.constructor.softDelete) {
|
|
@@ -219,14 +218,16 @@ export default class MongoModel extends Model {
|
|
|
219
218
|
|
|
220
219
|
/**
|
|
221
220
|
* 실제 삭제
|
|
221
|
+
*
|
|
222
|
+
* `_id` 없으면 no-op. DB 삭제 오류 시 throw — silent swallow X.
|
|
223
|
+
*
|
|
224
|
+
* @throws {Error} mongoose 미설치 / 연결 실패 / 권한 오류 등
|
|
222
225
|
*/
|
|
223
226
|
async forceDelete() {
|
|
224
227
|
const id = this._attributes._id || this._attributes[this.constructor.primaryKey];
|
|
225
228
|
if (!id) return;
|
|
226
229
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await MongooseModel.deleteOne({ _id: id });
|
|
230
|
-
} catch {}
|
|
230
|
+
const MongooseModel = this.constructor._getMongooseModel();
|
|
231
|
+
await MongooseModel.deleteOne({ _id: id });
|
|
231
232
|
}
|
|
232
233
|
}
|
|
@@ -29,7 +29,12 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* 결과 조회 — SQL SELECT 실행
|
|
32
|
+
*
|
|
33
|
+
* 지원 driver (sqlite/knex) 매칭 안 되거나 conn.db 비어 있으면 throw —
|
|
34
|
+
* silent [] 반환 X. driver 미지원/연결 미수립을 빈 결과로 착각하지 않게.
|
|
35
|
+
*
|
|
32
36
|
* @returns {Promise<Array<import('./Model.js').default>>}
|
|
37
|
+
* @throws {Error} 지원 driver 없음 / 연결 미수립
|
|
33
38
|
*/
|
|
34
39
|
async get() {
|
|
35
40
|
const conn = this._model.getConnection();
|
|
@@ -40,7 +45,9 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
40
45
|
} else if (conn.type === 'knex' && conn.db) {
|
|
41
46
|
results = await this._executeKnex(conn.db, 'select');
|
|
42
47
|
} else {
|
|
43
|
-
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[SqlQueryBuilder.get] '${this._model.table}' — unsupported driver or no connection (conn.type=${conn?.type}, conn.db=${!!conn?.db})`,
|
|
50
|
+
);
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
// eager-loading — with()로 지정된 관계 로드
|
|
@@ -63,7 +70,11 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* 카운트
|
|
73
|
+
*
|
|
74
|
+
* 지원 driver 매칭 안 되면 throw — silent 0 반환 X.
|
|
75
|
+
*
|
|
66
76
|
* @returns {Promise<number>}
|
|
77
|
+
* @throws {Error} 지원 driver 없음 / 연결 미수립
|
|
67
78
|
*/
|
|
68
79
|
async count() {
|
|
69
80
|
const conn = this._model.getConnection();
|
|
@@ -83,7 +94,9 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
83
94
|
return Number(cnt);
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[SqlQueryBuilder.count] '${this._model.table}' — unsupported driver or no connection (conn.type=${conn?.type}, conn.db=${!!conn?.db})`,
|
|
99
|
+
);
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
/**
|
|
@@ -115,12 +128,18 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
115
128
|
return query.update(data);
|
|
116
129
|
}
|
|
117
130
|
|
|
118
|
-
|
|
131
|
+
throw new Error(
|
|
132
|
+
`[SqlQueryBuilder.update] '${this._model.table}' — unsupported driver or no connection (conn.type=${conn?.type}, conn.db=${!!conn?.db})`,
|
|
133
|
+
);
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
/**
|
|
122
137
|
* 조건부 삭제
|
|
138
|
+
*
|
|
139
|
+
* 지원 driver 매칭 안 되면 throw — silent 0 반환 X.
|
|
140
|
+
*
|
|
123
141
|
* @returns {Promise<number>}
|
|
142
|
+
* @throws {Error} 지원 driver 없음 / 연결 미수립
|
|
124
143
|
*/
|
|
125
144
|
async delete() {
|
|
126
145
|
const conn = this._model.getConnection();
|
|
@@ -148,7 +167,9 @@ export default class SqlQueryBuilder extends QueryBuilder {
|
|
|
148
167
|
return query.delete();
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
|
|
170
|
+
throw new Error(
|
|
171
|
+
`[SqlQueryBuilder.delete] '${this._model.table}' — unsupported driver or no connection (conn.type=${conn?.type}, conn.db=${!!conn?.db})`,
|
|
172
|
+
);
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
/**
|
|
@@ -39,9 +39,25 @@ export default class FileHelper {
|
|
|
39
39
|
return stat.size;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 파일 존재 여부.
|
|
44
|
+
*
|
|
45
|
+
* ENOENT (파일 없음) 만 false 반환. 권한 오류(EACCES) / 심볼 무한 루프(ELOOP) /
|
|
46
|
+
* IO 오류 등은 throw — silent false 로 가리지 않음.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} filePath
|
|
49
|
+
* @returns {Promise<boolean>}
|
|
50
|
+
* @throws {Error} ENOENT 외의 fs 오류
|
|
51
|
+
*/
|
|
42
52
|
async exists(filePath) {
|
|
43
53
|
if (this._bridge?.fileExists) return this._bridge.fileExists(filePath);
|
|
44
|
-
try {
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(filePath);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err && err.code === 'ENOENT') return false;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
async remove(filePath) {
|
|
@@ -104,7 +104,16 @@ export default class MemoryQueueDriver {
|
|
|
104
104
|
if (!this._processing) this._process();
|
|
105
105
|
}, delay);
|
|
106
106
|
} else {
|
|
107
|
-
|
|
107
|
+
// Task.failed 콜백 자체가 실패해도 워커 루프는 멈추면 안 됨 (fire-and-forget).
|
|
108
|
+
// 단 silent 하지 않게 logger.warn 으로 노출 — 디버깅 가능.
|
|
109
|
+
try { await task.failed(job.data, err); }
|
|
110
|
+
catch (failedErr) {
|
|
111
|
+
const log = this.app?.logger || console;
|
|
112
|
+
log.warn?.(
|
|
113
|
+
`[MemoryQueueDriver] ${TaskClass.name || 'Task'}.failed() callback threw — ` +
|
|
114
|
+
`original error: ${err?.message || err}; failed-callback error: ${failedErr?.message || failedErr}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
}
|
|
@@ -191,7 +191,16 @@ export default class RedisQueueDriver {
|
|
|
191
191
|
job.dead_at = Date.now();
|
|
192
192
|
await this._client.lpush(deadKey, JSON.stringify(job))
|
|
193
193
|
.catch(e => this.app?.logger?.error?.(`[Queue:Redis] dead push failed: ${e.message}`));
|
|
194
|
-
|
|
194
|
+
// Task.failed 콜백 자체가 실패해도 워커 루프 유지 (fire-and-forget),
|
|
195
|
+
// 단 silent 하지 않게 logger.warn 으로 노출.
|
|
196
|
+
try { await task.failed(job.data, err); }
|
|
197
|
+
catch (failedErr) {
|
|
198
|
+
const log = this.app?.logger || console;
|
|
199
|
+
log.warn?.(
|
|
200
|
+
`[RedisQueueDriver] ${TaskClass.name || 'Task'}.failed() callback threw — ` +
|
|
201
|
+
`original error: ${err?.message || err}; failed-callback error: ${failedErr?.message || failedErr}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
}
|
package/lib/services/Storage.js
CHANGED
|
@@ -117,19 +117,25 @@ export default class Storage {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* 파일 존재
|
|
120
|
+
* 파일 존재 여부.
|
|
121
|
+
*
|
|
122
|
+
* ENOENT (파일 없음) 만 false 반환. 권한 오류 / IO 오류 / S3 인증 오류 등은
|
|
123
|
+
* throw — silent false 로 가리지 않음.
|
|
124
|
+
*
|
|
121
125
|
* @param {string} filePath
|
|
122
126
|
* @returns {Promise<boolean>}
|
|
127
|
+
* @throws {Error} ENOENT 외의 fs/S3 오류
|
|
123
128
|
*/
|
|
124
129
|
async exists(filePath) {
|
|
130
|
+
if (this.driver === 's3') {
|
|
131
|
+
return this._s3Exists(filePath);
|
|
132
|
+
}
|
|
125
133
|
try {
|
|
126
|
-
if (this.driver === 's3') {
|
|
127
|
-
return this._s3Exists(filePath);
|
|
128
|
-
}
|
|
129
134
|
await fs.access(path.join(this.basePath, filePath));
|
|
130
135
|
return true;
|
|
131
|
-
} catch {
|
|
132
|
-
return false;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err && err.code === 'ENOENT') return false;
|
|
138
|
+
throw err;
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
6
|
"main": "index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@aws-sdk/client-s3": "^3.1028.0",
|
|
38
|
-
"@fuzionx/core": "^0.1.
|
|
38
|
+
"@fuzionx/core": "^0.1.78",
|
|
39
39
|
"better-sqlite3": "^12.8.0",
|
|
40
40
|
"knex": "^3.2.5",
|
|
41
41
|
"mongoose": "^9.3.2",
|