@lenne.tech/nest-server 11.33.1 → 11.34.0
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/testing.md +181 -3
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/common/helpers/gridfs.helper.d.ts +1 -0
- package/dist/core/common/helpers/gridfs.helper.js +43 -5
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/services/core-s3.service.d.ts +4 -0
- package/dist/core/common/services/core-s3.service.js +28 -1
- package/dist/core/common/services/core-s3.service.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +4 -2
- package/dist/core/modules/file/core-file.service.js +46 -47
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +12 -1
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-action-messages.d.ts +1 -0
- package/dist/core/modules/hub/hub-action-messages.js +1 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -1
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +9 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +2 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -1
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +10 -2
- package/dist/core/modules/hub/services/core-hub-db.service.js +85 -29
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +2 -1
- package/dist/server/modules/file/file.service.js +2 -2
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.23.x-to-11.24.0.md +1 -1
- package/migration-guides/11.32.x-to-11.33.x.md +2 -0
- package/migration-guides/11.33.x-to-11.34.x.md +394 -0
- package/package.json +3 -1
- package/src/core/common/helpers/gridfs.helper.ts +92 -7
- package/src/core/common/services/core-s3.service.ts +71 -9
- package/src/core/modules/file/README.md +42 -6
- package/src/core/modules/file/core-file.service.ts +131 -59
- package/src/core/modules/hub/README.md +35 -9
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +12 -1
- package/src/core/modules/hub/hub-action-messages.ts +9 -1
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +31 -1
- package/src/core/modules/hub/services/core-hub-actions.service.ts +10 -2
- package/src/core/modules/hub/services/core-hub-db.service.ts +155 -31
- package/src/server/modules/file/file.service.ts +20 -3
|
@@ -3,11 +3,26 @@ import { InjectConnection } from '@nestjs/mongoose';
|
|
|
3
3
|
import * as mongo from 'mongodb';
|
|
4
4
|
import { Connection, Types } from 'mongoose';
|
|
5
5
|
|
|
6
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
7
|
+
import { CoreS3Service } from '../../../common/services/core-s3.service';
|
|
6
8
|
import { ModelRegistry } from '../../../common/services/model-registry.service';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_FILESYSTEM_DIR,
|
|
11
|
+
FILESYSTEM_FILES_COLLECTION,
|
|
12
|
+
FilesystemFileHelper,
|
|
13
|
+
} from '../../file/filesystem-file.helper';
|
|
14
|
+
import { S3_FILES_COLLECTION, S3FileHelper } from '../../file/s3-file.helper';
|
|
7
15
|
import { HubActionMessage } from '../hub-action-messages';
|
|
8
16
|
import { HUB_CONFIG } from '../hub.constants';
|
|
9
17
|
import { buildErDiagram, HubModelDescriptor, HubModelField } from '../helpers/hub-mermaid.helper';
|
|
10
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
HubDbData,
|
|
20
|
+
HubFilesData,
|
|
21
|
+
HubFileStore,
|
|
22
|
+
HubFileStoreSummary,
|
|
23
|
+
HubModelsData,
|
|
24
|
+
HubUnavailable,
|
|
25
|
+
} from '../interfaces/hub-panels.interface';
|
|
11
26
|
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
12
27
|
|
|
13
28
|
/**
|
|
@@ -23,6 +38,10 @@ export class CoreHubDbService {
|
|
|
23
38
|
constructor(
|
|
24
39
|
@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig,
|
|
25
40
|
@Optional() @InjectConnection() protected readonly connection?: Connection,
|
|
41
|
+
// Provided and exported globally by `CoreModule`, but INERT without an `s3` config — so it is
|
|
42
|
+
// optional here for the same reason it is everywhere else: a project that uses no S3 must not
|
|
43
|
+
// be forced to install `@aws-sdk/client-s3` to open the Hub.
|
|
44
|
+
@Optional() protected readonly s3Service?: CoreS3Service,
|
|
26
45
|
) {}
|
|
27
46
|
|
|
28
47
|
/** Database + per-collection statistics. */
|
|
@@ -85,10 +104,21 @@ export class CoreHubDbService {
|
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
/**
|
|
88
|
-
* Delete a
|
|
89
|
-
*
|
|
107
|
+
* Delete a file by id, from WHICHEVER store actually holds it.
|
|
108
|
+
*
|
|
109
|
+
* When `expectedFilename` is given it must match the stored filename (the type-to-confirm
|
|
110
|
+
* keyword). Returns the filename and the store it came from; throws when not found, when the name
|
|
111
|
+
* mismatches, or when the owning store cannot be reached.
|
|
112
|
+
*
|
|
113
|
+
* This used to delete from GridFS only, which meant an S3- or filesystem-backed file could not be
|
|
114
|
+
* removed from the Hub at all — the lookup answered `File not found.` for a file plainly sitting
|
|
115
|
+
* in the bucket.
|
|
90
116
|
*/
|
|
91
|
-
async deleteFile(
|
|
117
|
+
async deleteFile(
|
|
118
|
+
id: string,
|
|
119
|
+
expectedFilename?: string,
|
|
120
|
+
bucket = 'fs',
|
|
121
|
+
): Promise<{ filename: string; id: string; store: HubFileStore }> {
|
|
92
122
|
const db = this.connection?.db;
|
|
93
123
|
if (!db) {
|
|
94
124
|
throw new Error(HubActionMessage.mongoUnavailable);
|
|
@@ -97,46 +127,140 @@ export class CoreHubDbService {
|
|
|
97
127
|
throw new Error(HubActionMessage.invalidFileId);
|
|
98
128
|
}
|
|
99
129
|
const objectId = new Types.ObjectId(id);
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
|
|
131
|
+
// Same probe order as `CoreFileService.deleteFile()`: S3, then filesystem, then GridFS as the
|
|
132
|
+
// fallthrough. Keeping the two in step matters — a Hub that resolved a file differently from the
|
|
133
|
+
// service would delete a different file than the one it displayed.
|
|
134
|
+
let store: HubFileStore | undefined;
|
|
135
|
+
let doc: any;
|
|
136
|
+
for (const candidate of this.fileStores(bucket)) {
|
|
137
|
+
const [found] = await db.collection(candidate.collection).find({ _id: objectId }, { limit: 1 }).toArray();
|
|
138
|
+
if (found) {
|
|
139
|
+
doc = found;
|
|
140
|
+
store = candidate.store;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!doc || !store) {
|
|
102
145
|
throw new Error(HubActionMessage.fileNotFound);
|
|
103
146
|
}
|
|
104
147
|
if (expectedFilename !== undefined && expectedFilename !== doc.filename) {
|
|
105
148
|
throw new Error(HubActionMessage.confirmationFilenameMismatch);
|
|
106
149
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
|
|
151
|
+
switch (store) {
|
|
152
|
+
case 'filesystem': {
|
|
153
|
+
await FilesystemFileHelper.deleteFile(this.filesystemDir, db.collection(FILESYSTEM_FILES_COLLECTION), objectId);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 's3': {
|
|
157
|
+
// Refuse rather than half-delete — see HubActionMessage.s3Unavailable.
|
|
158
|
+
if (!this.s3Service?.enabled) {
|
|
159
|
+
throw new Error(HubActionMessage.s3Unavailable);
|
|
160
|
+
}
|
|
161
|
+
await S3FileHelper.deleteFile(this.s3Service, db.collection(S3_FILES_COLLECTION), objectId);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default: {
|
|
165
|
+
const gridFs = new mongo.GridFSBucket(db as unknown as mongo.Db, { bucketName: bucket });
|
|
166
|
+
await gridFs.delete(objectId as unknown as mongo.ObjectId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { filename: doc.filename, id, store };
|
|
110
171
|
}
|
|
111
172
|
|
|
112
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* File inventory across ALL THREE metadata stores.
|
|
175
|
+
*
|
|
176
|
+
* Metadata always lives in MongoDB whichever store holds the bytes (`fs.files` / `s3-files` /
|
|
177
|
+
* `filesystem-files`), so one connection can see everything — which is exactly what
|
|
178
|
+
* `CoreFileService.findFileInfo()` does, and what this panel now mirrors.
|
|
179
|
+
*
|
|
180
|
+
* It used to read `fs.files` alone. Under `file.storage: 's3'` or `'filesystem'` that made the
|
|
181
|
+
* panel report **0 files** — not "this panel does not cover your driver", but a confident wrong
|
|
182
|
+
* answer, in the one tool an operator opens BECAUSE they are unsure. Someone checking whether an
|
|
183
|
+
* upload landed would have concluded it had not.
|
|
184
|
+
*
|
|
185
|
+
* Paging over a merge is the trap here: taking `skip`/`limit` from each store and concatenating
|
|
186
|
+
* returns the wrong ROWS, not merely the wrong order (the same defect `sortMergedFileInfo()` was
|
|
187
|
+
* fixed for). So each store yields its newest `skip + limit`, the union is re-sorted, and only
|
|
188
|
+
* then is the window cut — which is correct because the global newest `skip + limit` rows are
|
|
189
|
+
* necessarily contained in the per-store newest `skip + limit`.
|
|
190
|
+
*
|
|
191
|
+
* READS NEVER CREATE A COLLECTION: `find()` and `countDocuments()` on an absent collection answer
|
|
192
|
+
* empty. A GridFS-only deployment therefore does not grow an empty `s3-files` from the Hub merely
|
|
193
|
+
* looking at it — the same rule `ensureFilenameIndex()` follows on the write path.
|
|
194
|
+
*/
|
|
113
195
|
async getFiles(bucket = 'fs', skip = 0, limit = 100): Promise<HubFilesData | HubUnavailable> {
|
|
114
196
|
const db = this.connection?.db;
|
|
115
197
|
if (!db) {
|
|
116
198
|
return { available: false, hint: 'No MongoDB connection is available.' };
|
|
117
199
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
|
|
201
|
+
const safeSkip = Math.max(0, skip);
|
|
202
|
+
const safeLimit = Math.min(Math.max(1, limit), 500);
|
|
203
|
+
// Per-store window: enough to guarantee the merged page is complete, capped so a huge `skip`
|
|
204
|
+
// cannot turn one panel poll into an unbounded read.
|
|
205
|
+
const window = Math.min(safeSkip + safeLimit, 1000);
|
|
206
|
+
|
|
207
|
+
const stores: HubFileStoreSummary[] = [];
|
|
208
|
+
const rows: HubFilesData['files'] = [];
|
|
209
|
+
|
|
210
|
+
for (const source of this.fileStores(bucket)) {
|
|
211
|
+
try {
|
|
212
|
+
const docs = await db
|
|
213
|
+
.collection(source.collection)
|
|
214
|
+
.find({}, { limit: window, sort: { uploadDate: -1 } })
|
|
215
|
+
.toArray();
|
|
216
|
+
const count = await db.collection(source.collection).countDocuments();
|
|
217
|
+
stores.push({ collection: source.collection, count, store: source.store });
|
|
218
|
+
for (const doc of docs) {
|
|
219
|
+
rows.push({
|
|
220
|
+
contentType: doc.contentType ?? doc.metadata?.contentType,
|
|
221
|
+
filename: doc.filename,
|
|
222
|
+
id: String(doc._id),
|
|
223
|
+
length: doc.length ?? 0,
|
|
224
|
+
store: source.store,
|
|
225
|
+
uploadDate: doc.uploadDate ? new Date(doc.uploadDate).toISOString() : undefined,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// One unreadable store must not blank the panel: the other two still hold real answers, and
|
|
230
|
+
// an operator is better served by "two stores listed, this one errored" than by nothing.
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
this.logger.warn(`Failed to list files in ${source.collection}: ${message}`);
|
|
233
|
+
stores.push({ collection: source.collection, count: 0, error: message, store: source.store });
|
|
234
|
+
}
|
|
139
235
|
}
|
|
236
|
+
|
|
237
|
+
rows.sort((a, b) => (b.uploadDate ?? '').localeCompare(a.uploadDate ?? ''));
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
bucket,
|
|
241
|
+
files: rows.slice(safeSkip, safeSkip + safeLimit),
|
|
242
|
+
stores,
|
|
243
|
+
total: stores.reduce((sum, entry) => sum + entry.count, 0),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Directory used by the `'filesystem'` driver — resolved exactly as `CoreFileService` does. */
|
|
248
|
+
protected get filesystemDir(): string {
|
|
249
|
+
return ConfigService.configFastButReadOnly?.file?.storageDir || DEFAULT_FILESYSTEM_DIR;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The three metadata stores, in the order both this service and `CoreFileService` probe them.
|
|
254
|
+
*
|
|
255
|
+
* GridFS is last because it is the fallthrough: its collection name depends on the bucket, and a
|
|
256
|
+
* document there carries no `storage` marker to identify it by.
|
|
257
|
+
*/
|
|
258
|
+
protected fileStores(bucket: string): { collection: string; store: HubFileStore }[] {
|
|
259
|
+
return [
|
|
260
|
+
{ collection: S3_FILES_COLLECTION, store: 's3' },
|
|
261
|
+
{ collection: FILESYSTEM_FILES_COLLECTION, store: 'filesystem' },
|
|
262
|
+
{ collection: `${bucket}.files`, store: 'gridfs' },
|
|
263
|
+
];
|
|
140
264
|
}
|
|
141
265
|
|
|
142
266
|
/** Model inventory + a Mermaid ER diagram derived from the registered Mongoose schemas. */
|
|
@@ -5,6 +5,7 @@ import { Connection } from 'mongoose';
|
|
|
5
5
|
import { RoleEnum } from '../../../core/common/enums/role.enum';
|
|
6
6
|
import { ConfigService } from '../../../core/common/services/config.service';
|
|
7
7
|
import { CoreS3Service } from '../../../core/common/services/core-s3.service';
|
|
8
|
+
import { CoreFileInfo } from '../../../core/modules/file/core-file-info.model';
|
|
8
9
|
import { CoreFileService, FileInputCheckType } from '../../../core/modules/file/core-file.service';
|
|
9
10
|
import { FileServiceOptions } from '../../../core/modules/file/interfaces/file-service-options.interface';
|
|
10
11
|
|
|
@@ -22,10 +23,26 @@ export class FileService extends CoreFileService {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Duplicate file by name
|
|
26
|
+
* Duplicate file by name.
|
|
27
|
+
*
|
|
28
|
+
* Delegates instead of reaching into `this.files` directly. The direct GridFS
|
|
29
|
+
* pipe this used to be was wrong in four separate ways, and every one of them
|
|
30
|
+
* is the kind of thing a consuming project copies out of here:
|
|
31
|
+
*
|
|
32
|
+
* - it only ever worked on the GridFS driver — under `file.storage: 's3'` or
|
|
33
|
+
* `'filesystem'` the source is simply not in the bucket;
|
|
34
|
+
* - it bypassed `checkRights()` entirely, so the owner rule below did not apply
|
|
35
|
+
* to a duplicate at all;
|
|
36
|
+
* - it returned the write stream without awaiting it, so the caller was told the
|
|
37
|
+
* copy existed while it was still being written;
|
|
38
|
+
* - neither stream carried an error handler, and an unhandled stream `'error'`
|
|
39
|
+
* takes the whole process down.
|
|
40
|
+
*
|
|
41
|
+
* `duplicateByName()` answers all four. Forward the caller's context so the
|
|
42
|
+
* duplicate is COVERED by the ownership rule rather than exempt from it.
|
|
26
43
|
*/
|
|
27
|
-
async duplicate(fileName: string, newName: string): Promise<
|
|
28
|
-
return this.
|
|
44
|
+
async duplicate(fileName: string, newName: string, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
45
|
+
return this.duplicateByName(fileName, newName, serviceOptions);
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
/**
|