@lenne.tech/nest-server 11.32.2 → 11.32.4

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 (40) hide show
  1. package/.claude/rules/configurable-features.md +1 -1
  2. package/FRAMEWORK-API.md +3 -1
  3. package/bin/migrate.js +13 -3
  4. package/dist/core/common/helpers/file.helper.d.ts +14 -2
  5. package/dist/core/common/helpers/file.helper.js +48 -9
  6. package/dist/core/common/helpers/file.helper.js.map +1 -1
  7. package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
  8. package/dist/core/modules/ai/inputs/core-ai-connection.input.js +2 -0
  9. package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -1
  10. package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -0
  11. package/dist/core/modules/ai/services/core-ai-connection.service.js +68 -0
  12. package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
  13. package/dist/core/modules/file/core-file.controller.d.ts +4 -1
  14. package/dist/core/modules/file/core-file.controller.js +39 -6
  15. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  16. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
  17. package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
  18. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  19. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +1 -0
  20. package/dist/core/modules/migrate/helpers/migration.helper.js +51 -4
  21. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  22. package/dist/tsconfig.build.tsbuildinfo +1 -1
  23. package/docs/security-overrides.md +9 -2
  24. package/migration-guides/11.32.2-to-11.32.3.md +129 -0
  25. package/migration-guides/11.32.3-to-11.32.4.md +323 -0
  26. package/package.json +1 -1
  27. package/src/core/common/helpers/file.helper.spec.ts +145 -0
  28. package/src/core/common/helpers/file.helper.ts +148 -10
  29. package/src/core/common/interfaces/server-options.interface.ts +23 -0
  30. package/src/core/modules/ai/README.md +6 -0
  31. package/src/core/modules/ai/inputs/core-ai-connection.input.ts +2 -0
  32. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +18 -3
  33. package/src/core/modules/ai/services/core-ai-connection.service.ts +135 -0
  34. package/src/core/modules/file/README.md +59 -0
  35. package/src/core/modules/file/core-file.controller.spec.ts +164 -0
  36. package/src/core/modules/file/core-file.controller.ts +100 -8
  37. package/src/core/modules/migrate/README.md +35 -0
  38. package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
  39. package/src/core/modules/migrate/helpers/migration.helper.spec.ts +85 -0
  40. package/src/core/modules/migrate/helpers/migration.helper.ts +131 -4
@@ -67,13 +67,76 @@ export const getDb = async (mongoUrl: string): Promise<Db> => {
67
67
  return client.db();
68
68
  };
69
69
 
