@lenne.tech/nest-server 8.6.7 → 8.6.10

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 (109) hide show
  1. package/dist/core/common/decorators/graphql-user.decorator.js +2 -3
  2. package/dist/core/common/decorators/graphql-user.decorator.js.map +1 -1
  3. package/dist/core/common/decorators/restricted.decorator.js +14 -13
  4. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  5. package/dist/core/common/helpers/context.helper.js +4 -5
  6. package/dist/core/common/helpers/context.helper.js.map +1 -1
  7. package/dist/core/common/helpers/db.helper.js +32 -21
  8. package/dist/core/common/helpers/db.helper.js.map +1 -1
  9. package/dist/core/common/helpers/file.helper.js +5 -1
  10. package/dist/core/common/helpers/file.helper.js.map +1 -1
  11. package/dist/core/common/helpers/filter.helper.js +16 -9
  12. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  13. package/dist/core/common/helpers/graphql.helper.js +11 -8
  14. package/dist/core/common/helpers/graphql.helper.js.map +1 -1
  15. package/dist/core/common/helpers/input.helper.js +17 -11
  16. package/dist/core/common/helpers/input.helper.js.map +1 -1
  17. package/dist/core/common/helpers/model.helper.d.ts +2 -0
  18. package/dist/core/common/helpers/model.helper.js +125 -3
  19. package/dist/core/common/helpers/model.helper.js.map +1 -1
  20. package/dist/core/common/helpers/service.helper.js +27 -13
  21. package/dist/core/common/helpers/service.helper.js.map +1 -1
  22. package/dist/core/common/inputs/core-input.input.js +6 -1
  23. package/dist/core/common/inputs/core-input.input.js.map +1 -1
  24. package/dist/core/common/inputs/single-filter.input.d.ts +1 -0
  25. package/dist/core/common/inputs/single-filter.input.js +8 -0
  26. package/dist/core/common/inputs/single-filter.input.js.map +1 -1
  27. package/dist/core/common/models/core-model.model.js +14 -2
  28. package/dist/core/common/models/core-model.model.js.map +1 -1
  29. package/dist/core/common/pipes/check-input.pipe.js +1 -1
  30. package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
  31. package/dist/core/common/pipes/map-and-validate.pipe.js +2 -2
  32. package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
  33. package/dist/core/common/services/crud.service.js +3 -5
  34. package/dist/core/common/services/crud.service.js.map +1 -1
  35. package/dist/core/common/services/email.service.js +5 -1
  36. package/dist/core/common/services/email.service.js.map +1 -1
  37. package/dist/core/common/services/mailjet.service.d.ts +1 -1
  38. package/dist/core/common/services/mailjet.service.js +10 -3
  39. package/dist/core/common/services/mailjet.service.js.map +1 -1
  40. package/dist/core/common/services/module.service.js +29 -7
  41. package/dist/core/common/services/module.service.js.map +1 -1
  42. package/dist/core/modules/auth/core-auth.module.js +2 -2
  43. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  44. package/dist/core/modules/auth/guards/auth.guard.js +4 -5
  45. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  46. package/dist/core/modules/auth/guards/roles.guard.js +1 -2
  47. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  48. package/dist/core/modules/file/core-file.service.d.ts +26 -0
  49. package/dist/core/modules/file/core-file.service.js +116 -0
  50. package/dist/core/modules/file/core-file.service.js.map +1 -0
  51. package/dist/core/modules/file/file-info.output.d.ts +10 -0
  52. package/dist/core/modules/file/file-info.output.js +49 -0
  53. package/dist/core/modules/file/file-info.output.js.map +1 -0
  54. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +7 -0
  55. package/dist/core/modules/file/interfaces/file-service-options.interface.js +3 -0
  56. package/dist/core/modules/file/interfaces/file-service-options.interface.js.map +1 -0
  57. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +13 -0
  58. package/dist/core/modules/file/interfaces/file-upload.interface.js +3 -0
  59. package/dist/core/modules/file/interfaces/file-upload.interface.js.map +1 -0
  60. package/dist/core/modules/user/core-user.service.js +7 -3
  61. package/dist/core/modules/user/core-user.service.js.map +1 -1
  62. package/dist/core.module.js +34 -39
  63. package/dist/core.module.js.map +1 -1
  64. package/dist/index.d.ts +4 -0
  65. package/dist/index.js +4 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/server/modules/auth/auth.module.js +4 -1
  68. package/dist/server/modules/auth/auth.module.js.map +1 -1
  69. package/dist/server/modules/file/file.controller.d.ts +6 -1
  70. package/dist/server/modules/file/file.controller.js +34 -4
  71. package/dist/server/modules/file/file.controller.js.map +1 -1
  72. package/dist/server/modules/file/file.resolver.d.ts +8 -2
  73. package/dist/server/modules/file/file.resolver.js +47 -11
  74. package/dist/server/modules/file/file.resolver.js.map +1 -1
  75. package/dist/server/modules/file/file.service.d.ts +6 -0
  76. package/dist/server/modules/file/file.service.js +32 -0
  77. package/dist/server/modules/file/file.service.js.map +1 -0
  78. package/dist/server/modules/user/user.model.d.ts +1 -0
  79. package/dist/server/modules/user/user.model.js +5 -0
  80. package/dist/server/modules/user/user.model.js.map +1 -1
  81. package/dist/server/modules/user/user.resolver.js +1 -2
  82. package/dist/server/modules/user/user.resolver.js.map +1 -1
  83. package/dist/server/modules/user/user.service.js +2 -2
  84. package/dist/server/modules/user/user.service.js.map +1 -1
  85. package/dist/server/server.module.js +2 -1
  86. package/dist/server/server.module.js.map +1 -1
  87. package/dist/test/test.helper.d.ts +24 -1
  88. package/dist/test/test.helper.js +100 -13
  89. package/dist/test/test.helper.js.map +1 -1
  90. package/dist/tsconfig.build.tsbuildinfo +1 -1
  91. package/package.json +24 -23
  92. package/src/core/common/helpers/db.helper.ts +2 -0
  93. package/src/core/common/helpers/filter.helper.ts +7 -1
  94. package/src/core/common/helpers/model.helper.ts +152 -0
  95. package/src/core/common/inputs/single-filter.input.ts +10 -1
  96. package/src/core/common/services/mailjet.service.ts +1 -1
  97. package/src/core/common/services/module.service.ts +1 -1
  98. package/src/core/modules/file/core-file.service.ts +179 -0
  99. package/src/core/modules/file/file-info.output.ts +26 -0
  100. package/src/core/modules/file/interfaces/file-service-options.interface.ts +7 -0
  101. package/src/core/modules/file/interfaces/file-upload.interface.ts +38 -0
  102. package/src/core.module.ts +1 -1
  103. package/src/index.ts +9 -0
  104. package/src/server/modules/file/file.controller.ts +43 -4
  105. package/src/server/modules/file/file.resolver.ts +58 -33
  106. package/src/server/modules/file/file.service.ts +11 -0
  107. package/src/server/modules/user/user.model.ts +9 -0
  108. package/src/server/server.module.ts +2 -1
  109. package/src/test/test.helper.ts +125 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "8.6.7",
