@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.
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@fuzionx/player": "^0.1.77",
12
+ "@fuzionx/player": "^0.1.78",
13
13
  "pinia": "^3.0.4",
14
14
  "vue": "^3.5.0",
15
15
  "vue-router": "^4.5.0"
@@ -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
- 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
- }
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
- 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
- }
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
- 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
- }
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
- try {
165
- const MongooseModel = this._getMongooseModel();
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
- try {
199
- const MongooseModel = this.constructor._getMongooseModel();
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
- try {
228
- const MongooseModel = this.constructor._getMongooseModel();
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
- results = [];
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
- return 0;
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
- return 0;
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
- return 0;
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 { await fs.access(filePath); return true; } catch { return false; }
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
- try { await task.failed(job.data, err); } catch {}
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
- try { await task.failed(job.data, err); } catch {}
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
  }
@@ -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.77",
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.77",
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",