@lenne.tech/nest-server 8.6.10 → 8.6.13
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/helpers/db.helper.d.ts +1 -1
- package/dist/core/common/helpers/db.helper.js +6 -2
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/interfaces/service-options.interface.d.ts +1 -1
- package/dist/core/common/types/field-selection.type.d.ts +1 -1
- package/dist/core/common/types/maybe-promise.type.d.ts +1 -0
- package/dist/core/common/types/maybe-promise.type.js +3 -0
- package/dist/core/common/types/maybe-promise.type.js.map +1 -0
- package/dist/core/modules/auth/core-auth.model.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.model.js +4 -0
- package/dist/core/modules/auth/core-auth.model.js.map +1 -1
- package/dist/core/modules/file/{file-info.output.d.ts → core-file-info.model.d.ts} +4 -3
- package/dist/core/modules/file/{file-info.output.js → core-file-info.model.js} +35 -20
- package/dist/core/modules/file/core-file-info.model.js.map +1 -0
- package/dist/core/modules/file/core-file.controller.d.ts +7 -0
- package/dist/core/modules/file/core-file.controller.js +52 -0
- package/dist/core/modules/file/core-file.controller.js.map +1 -0
- package/dist/core/modules/file/core-file.resolver.d.ts +10 -0
- package/dist/core/modules/file/core-file.resolver.js +70 -0
- package/dist/core/modules/file/core-file.resolver.js.map +1 -0
- package/dist/core/modules/file/core-file.service.d.ts +20 -15
- package/dist/core/modules/file/core-file.service.js +59 -22
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/user/core-user.model.d.ts +1 -0
- package/dist/core/modules/user/core-user.model.js +4 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core.module.js +1 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/server/common/models/persistence.model.d.ts +3 -3
- package/dist/server/common/models/persistence.model.js +8 -4
- package/dist/server/common/models/persistence.model.js.map +1 -1
- package/dist/server/modules/auth/auth.model.d.ts +1 -0
- package/dist/server/modules/auth/auth.model.js +5 -0
- package/dist/server/modules/auth/auth.model.js.map +1 -1
- package/dist/server/modules/file/file-info.model.d.ts +26 -0
- package/dist/server/modules/file/file-info.model.js +21 -0
- package/dist/server/modules/file/file-info.model.js.map +1 -0
- package/dist/server/modules/file/file.controller.d.ts +2 -3
- package/dist/server/modules/file/file.controller.js +4 -26
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.module.d.ts +2 -0
- package/dist/server/modules/file/file.module.js +27 -0
- package/dist/server/modules/file/file.module.js.map +1 -0
- package/dist/server/modules/file/file.resolver.d.ts +3 -4
- package/dist/server/modules/file/file.resolver.js +4 -5
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.js +1 -1
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/inputs/user-create.input.js.map +1 -1
- package/dist/server/modules/user/inputs/user.input.js.map +1 -1
- package/dist/server/modules/user/user.model.d.ts +3 -3
- package/dist/server/modules/user/user.model.js +4 -4
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/server.module.js +4 -6
- package/dist/server/server.module.js.map +1 -1
- package/dist/test/test.helper.js +1 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/helpers/db.helper.ts +7 -3
- package/src/core/common/interfaces/service-options.interface.ts +1 -1
- package/src/core/common/types/field-selection.type.ts +6 -1
- package/src/core/common/types/maybe-promise.type.ts +4 -0
- package/src/core/modules/auth/core-auth.model.ts +11 -1
- package/src/core/modules/file/core-file-info.model.ts +40 -0
- package/src/core/modules/file/core-file.controller.ts +34 -0
- package/src/core/modules/file/core-file.resolver.ts +56 -0
- package/src/core/modules/file/core-file.service.ts +81 -27
- package/src/core/modules/user/core-user.model.ts +10 -0
- package/src/core.module.ts +1 -2
- package/src/index.ts +4 -1
- package/src/server/common/models/persistence.model.ts +25 -11
- package/src/server/modules/auth/auth.model.ts +9 -0
- package/src/server/modules/file/file-info.model.ts +15 -0
- package/src/server/modules/file/file.controller.ts +8 -37
- package/src/server/modules/file/file.module.ts +17 -0
- package/src/server/modules/file/file.resolver.ts +1 -4
- package/src/server/modules/file/file.service.ts +4 -1
- package/src/server/modules/user/inputs/user-create.input.ts +0 -4
- package/src/server/modules/user/inputs/user.input.ts +0 -4
- package/src/server/modules/user/user.model.ts +8 -8
- package/src/server/server.module.ts +6 -8
- package/src/test/test.helper.ts +1 -1
- package/dist/core/modules/file/file-info.output.js.map +0 -1
- package/src/core/modules/file/file-info.output.ts +0 -26
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.13",
|
|
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",
|
|
@@ -543,7 +543,7 @@ export function removeIds(source: any[], ids: StringOrObjectId | StringOrObjectI
|
|
|
543
543
|
*/
|
|
544
544
|
export async function setPopulates<T = Query<any, any> | Document>(
|
|
545
545
|
queryOrDocument: T,
|
|
546
|
-
populateOptions: PopulateOptions[],
|
|
546
|
+
populateOptions: string[] | PopulateOptions[] | (string | PopulateOptions)[],
|
|
547
547
|
modelSchemaPaths?: { [key: string]: SchemaType }
|
|
548
548
|
): Promise<T> {
|
|
549
549
|
// Check parameters
|
|
@@ -553,8 +553,12 @@ export async function setPopulates<T = Query<any, any> | Document>(
|
|
|
553
553
|
|
|
554
554
|
// Filter populate options via model schema paths
|
|
555
555
|
if (modelSchemaPaths) {
|
|
556
|
-
populateOptions = populateOptions.filter((
|
|
557
|
-
|
|
556
|
+
populateOptions = populateOptions.filter((option: string | PopulateOptions) => {
|
|
557
|
+
let key: string = option as string;
|
|
558
|
+
if ((option as PopulateOptions)?.path) {
|
|
559
|
+
key = (option as PopulateOptions)?.path;
|
|
560
|
+
}
|
|
561
|
+
return Object.keys(modelSchemaPaths).includes(key);
|
|
558
562
|
});
|
|
559
563
|
}
|
|
560
564
|
|
|
@@ -35,7 +35,7 @@ export interface ServiceOptions {
|
|
|
35
35
|
outputType?: new (...params: any[]) => any;
|
|
36
36
|
|
|
37
37
|
// Alias for fieldSelection (if both are set fieldSelection is overwritten by populate)
|
|
38
|
-
populate?: PopulateOptions | (PopulateOptions | string)[];
|
|
38
|
+
populate?: string | PopulateOptions | (PopulateOptions | string)[];
|
|
39
39
|
|
|
40
40
|
// Process field selection
|
|
41
41
|
// If {} or not set, then the field selection runs with defaults
|
|
@@ -5,4 +5,9 @@ import { ResolveSelector } from '../interfaces/resolve-selector.interface';
|
|
|
5
5
|
/**
|
|
6
6
|
* Field selection to set fields of (populated) result
|
|
7
7
|
*/
|
|
8
|
-
export type FieldSelection =
|
|
8
|
+
export type FieldSelection =
|
|
9
|
+
| string
|
|
10
|
+
| PopulateOptions
|
|
11
|
+
| (PopulateOptions | string)[]
|
|
12
|
+
| SelectionNode[]
|
|
13
|
+
| ResolveSelector;
|
|
@@ -17,7 +17,7 @@ export class CoreAuthModel extends CoreModel {
|
|
|
17
17
|
token: string = undefined;
|
|
18
18
|
|
|
19
19
|
// ===================================================================================================================
|
|
20
|
-
//
|
|
20
|
+
// Methods
|
|
21
21
|
// ===================================================================================================================
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -28,4 +28,14 @@ export class CoreAuthModel extends CoreModel {
|
|
|
28
28
|
// Nothing more to initialize yet
|
|
29
29
|
return this;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Map input
|
|
34
|
+
*/
|
|
35
|
+
map(input) {
|
|
36
|
+
super.map(input);
|
|
37
|
+
// There is nothing to map yet. Non-primitive variables should always be mapped.
|
|
38
|
+
// If something comes up, you can use `mapClasses` / `mapClassesAsync` from ModelHelper.
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
31
41
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Field, ObjectType } from '@nestjs/graphql';
|
|
2
|
+
import { Prop } from '@nestjs/mongoose';
|
|
3
|
+
import { Types } from 'mongoose';
|
|
4
|
+
import { CoreModel } from '../../common/models/core-model.model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File info
|
|
8
|
+
*/
|
|
9
|
+
@ObjectType({ description: 'Information about file' })
|
|
10
|
+
export class CoreFileInfo extends CoreModel {
|
|
11
|
+
_id: Types.ObjectId;
|
|
12
|
+
|
|
13
|
+
@Field(() => String, { description: 'ID of the file' })
|
|
14
|
+
id: string = undefined;
|
|
15
|
+
|
|
16
|
+
@Field(() => Number, {
|
|
17
|
+
description:
|
|
18
|
+
'The size of each chunk in bytes. GridFS divides the document into chunks of size chunkSize, ' +
|
|
19
|
+
'except for the last, which is only as large as needed. The default size is 255 kilobytes (kB)',
|
|
20
|
+
nullable: true,
|
|
21
|
+
})
|
|
22
|
+
@Prop({ type: Number, required: false })
|
|
23
|
+
chunkSize: number = undefined;
|
|
24
|
+
|
|
25
|
+
@Field(() => String, { description: 'Content type', nullable: true })
|
|
26
|
+
@Prop({ type: String, required: false })
|
|
27
|
+
contentType?: string = undefined;
|
|
28
|
+
|
|
29
|
+
@Field(() => String, { description: 'Name of the file', nullable: true })
|
|
30
|
+
@Prop({ type: String, required: false })
|
|
31
|
+
filename?: string = undefined;
|
|
32
|
+
|
|
33
|
+
@Field(() => Number, { description: 'The size of the document in bytes', nullable: true })
|
|
34
|
+
@Prop({ type: Number, required: false })
|
|
35
|
+
length: number = undefined;
|
|
36
|
+
|
|
37
|
+
@Field(() => Date, { description: 'The date the file was first stored', nullable: true })
|
|
38
|
+
@Prop({ type: Date, required: false })
|
|
39
|
+
uploadDate: Date = undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BadRequestException, Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
|
|
2
|
+
import { User } from '../../../server/modules/user/user.model';
|
|
3
|
+
import { RESTUser } from '../../common/decorators/rest-user.decorator';
|
|
4
|
+
import { CoreFileService } from './core-file.service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File controller
|
|
8
|
+
*/
|
|
9
|
+
@Controller('files')
|
|
10
|
+
export abstract class CoreFileController {
|
|
11
|
+
/**
|
|
12
|
+
* Include services
|
|
13
|
+
*/
|
|
14
|
+
protected constructor(protected fileService: CoreFileService) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Download file
|
|
18
|
+
*/
|
|
19
|
+
@Get(':filename')
|
|
20
|
+
async getFile(@Param('filename') filename: string, @Res() res, @RESTUser() user: User) {
|
|
21
|
+
if (!filename) {
|
|
22
|
+
throw new BadRequestException('Missing filename for download');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const file = await this.fileService.getFileInfoByName(filename);
|
|
26
|
+
if (!file) {
|
|
27
|
+
throw new NotFoundException('File not found');
|
|
28
|
+
}
|
|
29
|
+
const filestream = await this.fileService.getFileStream(file.id);
|
|
30
|
+
res.header('Content-Type', file.contentType);
|
|
31
|
+
res.header('Content-Disposition', 'attachment; filename=' + file.filename);
|
|
32
|
+
return filestream.pipe(res);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
2
|
+
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
|
|
3
|
+
import { CoreFileInfo } from './core-file-info.model';
|
|
4
|
+
import { CoreFileService } from './core-file.service';
|
|
5
|
+
import { FileUpload } from './interfaces/file-upload.interface';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* File resolver
|
|
9
|
+
*/
|
|
10
|
+
@Resolver()
|
|
11
|
+
export class CoreFileResolver {
|
|
12
|
+
/**
|
|
13
|
+
* Integrate services
|
|
14
|
+
*/
|
|
15
|
+
constructor(protected readonly fileService: CoreFileService) {}
|
|
16
|
+
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
// Queries
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get file info
|
|
23
|
+
*/
|
|
24
|
+
@Query(() => CoreFileInfo, { nullable: true })
|
|
25
|
+
async getFileInfo(@Args({ name: 'filename', type: () => String }) filename: string): Promise<any> {
|
|
26
|
+
return await this.fileService.getFileInfoByName(filename);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ===========================================================================
|
|
30
|
+
// Mutations
|
|
31
|
+
// ===========================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Delete file
|
|
35
|
+
*/
|
|
36
|
+
@Mutation(() => CoreFileInfo)
|
|
37
|
+
async deleteFile(@Args({ name: 'filename', type: () => String }) filename: string): Promise<any> {
|
|
38
|
+
return await this.fileService.deleteFileByName(filename);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Upload file
|
|
43
|
+
*/
|
|
44
|
+
@Mutation(() => CoreFileInfo)
|
|
45
|
+
async uploadFile(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload): Promise<any> {
|
|
46
|
+
return await this.fileService.createFile(file);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Upload files
|
|
51
|
+
*/
|
|
52
|
+
@Mutation(() => [CoreFileInfo])
|
|
53
|
+
async uploadFiles(@Args({ name: 'files', type: () => [GraphQLUpload] }) files: FileUpload[]): Promise<any> {
|
|
54
|
+
return await this.fileService.createFiles(files);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -7,10 +7,16 @@ import { getObjectIds, getStringIds } from '../../common/helpers/db.helper';
|
|
|
7
7
|
import { convertFilterArgsToQuery } from '../../common/helpers/filter.helper';
|
|
8
8
|
import { check } from '../../common/helpers/input.helper';
|
|
9
9
|
import { prepareOutput } from '../../common/helpers/service.helper';
|
|
10
|
-
import {
|
|
10
|
+
import { MaybePromise } from '../../common/types/maybe-promise.type';
|
|
11
|
+
import { CoreFileInfo } from './core-file-info.model';
|
|
11
12
|
import { FileServiceOptions } from './interfaces/file-service-options.interface';
|
|
12
13
|
import { FileUpload } from './interfaces/file-upload.interface';
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Type for checking input
|
|
17
|
+
*/
|
|
18
|
+
export type FileInputCheckType = 'file' | 'files' | 'filterArgs' | 'id' | 'filename';
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Abstract core file service
|
|
16
22
|
*/
|
|
@@ -20,16 +26,19 @@ export abstract class CoreFileService {
|
|
|
20
26
|
/**
|
|
21
27
|
* Include MongoDB connection and create File bucket
|
|
22
28
|
*/
|
|
23
|
-
protected constructor(protected readonly connection: Connection,
|
|
24
|
-
this.files = createBucket({
|
|
29
|
+
protected constructor(protected readonly connection: Connection, bucketName = 'fs') {
|
|
30
|
+
this.files = createBucket({ bucketName, connection });
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Save file in DB
|
|
29
35
|
*/
|
|
30
|
-
createFile(file: FileUpload
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
async createFile(file: MaybePromise<FileUpload>, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
37
|
+
if (!(await this.checkRights(file, { ...serviceOptions, checkInputType: 'file' }))) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return await new Promise(async (resolve, reject) => {
|
|
41
|
+
const { filename, mimetype, encoding, createReadStream } = await file;
|
|
33
42
|
const readStream = createReadStream();
|
|
34
43
|
const options: MongoGridFSOptions = { filename, contentType: mimetype };
|
|
35
44
|
this.files.writeFile(options, readStream, (error, fileInfo) => {
|
|
@@ -41,8 +50,12 @@ export abstract class CoreFileService {
|
|
|
41
50
|
/**
|
|
42
51
|
* Save files in DB
|
|
43
52
|
*/
|
|
44
|
-
async createFiles(files: FileUpload[], serviceOptions?: FileServiceOptions): Promise<
|
|
45
|
-
|
|
53
|
+
async createFiles(files: MaybePromise<FileUpload>[], serviceOptions?: FileServiceOptions): Promise<CoreFileInfo[]> {
|
|
54
|
+
if (!(await this.checkRights(files, { ...serviceOptions, checkInputType: 'files' }))) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
console.log(files);
|
|
58
|
+
const promises: Promise<CoreFileInfo>[] = [];
|
|
46
59
|
for (const file of files) {
|
|
47
60
|
promises.push(this.createFile(file, serviceOptions));
|
|
48
61
|
}
|
|
@@ -52,8 +65,11 @@ export abstract class CoreFileService {
|
|
|
52
65
|
/**
|
|
53
66
|
* Get file infos via filter
|
|
54
67
|
*/
|
|
55
|
-
findFileInfo(filterArgs?: FilterArgs, serviceOptions?: FileServiceOptions): Promise<
|
|
56
|
-
|
|
68
|
+
async findFileInfo(filterArgs?: FilterArgs, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo[]> {
|
|
69
|
+
if (!(await this.checkRights(filterArgs, { ...serviceOptions, checkInputType: 'filterArgs' }))) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return await new Promise((resolve, reject) => {
|
|
57
73
|
const filterQuery = convertFilterArgsToQuery(filterArgs);
|
|
58
74
|
const cursor = this.files.find(filterQuery[0], filterQuery[1]);
|
|
59
75
|
if (!cursor) {
|
|
@@ -68,8 +84,11 @@ export abstract class CoreFileService {
|
|
|
68
84
|
/**
|
|
69
85
|
* Get info about file via file ID
|
|
70
86
|
*/
|
|
71
|
-
getFileInfo(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<
|
|
72
|
-
|
|
87
|
+
async getFileInfo(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
88
|
+
if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return await new Promise((resolve, reject) => {
|
|
73
92
|
this.files.findById(getObjectIds(id), (error, fileInfo) => {
|
|
74
93
|
error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
|
|
75
94
|
});
|
|
@@ -79,8 +98,11 @@ export abstract class CoreFileService {
|
|
|
79
98
|
/**
|
|
80
99
|
* Get info about file via filename
|
|
81
100
|
*/
|
|
82
|
-
getFileInfoByName(filename: string, serviceOptions?: FileServiceOptions): Promise<
|
|
83
|
-
|
|
101
|
+
async getFileInfoByName(filename: string, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
102
|
+
if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return await new Promise((resolve, reject) => {
|
|
84
106
|
this.files.findOne({ filename }, (error, fileInfo) => {
|
|
85
107
|
error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
|
|
86
108
|
});
|
|
@@ -90,22 +112,34 @@ export abstract class CoreFileService {
|
|
|
90
112
|
/**
|
|
91
113
|
* Get file stream (for big files) via file ID
|
|
92
114
|
*/
|
|
93
|
-
getFileStream(id: string | Types.ObjectId,
|
|
94
|
-
|
|
115
|
+
async getFileStream(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
|
|
116
|
+
if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return this.files.openDownloadStream(getObjectIds(id));
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
/**
|
|
98
123
|
* Get file stream (for big files) via filename
|
|
99
124
|
*/
|
|
100
|
-
getFileStreamByName(
|
|
125
|
+
async getFileStreamByName(
|
|
126
|
+
filename: string,
|
|
127
|
+
serviceOptions?: FileServiceOptions
|
|
128
|
+
): Promise<GridFSBucketReadStreamOptions> {
|
|
129
|
+
if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
101
132
|
return this.files.readFile({ filename });
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
/**
|
|
105
136
|
* Get file buffer (for small files) via file ID
|
|
106
137
|
*/
|
|
107
|
-
getBuffer(id: string | Types.ObjectId): Promise<Buffer> {
|
|
108
|
-
|
|
138
|
+
async getBuffer(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<Buffer> {
|
|
139
|
+
if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return await new Promise((resolve, reject) => {
|
|
109
143
|
this.files.readFile({ _id: getObjectIds(id) }, (error, buffer) => {
|
|
110
144
|
error ? reject(error) : resolve(buffer);
|
|
111
145
|
});
|
|
@@ -115,8 +149,11 @@ export abstract class CoreFileService {
|
|
|
115
149
|
/**
|
|
116
150
|
* Get file buffer (for small files) via file ID
|
|
117
151
|
*/
|
|
118
|
-
getBufferByName(filename: string): Promise<Buffer> {
|
|
119
|
-
|
|
152
|
+
async getBufferByName(filename: string, serviceOptions?: FileServiceOptions): Promise<Buffer> {
|
|
153
|
+
if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return await new Promise((resolve, reject) => {
|
|
120
157
|
this.files.readFile({ filename }, (error, buffer) => {
|
|
121
158
|
error ? reject(error) : resolve(buffer);
|
|
122
159
|
});
|
|
@@ -126,8 +163,11 @@ export abstract class CoreFileService {
|
|
|
126
163
|
/**
|
|
127
164
|
* Delete file reference of avatar
|
|
128
165
|
*/
|
|
129
|
-
deleteFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<
|
|
130
|
-
|
|
166
|
+
async deleteFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
167
|
+
if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return await new Promise((resolve, reject) => {
|
|
131
171
|
return this.files.unlink(getObjectIds(id), (error, fileInfo) => {
|
|
132
172
|
error ? reject(error) : resolve(this.prepareOutput(fileInfo, serviceOptions));
|
|
133
173
|
});
|
|
@@ -137,7 +177,10 @@ export abstract class CoreFileService {
|
|
|
137
177
|
/**
|
|
138
178
|
* Delete file reference of avatar
|
|
139
179
|
*/
|
|
140
|
-
async deleteFileByName(filename: string, serviceOptions?: FileServiceOptions): Promise<
|
|
180
|
+
async deleteFileByName(filename: string, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
|
|
181
|
+
if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
141
184
|
const fileInfo = await this.getFileInfoByName(filename);
|
|
142
185
|
if (!fileInfo) {
|
|
143
186
|
throw new NotFoundException('File not found with filename ' + filename);
|
|
@@ -149,22 +192,33 @@ export abstract class CoreFileService {
|
|
|
149
192
|
// Helper methods
|
|
150
193
|
// ===================================================================================================================
|
|
151
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Check rights before processing file handling
|
|
197
|
+
* Can throw an exception if the rights do not fit
|
|
198
|
+
*/
|
|
199
|
+
protected checkRights(
|
|
200
|
+
input: any,
|
|
201
|
+
options?: FileServiceOptions & { checkInputType: FileInputCheckType }
|
|
202
|
+
): MaybePromise<boolean> {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
152
206
|
/**
|
|
153
207
|
* Prepare output before return
|
|
154
208
|
*/
|
|
155
|
-
protected async prepareOutput(fileInfo:
|
|
209
|
+
protected async prepareOutput(fileInfo: CoreFileInfo | CoreFileInfo[], options?: FileServiceOptions) {
|
|
156
210
|
if (!fileInfo) {
|
|
157
211
|
return fileInfo;
|
|
158
212
|
}
|
|
159
213
|
this.setId(fileInfo);
|
|
160
|
-
fileInfo = await prepareOutput(fileInfo, { targetModel:
|
|
214
|
+
fileInfo = await prepareOutput(fileInfo, { targetModel: CoreFileInfo });
|
|
161
215
|
return check(fileInfo, options?.currentUser, { roles: options?.roles });
|
|
162
216
|
}
|
|
163
217
|
|
|
164
218
|
/**
|
|
165
219
|
* Set file info ID via _id
|
|
166
220
|
*/
|
|
167
|
-
protected setId(fileInfo:
|
|
221
|
+
protected setId(fileInfo: CoreFileInfo | CoreFileInfo[]) {
|
|
168
222
|
if (Array.isArray(fileInfo)) {
|
|
169
223
|
fileInfo.forEach((item) => {
|
|
170
224
|
if (typeof item === 'object') {
|
|
@@ -122,4 +122,14 @@ export abstract class CoreUserModel extends CorePersistenceModel {
|
|
|
122
122
|
this.roles = this.roles === undefined ? [] : this.roles;
|
|
123
123
|
return this;
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Map input
|
|
128
|
+
*/
|
|
129
|
+
map(input) {
|
|
130
|
+
super.map(input);
|
|
131
|
+
// There is nothing to map yet. Non-primitive variables should always be mapped.
|
|
132
|
+
// If something comes up, you can use `mapClasses` / `mapClassesAsync` from ModelHelper.
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
125
135
|
}
|
package/src/core.module.ts
CHANGED
|
@@ -79,8 +79,7 @@ export class CoreModule implements NestModule {
|
|
|
79
79
|
const { connectionParams, extra } = context;
|
|
80
80
|
if (config.graphQl.enableSubscriptionAuth) {
|
|
81
81
|
// get authToken from authorization header
|
|
82
|
-
const authToken: string =
|
|
83
|
-
'Authorization' in connectionParams && connectionParams?.Authorization?.split(' ')[1];
|
|
82
|
+
const authToken: string = connectionParams?.Authorization?.split(' ')[1];
|
|
84
83
|
if (authToken) {
|
|
85
84
|
// verify authToken/getJwtPayLoad
|
|
86
85
|
const payload = authService.decodeJwt(authToken);
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export * from './core/common/services/template.service';
|
|
|
56
56
|
export * from './core/common/types/core-model-constructor.type';
|
|
57
57
|
export * from './core/common/types/field-selection.type';
|
|
58
58
|
export * from './core/common/types/ids.type';
|
|
59
|
+
export * from './core/common/types/maybe-promise.type';
|
|
59
60
|
export * from './core/common/types/plain-input.type';
|
|
60
61
|
export * from './core/common/types/require-only-one.type';
|
|
61
62
|
export * from './core/common/types/required-at-least-one.type';
|
|
@@ -84,8 +85,10 @@ export * from './core/modules/auth/jwt.strategy';
|
|
|
84
85
|
|
|
85
86
|
export * from './core/modules/file/interfaces/file-service-options.interface';
|
|
86
87
|
export * from './core/modules/file/interfaces/file-upload.interface';
|
|
88
|
+
export * from './core/modules/file/core-file.controller';
|
|
89
|
+
export * from './core/modules/file/core-file.resolver';
|
|
87
90
|
export * from './core/modules/file/core-file.service';
|
|
88
|
-
export * from './core/modules/file/file-info.
|
|
91
|
+
export * from './core/modules/file/core-file-info.model';
|
|
89
92
|
|
|
90
93
|
// =====================================================================================================================
|
|
91
94
|
// Core - Modules - User
|
|
@@ -15,33 +15,37 @@ import { User } from '../../modules/user/user.model';
|
|
|
15
15
|
isAbstract: true,
|
|
16
16
|
})
|
|
17
17
|
export abstract class PersistenceModel extends CorePersistenceModel {
|
|
18
|
+
// ===================================================================================================================
|
|
19
|
+
// Properties
|
|
20
|
+
// ===================================================================================================================
|
|
21
|
+
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
23
|
+
* ID of the user who created the object
|
|
20
24
|
*
|
|
21
25
|
* Not set when created by system
|
|
22
26
|
*/
|
|
23
|
-
@Field((
|
|
24
|
-
description: '
|
|
27
|
+
@Field(() => User, {
|
|
28
|
+
description: 'ID of the user who created the object',
|
|
25
29
|
nullable: true,
|
|
26
30
|
})
|
|
27
31
|
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
|
|
28
|
-
createdBy?: Types.ObjectId |
|
|
32
|
+
createdBy?: Types.ObjectId | string = undefined;
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
|
-
*
|
|
35
|
+
* ID of the user who updated the object
|
|
32
36
|
*
|
|
33
37
|
* Not set when updated by system
|
|
34
38
|
*/
|
|
35
|
-
@Field((
|
|
36
|
-
description: '
|
|
39
|
+
@Field(() => User, {
|
|
40
|
+
description: 'ID of the user who updated the object',
|
|
37
41
|
nullable: true,
|
|
38
42
|
})
|
|
39
43
|
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
|
|
40
|
-
updatedBy?: Types.ObjectId |
|
|
44
|
+
updatedBy?: Types.ObjectId | string = undefined;
|
|
41
45
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
46
|
+
// ===================================================================================================================
|
|
47
|
+
// Methods
|
|
48
|
+
// ===================================================================================================================
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Initialize instance with default values instead of undefined
|
|
@@ -51,4 +55,14 @@ export abstract class PersistenceModel extends CorePersistenceModel {
|
|
|
51
55
|
// Nothing more to initialize yet
|
|
52
56
|
return this;
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Map input
|
|
61
|
+
*/
|
|
62
|
+
map(input) {
|
|
63
|
+
super.map(input);
|
|
64
|
+
// There is nothing to map yet. Non-primitive variables should always be mapped.
|
|
65
|
+
// If something comes up, you can use `mapClasses` / `mapClassesAsync` from ModelHelper.
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
54
68
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Field, ObjectType } from '@nestjs/graphql';
|
|
2
|
+
import { mapClasses } from '../../../core/common/helpers/model.helper';
|
|
2
3
|
import { CoreAuthModel } from '../../../core/modules/auth/core-auth.model';
|
|
3
4
|
import { User } from '../user/user.model';
|
|
4
5
|
|
|
@@ -29,4 +30,12 @@ export class Auth extends CoreAuthModel {
|
|
|
29
30
|
// Nothing more to initialize yet
|
|
30
31
|
return this;
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Map input
|
|
36
|
+
*/
|
|
37
|
+
map(input) {
|
|
38
|
+
super.map(input);
|
|
39
|
+
return mapClasses(input, { user: User }, this);
|
|
40
|
+
}
|
|
32
41
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ObjectType } from '@nestjs/graphql';
|
|
2
|
+
import { Schema as MongooseSchema, SchemaFactory } from '@nestjs/mongoose';
|
|
3
|
+
import { CoreFileInfo } from '../../../core/modules/file/core-file-info.model';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File info model
|
|
7
|
+
*/
|
|
8
|
+
@ObjectType({ description: 'Information about file' })
|
|
9
|
+
@MongooseSchema({ collection: 'fs.files' })
|
|
10
|
+
export class FileInfo extends CoreFileInfo {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* File info schema
|
|
14
|
+
*/
|
|
15
|
+
export const FileInfoSchema = SchemaFactory.createForClass(FileInfo);
|
|
@@ -1,34 +1,25 @@
|
|
|
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';
|
|
1
|
+
import { Body, Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
|
13
2
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
|
14
3
|
import { diskStorage } from 'multer';
|
|
15
4
|
import envConfig from '../../../config.env';
|
|
16
|
-
import { RESTUser } from '../../../core/common/decorators/rest-user.decorator';
|
|
17
5
|
import { Roles } from '../../../core/common/decorators/roles.decorator';
|
|
18
6
|
import { RoleEnum } from '../../../core/common/enums/role.enum';
|
|
19
7
|
import { multerRandomFileName } from '../../../core/common/helpers/file.helper';
|
|
20
|
-
import {
|
|
8
|
+
import { CoreFileController } from '../../../core/modules/file/core-file.controller';
|
|
21
9
|
import { FileService } from './file.service';
|
|
22
10
|
|
|
23
11
|
/**
|
|
24
|
-
* File controller
|
|
12
|
+
* File controller
|
|
25
13
|
*/
|
|
14
|
+
@Roles(RoleEnum.S_USER)
|
|
26
15
|
@Controller('files')
|
|
27
|
-
export class FileController {
|
|
16
|
+
export class FileController extends CoreFileController {
|
|
28
17
|
/**
|
|
29
18
|
* Include services
|
|
30
19
|
*/
|
|
31
|
-
constructor(protected fileService: FileService) {
|
|
20
|
+
constructor(protected fileService: FileService) {
|
|
21
|
+
super(fileService);
|
|
22
|
+
}
|
|
32
23
|
|
|
33
24
|
/**
|
|
34
25
|
* Upload files via REST as an alternative to uploading via GraphQL (see file.resolver.ts)
|
|
@@ -53,24 +44,4 @@ export class FileController {
|
|
|
53
44
|
uploadFiles(@UploadedFiles() files, @Body() fields: any) {
|
|
54
45
|
console.log('Saved file info', JSON.stringify({ files, fields }, null, 2));
|
|
55
46
|
}
|
|
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
|
-
}
|
|
76
47
|
}
|