3
+ "version": "8.6.10",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -58,53 +58,54 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@apollo/gateway": "2.0.5",
61
- "@nestjs/apollo": "10.0.14",
62
- "@nestjs/common": "8.4.6",
63
- "@nestjs/core": "8.4.6",
64
- "@nestjs/graphql": "10.0.15",
61
+ "@nestjs/apollo": "10.0.16",
62
+ "@nestjs/common": "8.4.7",
63
+ "@nestjs/core": "8.4.7",
64
+ "@nestjs/graphql": "10.0.16",
65
65
  "@nestjs/jwt": "8.0.1",
66
- "@nestjs/mongoose": "9.1.0",
67
- "@nestjs/passport": "8.2.1",
68
- "@nestjs/platform-express": "8.4.6",
69
- "apollo-server-core": "3.8.2",
70
- "apollo-server-express": "3.8.2",
66
+ "@nestjs/mongoose": "9.1.1",
67
+ "@nestjs/passport": "8.2.2",
68
+ "@nestjs/platform-express": "8.4.7",
69
+ "apollo-server-core": "3.9.0",
70
+ "apollo-server-express": "3.9.0",
71
71
  "bcrypt": "5.0.1",
72
72
  "class-transformer": "0.5.1",
73
73
  "class-validator": "0.13.2",
