@lenne.tech/nest-server 11.1.14 → 11.2.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.
Files changed (45) hide show
  1. package/dist/core/common/helpers/gridfs.helper.d.ts +42 -0
  2. package/dist/core/common/helpers/gridfs.helper.js +107 -0
  3. package/dist/core/common/helpers/gridfs.helper.js.map +1 -0
  4. package/dist/core/common/services/brevo.service.d.ts +1 -0
  5. package/dist/core/common/services/brevo.service.js +19 -18
  6. package/dist/core/common/services/brevo.service.js.map +1 -1
  7. package/dist/core/common/services/crud.service.js +1 -1
  8. package/dist/core/common/services/crud.service.js.map +1 -1
  9. package/dist/core/modules/file/core-file.controller.d.ts +2 -1
  10. package/dist/core/modules/file/core-file.controller.js +1 -1
  11. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  12. package/dist/core/modules/file/core-file.service.d.ts +7 -7
  13. package/dist/core/modules/file/core-file.service.js +28 -51
  14. package/dist/core/modules/file/core-file.service.js.map +1 -1
  15. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +1 -1
  16. package/dist/core/modules/user/core-user.service.js +2 -3
  17. package/dist/core/modules/user/core-user.service.js.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/server/modules/file/file-info.model.d.ts +7 -3
  22. package/dist/server/modules/file/file.controller.d.ts +6 -4
  23. package/dist/server/modules/file/file.controller.js +15 -4
  24. package/dist/server/modules/file/file.controller.js.map +1 -1
  25. package/dist/server/modules/file/file.resolver.js +1 -1
  26. package/dist/server/modules/file/file.resolver.js.map +1 -1
  27. package/dist/server/modules/file/multer-config.service.d.ts +0 -2
  28. package/dist/server/modules/file/multer-config.service.js +3 -22
  29. package/dist/server/modules/file/multer-config.service.js.map +1 -1
  30. package/dist/server/modules/user/user.model.d.ts +7 -3
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/dist/types/graphql-upload.d.ts +25 -0
  33. package/package.json +21 -23
  34. package/src/core/common/helpers/gridfs.helper.ts +227 -0
  35. package/src/core/common/services/brevo.service.ts +20 -18
  36. package/src/core/common/services/crud.service.ts +3 -3
  37. package/src/core/modules/file/core-file.controller.ts +3 -2
  38. package/src/core/modules/file/core-file.service.ts +49 -60
  39. package/src/core/modules/file/interfaces/file-upload.interface.ts +2 -1
  40. package/src/core/modules/user/core-user.service.ts +3 -3
  41. package/src/index.ts +1 -0
  42. package/src/server/modules/file/file.controller.ts +25 -7
  43. package/src/server/modules/file/file.resolver.ts +1 -2
  44. package/src/server/modules/file/multer-config.service.ts +6 -21
  45. package/src/types/graphql-upload.d.ts +25 -0
@@ -1,11 +1,10 @@
1
- import { createBucket, MongoGridFSOptions, MongooseGridFS } from '@lenne.tech/mongoose-gridfs';
2
1
  import { NotFoundException } from '@nestjs/common';
3
- import { GridFSBucket, GridFSBucketReadStream, GridFSBucketReadStreamOptions } from 'mongodb';
4
- import mongoose, { Connection, Types } from 'mongoose';
2
+ import mongoose, { Connection, mongo, Types } from 'mongoose';
5
3
 
6
4
  import { FilterArgs } from '../../common/args/filter.args';
7
5
  import { getObjectIds, getStringIds } from '../../common/helpers/db.helper';
8
6
  import { convertFilterArgsToQuery } from '../../common/helpers/filter.helper';
7
+ import { GridFSHelper } from '../../common/helpers/gridfs.helper';
9
8
  import { check } from '../../common/helpers/input.helper';
10
9
  import { prepareOutput } from '../../common/helpers/service.helper';
11
10
  import { MaybePromise } from '../../common/types/maybe-promise.type';
@@ -22,7 +21,8 @@ export type FileInputCheckType = 'file' | 'filename' | 'files' | 'filterArgs' |
22
21
  * Abstract core file service
23
22
  */