70
+ /**
71
+ * Throw unless every chunk of a stored GridFS file is present.
72
+ *
73
+ * A GridFS upload is not one write but many: N chunk documents plus the files
74
+ * document that describes them. The write stream's `'finish'` event says the
75
+ * stream ended — it does not prove every chunk is durably there, and a
76
+ * connection that goes away at the wrong moment can leave a files document
77
+ * behind that promises more bytes than exist. The upload then "succeeds", the
78
+ * caller stores the id, and the defect only surfaces much later as a broken
79
+ * download from a record that looks perfectly healthy.
80
+ *
81
+ * Counts documents rather than reading bytes, so a chunk that was written but
82
+ * truncated still passes. Catching that would mean streaming the whole file
83
+ * back on every upload; the cheap count catches the failure that actually
84
+ * occurs (a missing chunk) and is index-only via the GridFS default index.
85
+ *
86
+ * @param db - Database holding the bucket
87
+ * @param bucketName - GridFS bucket name, i.e. the prefix of `<bucket>.files` / `<bucket>.chunks`
88
+ * @param id - `_id` of the files document
89
+ * @param label - Optional human-readable name for the error message; defaults to the id
90
+ * @throws Error if the files document is missing or fewer chunks are stored than its length implies
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * await assertGridFsFileComplete(db, 'images', fileId, 'logo.png');
95
+ * ```
96
+ */
97
+ export const assertGridFsFileComplete = async (
98
+ db: Db,
99
+ bucketName: string,
100
+ id: ObjectId,
101
+ label?: string,
102
+ ): Promise<void> => {
103
+ const name = label || String(id);
104
+ const fileDoc = await db.collection(`${bucketName}.files`).findOne({ _id: id });
105
+ if (!fileDoc) {
106
+ throw new Error(`GridFS file '${name}' has no file document (id ${String(id)})`);
107
+ }
108
+
109
+ const chunkSize: number = fileDoc.chunkSize || 255 * 1024;
110
+ // NO `Math.max(1, …)` floor here: GridFS stores ZERO chunk documents for a
111
+ // zero-byte file — the driver's `writeRemnant()` returns early on `pos === 0`
112
+ // rather than inserting an empty chunk. A floor of 1 would reject every
113
+ // legitimately empty asset, and because the container entrypoint defaults to
114
+ // `MIGRATE_FAILURE_POLICY=abort`, that failure would keep the server from
115
+ // starting at all.
116
+ const expected = Math.ceil((fileDoc.length || 0) / chunkSize);
117
+ const actual = await db
118
+ .collection(`${bucketName}.chunks`)
119
+ // Read from the primary: this is a read-your-own-write, and a URI carrying
120
+ // `readPreference=secondaryPreferred` would otherwise fail a healthy upload
121
+ // against a secondary that has not caught up yet.
122
+ .countDocuments({ files_id: id }, { readPreference: 'primary' });
123
+ if (actual < expected) {
124
+ throw new Error(`GridFS file '${name}' is incomplete: ${actual} of ${expected} chunks stored (id ${String(id)})`);
125
+ }
126
+ };
127
+
70
128
  /**
71
129
  * Upload file to GridFS
72
130
  *
131
+ * Resolves only once the upload is verified complete (see
132
+ * {@link assertGridFsFileComplete}); rejects — rather than hanging — when the
133
+ * source file cannot be read. Closes its own connection either way.
134
+ *
73
135
  * @param mongoUrl - MongoDB connection URI
74
- * @param relativePath - Relative path to the file
136
+ * @param relativePath - Path to the file, resolved against this module's directory
75
137
  * @param options - Optional bucket name and filename
76
138
  * @returns Promise with ObjectId of uploaded file
139
+ * @throws Error if the source cannot be read, or if the stored file is incomplete
77
140
  *
78
141
  * @example
79
142
  * ```typescript
@@ -100,19 +163,83 @@ export const uploadFileToGridFS = async (
100
163
  };
101
164
 
102
165
  const client = await MongoClient.connect(mongoUrl);
166
+ // Registered unconditionally — unlike `getDb()`, which only registers inside a
167
+ // migration context. This client is always ours to close, so `_endMigration()`
168
+ // stays a backstop for the case where the upload throws before `settle()` runs.
169
+ activeConnections.add(client);
170
+
103
171
  const db = client.db();
104
172
  const bucket = new GridFSBucket(db, { bucketName });
105
173
  const writeStream = bucket.openUploadStream(filename);
106
174
 
107
- const rs = fs.createReadStream(path.resolve(__dirname, relativePath)).pipe(writeStream);
175
+ const readStream = fs.createReadStream(path.resolve(__dirname, relativePath));
176
+ const rs = readStream.pipe(writeStream);
177
+
178
+ /**
179
+ * Read errors need their own handler — `pipe()` does not forward them.
180
+ *
181
+ * An unreadable source (missing file, wrong path inside a container image)
182
+ * emits on the READ stream, where nothing was listening: the write stream
183
+ * never finished, the promise below never settled, and the migration hung
184
+ * until something else timed out. Destroying the write stream routes it into
185
+ * the rejection path so the caller sees the actual cause.
186
+ */
187
+ readStream.on('error', (err) => {
188
+ writeStream.destroy(err);
189
+ });
190
+
191
+ /**
192
+ * Close the connection before settling.
193
+ *
194
+ * This client used to be opened and never closed, and it was not registered
195
+ * either — so `_endMigration()` could not reach it. Every uploaded file left a
196
+ * live connection behind, and a live connection keeps an SDAM monitor timer
197
+ * alive, which keeps the Node event loop busy: `migrate up` finished its work,
198
+ * printed "All migrations completed successfully" and then never exited. That
199
+ * is invisible on a developer machine and blocks a CI job until its timeout.
200
+ *
201
+ * A failing `close()` must never become the error the caller sees: the reason
202
+ * the upload ended is what matters, and the settle path is also reached while
203
+ * rejecting. So the close error is logged and swallowed, and `settled` keeps a
204
+ * second call (success path falling into `.catch`) from closing twice.
205
+ */
206
+ let settled = false;
207
+ const settle = async <T>(action: () => T): Promise<T> => {
208
+ if (!settled) {
209
+ settled = true;
210
+ try {
211
+ await client.close();
212
+ } catch (closeErr) {
213
+ console.warn('Failed to close migration connection:', closeErr);
214
+ } finally {
215
+ activeConnections.delete(client);
216
+ }
217
+ }
218
+ return action();
219
+ };
108
220
 
109
221
  return new Promise<ObjectId>((resolve, reject) => {
110
222
  rs.on('finish', () => {
111
- resolve(writeStream.id as ObjectId);
223
+ const id = writeStream.id as ObjectId;
224
+ // Verify BEFORE closing: the check needs the same open client, and a
225
+ // failure has to reject rather than hand back an id that points at nothing.
226
+ assertGridFsFileComplete(db, bucketName, id, filename)
227
+ .then(() => settle(() => id))
228
+ .then(resolve)
229
+ .catch((err: unknown) => {
230
+ // Drop the incomplete file, otherwise every re-run of the migration
231
+ // leaves another orphaned files document (plus its partial chunks)
232
+ // behind, since a retry uploads under a fresh ObjectId.
233
+ bucket
234
+ .delete(id)
235
+ .catch(() => undefined)
236
+ .then(() => settle(() => undefined))
237
+ .finally(() => reject(err));
238
+ });
112
239
  });
113
240
 
114
241
  rs.on('error', (err) => {
115
- reject(err);
242
+ settle(() => undefined).finally(() => reject(err));
116
243
  });
117
244
  });
118
245
  };