74
74
  "ejs": "3.1.8",
75
75
  "graphql": "16.5.0",
76
76
  "graphql-subscriptions": "2.0.0",
77
- "graphql-upload": "13.0.0",
77
+ "graphql-upload": "15.0.1",
78
78
  "json-to-graphql-query": "2.2.4",
79
79
  "light-my-request": "5.0.0",
80
80
  "lodash": "4.17.21",
81
81
  "mongodb": "4.7.0",
82
- "mongoose": "6.3.6",
82
+ "mongoose": "6.3.9",
83
+ "mongoose-gridfs": "1.3.0",
83
84
  "multer": "1.4.4",
84
85
  "node-mailjet": "3.4.1",
85
86
  "nodemailer": "6.7.5",
86
87
  "nodemon": "2.0.16",
87
- "passport": "0.5.3",
88
+ "passport": "0.6.0",
88
89
  "passport-jwt": "4.0.0",
89
90
  "reflect-metadata": "0.1.13",
90
91
  "rimraf": "3.0.2",
91
92
  "rxjs": "7.5.5"
92
93
  },
93
94
  "devDependencies": {
94
- "@nestjs/testing": "8.4.6",
95
+ "@nestjs/testing": "8.4.7",
95
96
  "@types/ejs": "3.1.1",
96
- "@types/jest": "28.1.1",
97
+ "@types/jest": "28.1.3",
97
98
  "@types/lodash": "4.14.182",
98
99
  "@types/multer": "1.4.7",
99
- "@types/node": "16.11.39",
100
+ "@types/node": "18.0.0",
100
101
  "@types/node-mailjet": "3.3.9",
101
102
  "@types/nodemailer": "6.4.4",
102
- "@types/passport": "1.0.8",
103
+ "@types/passport": "1.0.9",
103
104
  "@types/supertest": "2.0.12",
104
- "@typescript-eslint/eslint-plugin": "5.27.1",
105
- "@typescript-eslint/parser": "5.27.1",
105
+ "@typescript-eslint/eslint-plugin": "5.29.0",
106
+ "@typescript-eslint/parser": "5.29.0",
106
107
  "coffeescript": "2.7.0",
107
- "eslint": "8.17.0",
108
+ "eslint": "8.18.0",
108
109
  "eslint-config-prettier": "8.5.0",
109
110
  "find-file-up": "2.0.1",
110
111
  "grunt": "1.5.3",
@@ -115,14 +116,14 @@
115
116
  "husky": "8.0.1",
116
117
  "jest": "28.1.1",
117
118
  "pm2": "5.2.0",
118
- "prettier": "2.6.2",
119
+ "prettier": "2.7.1",
119
120
  "pretty-quick": "3.1.3",
120
121
  "supertest": "6.2.3",
121
- "ts-jest": "28.0.4",
122
+ "ts-jest": "28.0.5",
122
123
  "ts-morph": "15.1.0",
123
124
  "ts-node": "10.8.1",
124
125
  "tsconfig-paths": "4.0.0",
125
- "typescript": "4.7.3"
126
+ "typescript": "4.7.4"
126
127
  },
127
128
  "jest": {
128
129
  "collectCoverage": true,
@@ -473,6 +473,8 @@ export async function popAndMap<T extends CoreModel>(
473
473
  populateOptions = getPopulatOptionsFromSelections(populate as SelectionNode[]);
474
474
  } else if ((populate as ResolveSelector).info) {
475
475
  populateOptions = getPopulateOptions((populate as ResolveSelector).info, (populate as ResolveSelector).select);
476
+ } else if (typeof populate === 'string' || (populate as PopulateOptions).path) {
477
+ populateOptions = [populate as PopulateOptions];
476
478
  }
477
479
  }