24
23
  export abstract class CoreFileService {
25
- files: GridFSBucket & MongooseGridFS;
24
+ // Use the native MongoDB driver's types (accessed via Mongoose's exports) to avoid BSON version conflicts
25
+ files: mongo.GridFSBucket;
26
26
 
27
27
  /**
28
28
  * Include MongoDB connection and create File bucket
@@ -31,7 +31,8 @@ export abstract class CoreFileService {
31
31
  protected readonly connection: Connection,
32
32
  bucketName = 'fs',
33
33
  ) {
34
- this.files = createBucket({ bucketName, connection });
34
+ // Use the native MongoDB driver's GridFSBucket via Mongoose's mongo export to avoid BSON version conflicts
35
+ this.files = new mongo.GridFSBucket(connection.db, { bucketName });
35
36
  }
36
37
 
37
38
  /**
@@ -41,15 +42,13 @@ export abstract class CoreFileService {
41
42
  if (!(await this.checkRights(file, { ...serviceOptions, checkInputType: 'file' }))) {
42
43
  return null;
43
44
  }
44
- return await new Promise(async (resolve, reject) => {
45
- // eslint-disable-next-line unused-imports/no-unused-vars
46
- const { createReadStream, encoding, filename, mimetype } = await file;
47
- const readStream = createReadStream();
48
- const options: MongoGridFSOptions = { contentType: mimetype, filename };
49
- this.files.writeFile(options, readStream, (error, fileInfo) => {
50
- error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
51
- });
45
+ const { createReadStream, filename, mimetype } = await file;
46
+ const readStream = createReadStream();
47
+ const fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
48
+ contentType: mimetype,
49
+ filename,
52
50
  });
51
+ return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
53
52
  }
54
53
 
55
54
  /**
@@ -71,7 +70,11 @@ export abstract class CoreFileService {
71
70
  */
72
71
  async duplicateByName(name: string, newName: string): Promise<any> {
73
72
  return new Promise(async (resolve) => {
74
- resolve(this.files.openDownloadStreamByName(name).pipe(this.files.openUploadStream(newName)));
73
+ resolve(
74
+ GridFSHelper.openDownloadStreamByName(this.files, name).pipe(
75
+ GridFSHelper.openUploadStream(this.files, newName),
76
+ ),
77
+ );
75
78
  });
76
79
  }
77
80
 
@@ -82,10 +85,10 @@ export abstract class CoreFileService {
82
85
  const objectId = getObjectIds(id);
83
86
  const file = await this.getFileInfo(objectId);
84
87
  return new Promise((resolve, reject) => {
85
- const downloadStream = this.files.openDownloadStream(objectId);
88
+ const downloadStream = GridFSHelper.openDownloadStream(this.files, objectId);
86
89
 
87
90
  const newFileId = new mongoose.Types.ObjectId();
88
- const uploadStream = this.files.openUploadStreamWithId(newFileId, file.filename, {
91
+ const uploadStream = GridFSHelper.openUploadStreamWithId(this.files, newFileId, file.filename, {
89
92
  contentType: file.contentType,
90
93
  });
91
94
 
@@ -112,16 +115,9 @@ export abstract class CoreFileService {
112
115
  if (!(await this.checkRights(filterArgs, { ...serviceOptions, checkInputType: 'filterArgs' }))) {
113
116
  return null;
114
117
  }
115
- return await new Promise((resolve, reject) => {
116
- const filterQuery = convertFilterArgsToQuery(filterArgs);
117
- const cursor = this.files.find(filterQuery[0], filterQuery[1]);
118
- if (!cursor) {
119
- throw new NotFoundException('File collection not found');
120
- }
121
- cursor.toArray((error, docs) => {
122
- error ? reject(error) : resolve(this.prepareOutput(docs, serviceOptions));
123
- });
124
- });
118
+ const filterQuery = convertFilterArgsToQuery(filterArgs);
119
+ const docs = await GridFSHelper.findFiles(this.files, filterQuery[0], filterQuery[1]);
120
+ return this.prepareOutput(docs as unknown as CoreFileInfo[], serviceOptions);
125
121
  }
126
122
 
127
123
  /**
@@ -131,11 +127,8 @@ export abstract class CoreFileService {
131
127
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
132
128
  return null;
133
129
  }
134
- return await new Promise((resolve, reject) => {
135
- this.files.findById(getObjectIds(id), (error, fileInfo) => {
136
- error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
137
- });
138
- });
130
+ const fileInfo = await GridFSHelper.findFileById(this.files, getObjectIds(id));
131
+ return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
139
132
  }
140
133
 
141
134
  /**
@@ -145,11 +138,8 @@ export abstract class CoreFileService {
145
138
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
146
139
  return null;
147
140
  }
148
- return await new Promise((resolve, reject) => {
149
- this.files.findOne({ filename }, (error, fileInfo) => {
150
- error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
151
- });
152
- });
141
+ const fileInfo = await GridFSHelper.findFileByName(this.files, filename);
142
+ return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
153
143
  }
154
144
 
155
145
  /**
@@ -159,7 +149,7 @@ export abstract class CoreFileService {
159
149
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
160
150
  return null;
161
151
  }
162
- return this.files.openDownloadStream(getObjectIds(id)) as GridFSBucketReadStream;
152
+ return GridFSHelper.openDownloadStream(this.files, getObjectIds(id)) as mongo.GridFSBucketReadStream;
163
153
  }
164
154
 
165
155
  /**
@@ -168,11 +158,11 @@ export abstract class CoreFileService {
168
158
  async getFileStreamByName(
169
159
  filename: string,
170
160
  serviceOptions?: FileServiceOptions,
171
- ): Promise<GridFSBucketReadStreamOptions> {
161
+ ): Promise<mongo.GridFSBucketReadStream> {
172
162
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
173
163
  return null;
174
164
  }
175
- return this.files.readFile({ filename });
165
+ return GridFSHelper.openDownloadStreamByName(this.files, filename);
176
166
  }
177
167
 
178
168
  /**
@@ -182,39 +172,30 @@ export abstract class CoreFileService {
182
172
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
183
173
  return null;
184
174
  }
185
- return await new Promise((resolve, reject) => {
186
- this.files.readFile({ _id: getObjectIds(id) }, (error, buffer) => {
187
- error ? reject(error) : resolve(buffer);
188
- });
189
- });
175
+ return await GridFSHelper.readFileToBuffer(this.files, { _id: getObjectIds(id) });
190
176
  }
191
177
 
192
178
  /**
193
- * Get file buffer (for small files) via file ID
179
+ * Get file buffer (for small files) via filename
194
180
  */
195
181
  async getBufferByName(filename: string, serviceOptions?: FileServiceOptions): Promise<Buffer> {
196
182
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
197
183
  return null;
198
184
  }
199
- return await new Promise((resolve, reject) => {
200
- this.files.readFile({ filename }, (error, buffer) => {
201
- error ? reject(error) : resolve(buffer);
202
- });
203
- });
185
+ return await GridFSHelper.readFileToBuffer(this.files, { filename });
204
186
  }
205
187
 
206
188
  /**
207
- * Delete file reference of avatar
189
+ * Delete file
208
190
  */
209
191
  async deleteFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
210
192
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
211
193
  return null;
212
194
  }
213
- return await new Promise((resolve, reject) => {
214
- return this.files.unlink(getObjectIds(id), (error, fileInfo) => {
215
- error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
216
- });
217
- });
195
+ const objectId = getObjectIds(id);
196
+ const fileInfo = await this.getFileInfo(objectId, serviceOptions);
197
+ await GridFSHelper.deleteFile(this.files, objectId);
198
+ return fileInfo;
218
199
  }
219
200
 
220
201
  /**
@@ -247,15 +228,23 @@ export abstract class CoreFileService {
247
228
  }
248
229
 
249
230
  /**
250
- * Prepare output before return
231
+ * Prepare output before return - single file
232
+ * Accepts both GridFSFileInfo (from GridFS operations) and CoreFileInfo
233
+ * They are structurally compatible (duck typing), so we use type assertion
251
234
  */
252
- protected async prepareOutput(fileInfo: CoreFileInfo | CoreFileInfo[], options?: FileServiceOptions) {
235
+ protected async prepareOutput(fileInfo: CoreFileInfo, options?: FileServiceOptions): Promise<CoreFileInfo>;
236
+ protected async prepareOutput(fileInfo: null, options?: FileServiceOptions): Promise<null>;
237
+ protected async prepareOutput(fileInfo: CoreFileInfo[], options?: FileServiceOptions): Promise<CoreFileInfo[]>;
238
+ protected async prepareOutput(
239
+ fileInfo: CoreFileInfo | CoreFileInfo[] | null,
240
+ options?: FileServiceOptions,
241
+ ): Promise<CoreFileInfo | CoreFileInfo[] | null> {
253
242
  if (!fileInfo) {
254
243
  return fileInfo;
255
244
  }
256
245
  this.setId(fileInfo);
257
- fileInfo = await prepareOutput(fileInfo, { targetModel: CoreFileInfo });
258
- return check(fileInfo, options?.currentUser, { roles: options?.roles });
246
+ const prepared = await prepareOutput(fileInfo, { targetModel: CoreFileInfo });
247
+ return check(prepared, options?.currentUser, { roles: options?.roles });
259
248
  }
260
249
 
261
250
  /**
@@ -23,8 +23,9 @@ export interface FileUpload {
23
23
 
24
24
  /**
25
25
  * Stream transfer encoding of the file
26
+ * @deprecated This property is deprecated and may be removed in future versions
26
27
  */
27
- encoding: string;
28
+ encoding?: string;
28
29
 
29
30
  /**
30
31
  * Name of the file
@@ -115,9 +115,9 @@ export abstract class CoreUserService<
115
115
  return this.process(
116
116
  async () => {
117
117
  // Update and return user
118
- return await this.mainDbModel
119
- .updateOne({ _id: dbObject.id }, { verified: true, verifiedAt: new Date() }, { returnDocument: 'after' })
120
- .exec();
118
+ await this.mainDbModel.updateOne({ _id: dbObject.id }, { verified: true, verifiedAt: new Date() }).exec();
119
+ // Return the updated user
120
+ return await this.mainDbModel.findById(dbObject.id).exec();
121
121
  },
122
122
  { dbObject, serviceOptions },
123
123
  );
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ export * from './core/common/helpers/decorator.helper';
34
34
  export * from './core/common/helpers/file.helper';
35
35
  export * from './core/common/helpers/filter.helper';
36
36
  export * from './core/common/helpers/graphql.helper';
37
+ export * from './core/common/helpers/gridfs.helper';
37
38
  export * from './core/common/helpers/input.helper';
38
39
  export * from './core/common/helpers/model.helper';
39
40
  export * from './core/common/helpers/scim.helper';
@@ -11,13 +11,17 @@ import {
11
11
  UseInterceptors,
12
12
  } from '@nestjs/common';
13
13
  import { FileInterceptor } from '@nestjs/platform-express';
14
+ import { Response } from 'express';
15
+ import { Readable } from 'stream';
14
16
 
15
17
  import { Roles } from '../../../core/common/decorators/roles.decorator';
16
18
  import { RoleEnum } from '../../../core/common/enums/role.enum';
19
+ import { CoreFileInfo } from '../../../core/modules/file/core-file-info.model';
20
+ import { FileUpload } from '../../../core/modules/file/interfaces/file-upload.interface';
17
21
  import { FileService } from './file.service';
18
22
 
19
23
  /**
20
- * File controller for
24
+ * File controller
21
25
  */
22
26
  @Controller('files')
23
27
  @Roles(RoleEnum.ADMIN)
@@ -28,13 +32,26 @@ export class FileController {
28
32
  constructor(private readonly fileService: FileService) {}
29
33
 
30
34
  /**
31
- * Upload file
35
+ * Upload file via HTTP
32
36
  */
33
37
  @Post('upload')
34
38
  @Roles(RoleEnum.ADMIN)
35
39
  @UseInterceptors(FileInterceptor('file'))
36
- uploadFile(@UploadedFile() file: Express.Multer.File): any {
37
- return file;
40
+ async uploadFile(@UploadedFile() file: Express.Multer.File): Promise<any> {
41
+ if (!file) {
42
+ throw new BadRequestException('No file provided');
43
+ }
44
+
45
+ // Convert Multer file to FileUpload interface
46
+ const fileUpload: FileUpload = {
47
+ capacitor: null, // Not used when creating from buffer
48
+ createReadStream: () => Readable.from(file.buffer),
49
+ filename: file.originalname,
50
+ mimetype: file.mimetype,
51
+ };
52
+
53
+ // Save to GridFS using FileService
54
+ return await this.fileService.createFile(fileUpload);
38
55
  }
39
56
 
40
57
  /**
@@ -42,23 +59,24 @@ export class FileController {
42
59
  */
43
60
  @Get(':id')
44
61
  @Roles(RoleEnum.ADMIN)
45
- async getFile(@Param('id') id: string, @Res() res) {
62
+ async getFile(@Param('id') id: string, @Res() res: Response) {
46
63
  if (!id) {
47
64
  throw new BadRequestException('Missing ID');
48
65
  }
49
66
 
50
- let file;
67
+ let file: CoreFileInfo | null;
51
68
  try {
52
69
  file = await this.fileService.getFileInfo(id);
53
70
  } catch (e) {
54
71
  console.error(e);
72
+ file = null;
55
73
  }
56
74
 
57
75
  if (!file) {
58
76
  throw new NotFoundException('File not found');
59
77
  }
60
78
  const filestream = await this.fileService.getFileStream(id);
61
- res.header('Content-Type', file.contentType);
79
+ res.header('Content-Type', file.contentType || 'application/octet-stream');
62
80
  res.header('Content-Disposition', `attachment; filename=${file.filename}`);
63
81
  res.header('Cache-Control', 'max-age=31536000');
64
82
  return filestream.pipe(res);
@@ -64,8 +64,7 @@ export class FileResolver {
64
64
  // Save files in filesystem
65
65
  const promises: Promise<any>[] = [];
66
66
  for (const file of files) {
67
- // eslint-disable-next-line unused-imports/no-unused-vars
68
- const { createReadStream, encoding, filename, mimetype } = await file;
67
+ const { createReadStream, filename } = await file;
69
68
  await fs.promises.mkdir('./uploads', { recursive: true });
70
69
  promises.push(
71
70
  new Promise((resolve, reject) =>
@@ -1,31 +1,16 @@
1
- import { GridFsStorage } from '@lenne.tech/multer-gridfs-storage';
2
1
  import { Injectable } from '@nestjs/common';
3
2
  import { MulterModuleOptions, MulterOptionsFactory } from '@nestjs/platform-express';
3
+ import { memoryStorage } from 'multer';
4
4
 
5
- import envConfig from '../../../config.env';
6
-
5
+ /**
6
+ * Multer configuration service using MemoryStorage
7
+ * Files are stored in memory as Buffer objects and then manually saved to GridFS
8
+ */
7
9
  @Injectable()
8
10
  export class GridFsMulterConfigService implements MulterOptionsFactory {
9
- gridFsStorage: any;
10
-
11
- constructor() {
12
- this.gridFsStorage = new GridFsStorage({
13
- file: (req, file) => {
14
- return new Promise((resolve) => {
15
- const filename = file.originalname.trim();
16
- const fileInfo = {
17
- filename,
18
- };
19
- resolve(fileInfo);
20
- });
21
- },
22
- url: envConfig.mongoose.uri,
23
- });
24
- }
25
-
26
11
  createMulterOptions(): MulterModuleOptions {
27
12
  return {
28
- storage: this.gridFsStorage,
13
+ storage: memoryStorage(),
29
14
  };
30
15
  }
31
16
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Type definitions for graphql-upload
3
+ * graphql-upload uses deep imports, so we declare types for the specific modules
4
+ */
5
+
6
+ declare module 'graphql-upload/graphqlUploadExpress.js' {
7
+ import { RequestHandler } from 'express';
8
+
9
+ interface GraphQLUploadOptions {
10
+ maxFiles?: number;
11
+ maxFileSize?: number;
12
+ }
13
+
14
+ function graphqlUploadExpress(options?: GraphQLUploadOptions): RequestHandler;
15
+
16
+ export = graphqlUploadExpress;
17
+ }
18
+
19
+ declare module 'graphql-upload/GraphQLUpload.js' {
20
+ import { GraphQLScalarType } from 'graphql';
21
+
22
+ const GraphQLUpload: GraphQLScalarType;
23
+
24
+ export = GraphQLUpload;
25
+ }