@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.
- package/.claude/rules/configurable-features.md +1 -1
- package/FRAMEWORK-API.md +3 -1
- package/bin/migrate.js +13 -3
- package/dist/core/common/helpers/file.helper.d.ts +14 -2
- package/dist/core/common/helpers/file.helper.js +48 -9
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js +2 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js +68 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +4 -1
- package/dist/core/modules/file/core-file.controller.js +39 -6
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
- package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
- package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +1 -0
- package/dist/core/modules/migrate/helpers/migration.helper.js +51 -4
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/security-overrides.md +9 -2
- package/migration-guides/11.32.2-to-11.32.3.md +129 -0
- package/migration-guides/11.32.3-to-11.32.4.md +323 -0
- package/package.json +1 -1
- package/src/core/common/helpers/file.helper.spec.ts +145 -0
- package/src/core/common/helpers/file.helper.ts +148 -10
- package/src/core/common/interfaces/server-options.interface.ts +23 -0
- package/src/core/modules/ai/README.md +6 -0
- package/src/core/modules/ai/inputs/core-ai-connection.input.ts +2 -0
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +18 -3
- package/src/core/modules/ai/services/core-ai-connection.service.ts +135 -0
- package/src/core/modules/file/README.md +59 -0
- package/src/core/modules/file/core-file.controller.spec.ts +164 -0
- package/src/core/modules/file/core-file.controller.ts +100 -8
- package/src/core/modules/migrate/README.md +35 -0
- package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
- package/src/core/modules/migrate/helpers/migration.helper.spec.ts +85 -0
- 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 -
|
|
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
|
|
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
|
-
|
|
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
|
};
|