@lenne.tech/nest-server 8.6.8 → 8.6.9
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/dist/core/common/decorators/graphql-user.decorator.js +2 -3
- package/dist/core/common/decorators/graphql-user.decorator.js.map +1 -1
- package/dist/core/common/decorators/restricted.decorator.js +14 -13
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/helpers/context.helper.js +4 -5
- package/dist/core/common/helpers/context.helper.js.map +1 -1
- package/dist/core/common/helpers/db.helper.js +32 -21
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/file.helper.js +5 -1
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/filter.helper.js +16 -9
- package/dist/core/common/helpers/filter.helper.js.map +1 -1
- package/dist/core/common/helpers/graphql.helper.js +11 -8
- package/dist/core/common/helpers/graphql.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.js +17 -11
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/model.helper.d.ts +2 -0
- package/dist/core/common/helpers/model.helper.js +125 -3
- package/dist/core/common/helpers/model.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +27 -13
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/inputs/core-input.input.js +6 -1
- package/dist/core/common/inputs/core-input.input.js.map +1 -1
- package/dist/core/common/inputs/single-filter.input.d.ts +1 -0
- package/dist/core/common/inputs/single-filter.input.js +8 -0
- package/dist/core/common/inputs/single-filter.input.js.map +1 -1
- package/dist/core/common/models/core-model.model.js +14 -2
- package/dist/core/common/models/core-model.model.js.map +1 -1
- package/dist/core/common/pipes/check-input.pipe.js +1 -1
- package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
- package/dist/core/common/pipes/map-and-validate.pipe.js +2 -2
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/core/common/services/crud.service.js +3 -5
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/common/services/email.service.js +5 -1
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/mailjet.service.js +8 -1
- package/dist/core/common/services/mailjet.service.js.map +1 -1
- package/dist/core/common/services/module.service.js +29 -7
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +2 -2
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/auth.guard.js +4 -5
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +1 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +26 -0
- package/dist/core/modules/file/core-file.service.js +116 -0
- package/dist/core/modules/file/core-file.service.js.map +1 -0
- package/dist/core/modules/file/file-info.output.d.ts +10 -0
- package/dist/core/modules/file/file-info.output.js +49 -0
- package/dist/core/modules/file/file-info.output.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +7 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.js +3 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +13 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.js +3 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.js.map +1 -0
- package/dist/core/modules/user/core-user.service.js +7 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +32 -37
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/auth/auth.module.js +4 -1
- package/dist/server/modules/auth/auth.module.js.map +1 -1
- package/dist/server/modules/file/file.controller.d.ts +6 -1
- package/dist/server/modules/file/file.controller.js +33 -3
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.d.ts +8 -2
- package/dist/server/modules/file/file.resolver.js +43 -6
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +6 -0
- package/dist/server/modules/file/file.service.js +32 -0
- package/dist/server/modules/file/file.service.js.map +1 -0
- package/dist/server/modules/user/user.model.d.ts +1 -0
- package/dist/server/modules/user/user.model.js +5 -0
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.resolver.js +1 -2
- package/dist/server/modules/user/user.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.js +2 -2
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/server/server.module.js +2 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/test/test.helper.d.ts +21 -1
- package/dist/test/test.helper.js +99 -12
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/core/common/helpers/db.helper.ts +2 -0
- package/src/core/common/helpers/filter.helper.ts +7 -1
- package/src/core/common/helpers/model.helper.ts +152 -0
- package/src/core/common/inputs/single-filter.input.ts +10 -1
- package/src/core/common/services/module.service.ts +1 -1
- package/src/core/modules/file/core-file.service.ts +179 -0
- package/src/core/modules/file/file-info.output.ts +26 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +7 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +38 -0
- package/src/server/modules/file/file.controller.ts +42 -3
- package/src/server/modules/file/file.resolver.ts +57 -33
- package/src/server/modules/file/file.service.ts +11 -0
- package/src/server/modules/user/user.model.ts +9 -0
- package/src/server/server.module.ts +2 -1
- package/src/test/test.helper.ts +122 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "8.6.
|
|
3
|
+
"version": "8.6.9",
|
|
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",
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"light-my-request": "5.0.0",
|
|
80
80
|
"lodash": "4.17.21",
|
|
81
81
|
"mongodb": "4.7.0",
|
|
82
|
-
"mongoose": "6.3.
|
|
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",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"devDependencies": {
|
|
94
95
|
"@nestjs/testing": "8.4.7",
|
|
95
96
|
"@types/ejs": "3.1.1",
|
|
96
|
-
"@types/jest": "28.1.
|
|
97
|
+
"@types/jest": "28.1.3",
|
|
97
98
|
"@types/lodash": "4.14.182",
|
|
98
99
|
"@types/multer": "1.4.7",
|
|
99
100
|
"@types/node": "18.0.0",
|
|
@@ -101,10 +102,10 @@
|
|
|
101
102
|
"@types/nodemailer": "6.4.4",
|
|
102
103
|
"@types/passport": "1.0.9",
|
|
103
104
|
"@types/supertest": "2.0.12",
|
|
104
|
-
"@typescript-eslint/eslint-plugin": "5.
|
|
105
|
-
"@typescript-eslint/parser": "5.
|
|
105
|
+
"@typescript-eslint/eslint-plugin": "5.29.0",
|
|
106
|
+
"@typescript-eslint/parser": "5.29.0",
|
|
106
107
|
"coffeescript": "2.7.0",
|
|
107
|
-
"eslint": "8.
|
|
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",
|
|
@@ -122,7 +123,7 @@
|
|
|
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.
|
|
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,
|
|
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
|
-
*
|
|
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;
|
|
@@ -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,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
|
+
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
|
|
53
|
+
uploadFiles(@UploadedFiles() files, @Body() fields: any) {
|
|
35
54
|
console.log(JSON.stringify({ files, fields }, null, 2));
|
|
36
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);
|
|
75
|
+
}
|
|
37
76
|
}
|