478
480
  if (queryOrDocument instanceof Query) {
@@ -139,7 +139,13 @@ export function generateFilterQuery<T = any>(filter?: Partial<FilterInput>): Fil
139
139
  // Process single filter
140
140
  if (filter.singleFilter) {
141
141
  // Init variables
142
- const { not, options, field, value } = filter.singleFilter;
142
+ const { not, options, field, convertToObjectId } = filter.singleFilter;
143
+ let value = filter.singleFilter.value;
144
+
145
+ // Convert value to object ID(s)
146
+ if (convertToObjectId) {
147
+ value = getObjectIds(value);
148
+ }
143
149
 
144
150
  // Convert filter
145
151
  switch (filter.singleFilter.operator) {
@@ -1,4 +1,6 @@
1
+ import { plainToInstance } from 'class-transformer';
1
2
  import * as _ from 'lodash';
3
+ import { Types } from 'mongoose';
2
4
 
3
5
  /**
4
6
  * Helper class for models
@@ -150,3 +152,153 @@ export function maps<T = Record<string, any>>(
150
152
  return (targetClass as any).map(item, { cloneDeep });
151
153
  });
152
154
  }
155
+
156
+ /**
157
+ * It takes an object, a mapping of properties to classes, and returns a new object with the properties mapped to instances
158
+ * of the classes
159
+ * @param input - The input object to map
160
+ * @param mapping - A mapping of property names to classes
161
+ * @param [target] - The object to map the input to. If not provided, a new object will be created
162
+ * @returns Record with mapped objects
163
+ */
164
+ export function mapClasses<T = Record<string, any>>(
165
+ input: Record<string, any>,
166
+ mapping: Record<string, new (...args: any[]) => any>,
167
+ target?: T
168
+ ): T {
169
+ // Check params
170
+ if (!target) {
171
+ target = {} as T;
172
+ }
173
+ if (!input || !mapping) {
174
+ return target;
175
+ }
176
+
177
+ // Process input
178
+ for (const [prop, value] of Object.entries(input)) {
179
+ if (prop in mapping) {
180
+ const targetClass = mapping[prop] as any;
181
+
182
+ // Process array
183
+ if (Array.isArray(value)) {
184
+ const arr = [];
185
+ for (const item of value) {
186
+ if (value instanceof targetClass) {
187
+ arr.push(value);
188
+ }
189
+ if (value instanceof Types.ObjectId) {
190
+ arr.push(value);
191
+ } else if (typeof value === 'object') {
192
+ if (targetClass.map) {
193
+ arr.push(targetClass.map(item));
194
+ } else if (typeof value === 'object') {
195
+ arr.push(plainToInstance(targetClass, item));
196
+ }
197
+ } else {
198
+ arr.push(value);
199
+ }
200
+ }
201
+ target[prop] = arr as any;
202
+ }
203
+
204
+ // Process ObjectId
205
+ else if (value instanceof Types.ObjectId) {
206
+ target[prop] = value as any;
207
+ }
208
+
209
+ // Process object
210
+ else if (typeof value === 'object') {
211
+ if (value instanceof targetClass) {
212
+ target[prop] = value as any;
213
+ }
214
+ if (targetClass.map) {
215
+ target[prop] = targetClass.map(value);
216
+ } else {
217
+ target[prop] = plainToInstance(targetClass, value) as any;
218
+ }
219
+ }
220
+
221
+ // Others
222
+ else {
223
+ target[prop] = value;
224
+ }
225
+ }
226
+ }
227
+
228
+ return target;
229
+ }
230
+
231
+ /**
232
+ * It takes an object, a mapping of properties to classes, and returns a new object with the properties mapped to instances
233
+ * of the classes async
234
+ * @param input - The input object to map
235
+ * @param mapping - A mapping of property names to classes
236
+ * @param [target] - The object to map the input to. If not provided, a new object will be created
237
+ * @returns Record with mapped objects
238
+ */
239
+ export async function mapClassesAsync<T = Record<string, any>>(
240
+ input: Record<string, any>,
241
+ mapping: Record<string, new (...args: any[]) => any>,
242
+ target?: T
243
+ ): Promise<T> {
244
+ // Check params
245
+ if (!target) {
246
+ target = {} as T;
247
+ }
248
+ if (!input || !mapping) {
249
+ return target;
250
+ }
251
+
252
+ // Process input
253
+ for (const [prop, value] of Object.entries(input)) {
254
+ if (prop in mapping) {
255
+ const targetClass = mapping[prop] as any;
256
+
257
+ // Process array
258
+ if (Array.isArray(value)) {
259
+ const arr = [];
260
+ for (const item of value) {
261
+ if (value instanceof targetClass) {
262
+ arr.push(value);
263
+ }
264
+ if (value instanceof Types.ObjectId) {
265
+ arr.push(value);
266
+ } else if (typeof value === 'object') {
267
+ if (targetClass.map) {
268
+ arr.push(await targetClass.map(item));
269
+ } else if (typeof value === 'object') {
270
+ arr.push(plainToInstance(targetClass, item));
271
+ }
272
+ } else {
273
+ arr.push(value);
274
+ }
275
+ }
276
+ target[prop] = arr as any;
277
+ }
278
+
279
+ // Process ObjectId
280
+ else if (value instanceof Types.ObjectId) {
281
+ target[prop] = value as any;
282
+ }
283
+
284
+ // Process object
285
+ else if (typeof value === 'object') {
286
+ if (value instanceof targetClass) {
287
+ target[prop] = value as any;
288
+ }
289
+ if (targetClass.map) {
290
+ target[prop] = await targetClass.map(value);
291
+ } else {
292
+ target[prop] = plainToInstance(targetClass, value) as any;
293
+ }
294
+ }
295
+
296
+ // Others
297
+ else {
298
+ target[prop] = value;
299
+ }
300
+ }
301
+ }
302
+
303
+ return target;
304
+ }
@@ -9,7 +9,16 @@ import { CoreInput } from './core-input.input';
9
9
  @InputType({ description: 'Input for a configuration of a filter' })
10
10
  export class SingleFilterInput extends CoreInput {
11
11
  /**
12
- * Name of the property to be used for the filter'
12
+ * Convert value to ObjectId
13
+ */
14
+ @Field({
15
+ description: 'Convert value to ObjectId',
16
+ nullable: true,
17
+ })
18
+ convertToObjectId?: boolean = undefined;
19
+
20
+ /**
21
+ * Name of the property to be used for the filter
13
22
  */
14
23
  @Field({ description: 'Name of the property to be used for the filter' })
15
24
  field: string = undefined;
@@ -1,6 +1,6 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
  import { ConfigService } from './config.service';
3
- import * as mailjet from 'node-mailjet';
3
+ import mailjet from 'node-mailjet';
4
4
 
5
5
  /**
6
6
  * Mailjet service
@@ -71,7 +71,7 @@ export abstract class ModuleService<T extends CoreModel = any> {
71
71
  }
72
72
  ) {
73
73
  // Configuration with default values
74
- const config = {
74
+ const config: { dbObject: string | Types.ObjectId | any; input: any } & ServiceOptions = {
75
75
  checkRights: true,
76
76
  dbObject: options?.dbObject,
77
77
  force: false,
@@ -0,0 +1,179 @@
1
+ import { NotFoundException } from '@nestjs/common';
2
+ import { GridFSBucket, GridFSBucketReadStreamOptions } from 'mongodb';
3
+ import { Connection, Types } from 'mongoose';
4
+ import { createBucket, MongoGridFSOptions, MongooseGridFS } from 'mongoose-gridfs';
5
+ import { FilterArgs } from '../../common/args/filter.args';
6
+ import { getObjectIds, getStringIds } from '../../common/helpers/db.helper';
7
+ import { convertFilterArgsToQuery } from '../../common/helpers/filter.helper';
8
+ import { check } from '../../common/helpers/input.helper';
9
+ import { prepareOutput } from '../../common/helpers/service.helper';
10
+ import { FileInfo } from './file-info.output';
11
+ import { FileServiceOptions } from './interfaces/file-service-options.interface';
12
+ import { FileUpload } from './interfaces/file-upload.interface';
13
+
14
+ /**
15
+ * Abstract core file service
16
+ */
17
+ export abstract class CoreFileService {
18
+ files: GridFSBucket & MongooseGridFS;
19
+
20
+ /**
21
+ * Include MongoDB connection and create File bucket
22
+ */
23
+ protected constructor(protected readonly connection: Connection, modelName = 'File') {
24
+ this.files = createBucket({ modelName, connection });
25
+ }
26
+
27
+ /**
28
+ * Save file in DB
29
+ */
30
+ createFile(file: FileUpload, serviceOptions?: FileServiceOptions): Promise<FileInfo> {
31
+ return new Promise(async (resolve, reject) => {
32
+ const { filename, mimetype, encoding, createReadStream } = file;
33
+ const readStream = createReadStream();
34
+ const options: MongoGridFSOptions = { filename, contentType: mimetype };
35
+ this.files.writeFile(options, readStream, (error, fileInfo) => {
36
+ error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
37
+ });
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Save files in DB
43
+ */
44
+ async createFiles(files: FileUpload[], serviceOptions?: FileServiceOptions): Promise<FileInfo[]> {
45
+ const promises: Promise<FileInfo>[] = [];
46
+ for (const file of files) {
47
+ promises.push(this.createFile(file, serviceOptions));
48
+ }
49
+ return await Promise.all(promises);
50
+ }
51
+
52
+ /**
53
+ * Get file infos via filter
54
+ */
55
+ findFileInfo(filterArgs?: FilterArgs, serviceOptions?: FileServiceOptions): Promise<FileInfo[]> {
56
+ return new Promise((resolve, reject) => {
57
+ const filterQuery = convertFilterArgsToQuery(filterArgs);
58
+ const cursor = this.files.find(filterQuery[0], filterQuery[1]);
59
+ if (!cursor) {
60
+ throw new Error('File collection not found');
61
+ }
62
+ cursor.toArray((error, docs) => {
63
+ error ? reject(error) : resolve(this.prepareOutput(docs, serviceOptions));
64
+ });
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Get info about file via file ID
70
+ */
71
+ getFileInfo(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<FileInfo> {
72
+ return new Promise((resolve, reject) => {
73
+ this.files.findById(getObjectIds(id), (error, fileInfo) => {
74
+ error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
75
+ });
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Get info about file via filename
81
+ */
82
+ getFileInfoByName(filename: string, serviceOptions?: FileServiceOptions): Promise<FileInfo> {
83
+ return new Promise((resolve, reject) => {
84
+ this.files.findOne({ filename }, (error, fileInfo) => {
85
+ error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
86
+ });
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Get file stream (for big files) via file ID
92
+ */
93
+ getFileStream(id: string | Types.ObjectId, options?: GridFSBucketReadStreamOptions) {
94
+ return this.files.openDownloadStream(getObjectIds(id), options);
95
+ }
96
+
97
+ /**
98
+ * Get file stream (for big files) via filename
99
+ */
100
+ getFileStreamByName(filename: string): GridFSBucketReadStreamOptions {
101
+ return this.files.readFile({ filename });
102
+ }
103
+
104
+ /**
105
+ * Get file buffer (for small files) via file ID
106
+ */
107
+ getBuffer(id: string | Types.ObjectId): Promise<Buffer> {
108
+ return new Promise((resolve, reject) => {
109
+ this.files.readFile({ _id: getObjectIds(id) }, (error, buffer) => {
110
+ error ? reject(error) : resolve(buffer);
111
+ });
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Get file buffer (for small files) via file ID
117
+ */
118
+ getBufferByName(filename: string): Promise<Buffer> {
119
+ return new Promise((resolve, reject) => {
120
+ this.files.readFile({ filename }, (error, buffer) => {
121
+ error ? reject(error) : resolve(buffer);
122
+ });
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Delete file reference of avatar
128
+ */
129
+ deleteFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<FileInfo> {
130
+ return new Promise((resolve, reject) => {
131
+ return this.files.unlink(getObjectIds(id), (error, fileInfo) => {
132
+ error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
133
+ });
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Delete file reference of avatar
139
+ */
140
+ async deleteFileByName(filename: string, serviceOptions?: FileServiceOptions): Promise<FileInfo> {
141
+ const fileInfo = await this.getFileInfoByName(filename);
142
+ if (!fileInfo) {
143
+ throw new NotFoundException('File not found with filename ' + filename);
144
+ }
145
+ return await this.deleteFile(fileInfo.id, serviceOptions);
146
+ }
147
+
148
+ // ===================================================================================================================
149
+ // Helper methods
150
+ // ===================================================================================================================
151
+
152
+ /**
153
+ * Prepare output before return
154
+ */
155
+ protected async prepareOutput(fileInfo: FileInfo | FileInfo[], options?: FileServiceOptions) {
156
+ if (!fileInfo) {
157
+ return fileInfo;
158
+ }
159
+ this.setId(fileInfo);
160
+ fileInfo = await prepareOutput(fileInfo, { targetModel: FileInfo });
161
+ return check(fileInfo, options?.currentUser, { roles: options?.roles });
162
+ }
163
+
164
+ /**
165
+ * Set file info ID via _id
166
+ */
167
+ protected setId(fileInfo: FileInfo | FileInfo[]) {
168
+ if (Array.isArray(fileInfo)) {
169
+ fileInfo.forEach((item) => {
170
+ if (typeof item === 'object') {
171
+ item.id = getStringIds(item._id);
172
+ }
173
+ });
174
+ } else if (typeof fileInfo === 'object') {
175
+ fileInfo.id = getStringIds(fileInfo._id);
176
+ }
177
+ return fileInfo;
178
+ }
179
+ }
@@ -0,0 +1,26 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+ import { Types } from 'mongoose';
3
+ import { CoreModel } from '../../common/models/core-model.model';
4
+
5
+ /**
6
+ * File info (output)
7
+ */
8
+ @ObjectType({ description: 'Information about attachment file' })
9
+ export class FileInfo extends CoreModel {
10
+ _id: Types.ObjectId;
11
+
12
+ @Field(() => String, { description: 'ID of the file in bytes' })
13
+ id: string = undefined;
14
+
15
+ @Field(() => Number, { description: 'Length of the file in bytes', nullable: true })
16
+ length: number = undefined;
17
+
18
+ @Field(() => Number, { description: 'Size of the chunk', nullable: true })
19
+ chunkSize: number = undefined;
20
+
21
+ @Field(() => String, { description: 'Name of the file', nullable: true })
22
+ filename?: string = undefined;
23
+
24
+ @Field(() => String, { description: 'Content type', nullable: true })
25
+ contentType?: string = undefined;
26
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Interface for service options in file services
3
+ */
4
+ export interface FileServiceOptions {
5
+ currentUser?: { id: any; hasRole: (roles: string[]) => boolean };
6
+ roles?: string | string[];
7
+ }
@@ -0,0 +1,38 @@
1
+ import { WriteStream } from 'fs-capacitor';
2
+ import { Readable } from 'stream';
3
+
4
+ /**
5
+ * Interface for file uploads
6
+ */
7
+ export interface FileUpload {
8
+ /**
9
+ * A private implementation detail that shouldn’t be used outside
10
+ */
11
+ capacitor: WriteStream;
12
+
13
+ /**
14
+ * A function that returns a FileUploadCreateReadStream.
15
+ */
16
+ createReadStream: (options?: {
17
+ /** Specify an encoding for the chunks, default: utf8 */
18
+ encoding?: 'utf8' | 'utf8' | 'ucs2' | 'utf16le' | 'latin1' | 'ascii' | 'base64' | 'base64url' | 'hex';
19
+
20
+ /** Maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource, default: 16384 */
21
+ highWaterMark?: number;
22
+ }) => Readable;
23
+
24
+ /**
25
+ * Stream transfer encoding of the file
26
+ */
27
+ encoding: string;
28
+
29
+ /**
30
+ * Name of the file
31
+ */
32
+ filename: string;
33
+
34
+ /**
35
+ * Mimetype of the file
36
+ */
37
+ mimetype: string;
38
+ }
@@ -4,7 +4,7 @@ import { APP_PIPE } from '@nestjs/core';
4
4
  import { GraphQLModule } from '@nestjs/graphql';
5
5
  import { MongooseModule } from '@nestjs/mongoose';
6
6
  import { Context } from 'apollo-server-core';
7
- import { graphqlUploadExpress } from 'graphql-upload';
7
+ import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
8
8
  import { merge } from './core/common/helpers/config.helper';
9
9
  import { IServerOptions } from './core/common/interfaces/server-options.interface';
10
10
  import { MapAndValidatePipe } from './core/common/pipes/map-and-validate.pipe';
package/src/index.ts CHANGED
@@ -78,6 +78,15 @@ export * from './core/modules/auth/core-auth.module';
78
78
  export * from './core/modules/auth/core-auth.resolver';
79
79
  export * from './core/modules/auth/jwt.strategy';
80
80
 
81
+ // =====================================================================================================================
82
+ // Core - Modules - File
83
+ // =====================================================================================================================
84
+
85
+ export * from './core/modules/file/interfaces/file-service-options.interface';
86
+ export * from './core/modules/file/interfaces/file-upload.interface';
87
+ export * from './core/modules/file/core-file.service';
88
+ export * from './core/modules/file/file-info.output';
89
+
81
90
  // =====================================================================================================================
82
91
  // Core - Modules - User
83
92
  // =====================================================================================================================
@@ -1,10 +1,24 @@
1
- import { Body, Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
1
+ import {
2
+ BadRequestException,
3
+ Body,
4
+ Controller,
5
+ Get,
6
+ NotFoundException,
7
+ Param,
8
+ Post,
9
+ Res,
10
+ UploadedFiles,
11
+ UseInterceptors,
12
+ } from '@nestjs/common';
2
13
  import { FilesInterceptor } from '@nestjs/platform-express';
3
14
  import { diskStorage } from 'multer';
4
15
  import envConfig from '../../../config.env';
16
+ import { RESTUser } from '../../../core/common/decorators/rest-user.decorator';
5
17
  import { Roles } from '../../../core/common/decorators/roles.decorator';
6
18
  import { RoleEnum } from '../../../core/common/enums/role.enum';
7
19
  import { multerRandomFileName } from '../../../core/common/helpers/file.helper';
20
+ import { User } from '../user/user.model';
21
+ import { FileService } from './file.service';
8
22
 
9
23
  /**
10
24
  * File controller for
@@ -12,7 +26,12 @@ import { multerRandomFileName } from '../../../core/common/helpers/file.helper';
12
26
  @Controller('files')
13
27
  export class FileController {
14
28
  /**
15
- * Upload files
29
+ * Include services
30
+ */
31
+ constructor(protected fileService: FileService) {}
32
+
33
+ /**
34
+ * Upload files via REST as an alternative to uploading via GraphQL (see file.resolver.ts)
16
35
  */
17
36
  @Roles(RoleEnum.ADMIN)
18
37
  @Post('upload')
@@ -31,7 +50,27 @@ export class FileController {
31
50
  }),
32
51
  })
33
52
  )
34
- uploadFile(@UploadedFiles() files, @Body() fields: any) {
35
- console.log(JSON.stringify({ files, fields }, null, 2));
53
+ uploadFiles(@UploadedFiles() files, @Body() fields: any) {
54
+ console.log('Saved file info', JSON.stringify({ files, fields }, null, 2));
55
+ }
56
+
57
+ /**
58
+ * Download file
59
+ */
60
+ @Roles(RoleEnum.ADMIN)
61
+ @Get(':filename')
62
+ async getFile(@Param('filename') filename: string, @Res() res, @RESTUser() user: User) {
63
+ if (!filename) {
64
+ throw new BadRequestException('Missing filename for download');
65
+ }
66
+
67
+ const file = await this.fileService.getFileInfoByName(filename);
68
+ if (!file) {
69
+ throw new NotFoundException('File not found');
70
+ }
71
+ const filestream = await this.fileService.getFileStream(file.id);
72
+ res.header('Content-Type', file.contentType);
73
+ res.header('Content-Disposition', 'attachment; filename=' + file.filename);
74
+ return filestream.pipe(res);
36
75
  }
37
76
  }