@lenne.tech/nest-server 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/config.env.js +3 -2
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/restricted.decorator.d.ts +2 -0
  4. package/dist/core/common/decorators/restricted.decorator.js +10 -7
  5. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  6. package/dist/core/common/helpers/input.helper.d.ts +12 -2
  7. package/dist/core/common/helpers/input.helper.js +50 -11
  8. package/dist/core/common/helpers/input.helper.js.map +1 -1
  9. package/dist/core/common/interfaces/service-options.interface.d.ts +6 -0
  10. package/dist/core/common/models/core-model.model.d.ts +1 -1
  11. package/dist/core/common/models/core-model.model.js.map +1 -1
  12. package/dist/core/common/pipes/check-input.pipe.js +1 -1
  13. package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
  14. package/dist/core/common/services/crud.service.js +17 -19
  15. package/dist/core/common/services/crud.service.js.map +1 -1
  16. package/dist/core/common/services/module.service.d.ts +13 -1
  17. package/dist/core/common/services/module.service.js +39 -4
  18. package/dist/core/common/services/module.service.js.map +1 -1
  19. package/dist/core/common/types/ids.type.d.ts +8 -0
  20. package/dist/core/common/types/ids.type.js +3 -0
  21. package/dist/core/common/types/ids.type.js.map +1 -0
  22. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  23. package/dist/core/modules/user/core-user.service.js +38 -35
  24. package/dist/core/modules/user/core-user.service.js.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/server/modules/user/user.model.d.ts +1 -1
  29. package/dist/server/modules/user/user.resolver.d.ts +2 -2
  30. package/dist/server/modules/user/user.resolver.js +30 -14
  31. package/dist/server/modules/user/user.resolver.js.map +1 -1
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +37 -37
  34. package/src/config.env.ts +3 -2
  35. package/src/core/common/decorators/restricted.decorator.ts +16 -10
  36. package/src/core/common/helpers/input.helper.ts +63 -14
  37. package/src/core/common/interfaces/service-options.interface.ts +19 -1
  38. package/src/core/common/models/core-model.model.ts +1 -1
  39. package/src/core/common/pipes/check-input.pipe.ts +1 -1
  40. package/src/core/common/services/crud.service.ts +17 -22
  41. package/src/core/common/services/module.service.ts +75 -9
  42. package/src/core/common/types/ids.type.ts +7 -0
  43. package/src/core/modules/auth/guards/roles.guard.ts +1 -1
  44. package/src/core/modules/user/core-user.service.ts +42 -44
  45. package/src/index.ts +1 -0
  46. package/src/server/modules/user/user.resolver.ts +26 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
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",
@@ -20,7 +20,7 @@
20
20
  "format:staged": "pretty-quick --staged",
21
21
  "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
22
22
  "prestart:prod": "npm run build",
23
- "reinit": "rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i && npm run test:e2e",
23
+ "reinit": "rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i && npm run test:e2e && npm run build",
24
24
  "reinit:force": "rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i --force && npm run test:e2e",
25
25
  "reinit:legacy": "rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i --legacy-peer-deps && npm run test:e2e",
26
26
  "start": "./node_modules/.bin/grunt",
@@ -29,13 +29,13 @@
29
29
  "start:nodemon": "ts-node -r tsconfig-paths/register src/main.ts",
30
30
  "start:debug": "nodemon --config nodemon-debug.json",
31
31
  "start:dev": "nodemon",
32
- "test": "jest",
33
- "test:cov": "jest --coverage",
34
- "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
35
- "test:e2e": "jest --config jest-e2e.json",
36
- "test:e2e-cov": "jest --config jest-e2e.json --coverage",
37
- "test:ci": "jest --config jest-e2e.json --ci",
38
- "test:watch": "jest --watch",
32
+ "test": "NODE_ENV=local jest",
33
+ "test:cov": "NODE_ENV=local jest --coverage",
34
+ "test:debug": "NODE_ENV=local node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
35
+ "test:e2e": "NODE_ENV=local jest --config jest-e2e.json",
36
+ "test:e2e-cov": "NODE_ENV=local jest --config jest-e2e.json --coverage",
37
+ "test:ci": "NODE_ENV=local jest --config jest-e2e.json --ci",
38
+ "test:watch": "NODE_ENV=local jest --watch",
39
39
  "prepack": "npm run prestart:prod",
40
40
  "prepare": "husky install",
41
41
  "prepublishOnly": "npm run format && npm run lint && npm run test:ci",
@@ -52,39 +52,32 @@
52
52
  "node": ">= 16.13.0"
53
53
  },
54
54
  "dependencies": {
55
- "@apollo/gateway": "0.50.1",
56
- "@nestjs/apollo": "10.0.9",
55
+ "@apollo/gateway": "0.50.2",
56
+ "@nestjs/apollo": "10.0.11",
57
57
  "@nestjs/common": "8.4.4",
58
58
  "@nestjs/core": "8.4.4",
59
- "@nestjs/graphql": "10.0.9",
59
+ "@nestjs/graphql": "10.0.11",
60
60
  "@nestjs/jwt": "8.0.0",
61
61
  "@nestjs/mongoose": "9.0.3",
62
62
  "@nestjs/passport": "8.2.1",
63
63
  "@nestjs/platform-express": "8.4.4",
64
- "@types/ejs": "3.1.0",
65
- "@types/lodash": "4.14.182",
66
- "@types/multer": "1.4.7",
67
- "@types/node": "17.0.25",
68
- "@types/node-mailjet": "3.3.8",
69
- "@types/nodemailer": "6.4.4",
70
- "@types/passport": "1.0.7",
71
64
  "apollo-server-core": "3.6.7",
72
65
  "apollo-server-express": "3.6.7",
73
66
  "bcrypt": "5.0.1",
74
67
  "class-transformer": "0.5.1",
75
68
  "class-validator": "0.13.2",
76
- "ejs": "3.0.2",
77
- "graphql": "16.3.0",
69
+ "ejs": "3.1.7",
70
+ "graphql": "16.4.0",
78
71
  "graphql-subscriptions": "2.0.0",
79
- "json-to-graphql-query": "2.2.3",
80
- "light-my-request": "4.9.0",
72
+ "json-to-graphql-query": "2.2.4",
73
+ "light-my-request": "4.10.1",
81
74
  "lodash": "4.17.21",
82
75
  "mongodb": "4.5.0",
83
- "mongoose": "6.3.0",
76
+ "mongoose": "6.3.2",
84
77
  "multer": "1.4.4",
85
- "node-mailjet": "3.3.10",
86
- "nodemailer": "6.7.3",
87
- "nodemon": "2.0.15",
78
+ "node-mailjet": "3.3.13",
79
+ "nodemailer": "6.7.5",
80
+ "nodemon": "2.0.16",
88
81
  "passport": "0.5.2",
89
82
  "passport-jwt": "4.0.0",
90
83
  "reflect-metadata": "0.1.13",
@@ -93,12 +86,19 @@
93
86
  },
94
87
  "devDependencies": {
95
88
  "@nestjs/testing": "8.4.4",
96
- "@types/jest": "27.4.1",
89
+ "@types/ejs": "3.1.0",
90
+ "@types/jest": "27.5.0",
91
+ "@types/lodash": "4.14.182",
92
+ "@types/multer": "1.4.7",
93
+ "@types/node": "16.11.33",
94
+ "@types/node-mailjet": "3.3.8",
95
+ "@types/nodemailer": "6.4.4",
96
+ "@types/passport": "1.0.7",
97
97
  "@types/supertest": "2.0.12",
98
- "@typescript-eslint/eslint-plugin": "5.20.0",
99
- "@typescript-eslint/parser": "5.20.0",
100
- "coffeescript": "2.6.1",
101
- "eslint": "8.13.0",
98
+ "@typescript-eslint/eslint-plugin": "5.22.0",
99
+ "@typescript-eslint/parser": "5.22.0",
100
+ "coffeescript": "2.7.0",
101
+ "eslint": "8.14.0",
102
102
  "eslint-config-prettier": "8.5.0",
103
103
  "find-file-up": "2.0.1",
104
104
  "grunt": "1.5.2",
@@ -107,16 +107,16 @@
107
107
  "grunt-contrib-watch": "1.1.0",
108
108
  "grunt-sync": "0.8.2",
109
109
  "husky": "7.0.4",
110
- "jest": "27.5.1",
110
+ "jest": "28.0.3",
111
111
  "pm2": "5.2.0",
112
112
  "prettier": "2.6.2",
113
113
  "pretty-quick": "3.1.3",
114
- "supertest": "6.2.2",
115
- "ts-jest": "27.1.4",
114
+ "supertest": "6.2.3",
115
+ "ts-jest": "28.0.1",
116
116
  "ts-morph": "14.0.0",
117
117
  "ts-node": "10.7.0",
118
- "tsconfig-paths": "3.14.1",
119
- "typescript": "4.6.3"
118
+ "tsconfig-paths": "4.0.0",
119
+ "typescript": "4.6.4"
120
120
  },
121
121
  "jest": {
122
122
  "collectCoverage": true,
package/src/config.env.ts CHANGED
@@ -109,8 +109,9 @@ const config: { [env: string]: IServerOptions } = {
109
109
  *
110
110
  * default: development
111
111
  */
112
- const envConfig = config[process.env['NODE' + '_ENV'] || 'development'] || config.development;
113
- console.log('Server starts in mode: ', process.env['NODE' + '_ENV'] || 'development');
112
+ const env = process.env['NODE' + '_ENV'] || 'development';
113
+ const envConfig = config[env] || config.development;
114
+ console.log('Configured for: ' + envConfig.env + (env !== envConfig.env ? ' (requested: ' + env + ')' : ''));
114
115
 
115
116
  /**
116
117
  * Export envConfig as default
@@ -1,6 +1,8 @@
1
1
  import 'reflect-metadata';
2
2
  import { UnauthorizedException } from '@nestjs/common';
3
3
  import { RoleEnum } from '../enums/role.enum';
4
+ import { getStringIds } from '../helpers/db.helper';
5
+ import { IdsType } from '../types/ids.type';
4
6
 
5
7
  /**
6
8
  * Restricted meta key
@@ -28,11 +30,13 @@ export const getRestricted = (object: unknown, propertyKey: string) => {
28
30
 
29
31
  /**
30
32
  * Check data for restricted properties (properties with `Restricted` decorator)
33
+ *
34
+ * If restricted roles includes RoleEnum.OWNER, ownerId(s) from current data (in DB) must be set in options.
31
35
  */
32
36
  export const checkRestricted = (
33
37
  data: any,
34
38
  user: { id: any; hasRole: (roles: string[]) => boolean },
35
- options: { ignoreUndefined?: boolean; throwError?: boolean } = {},
39
+ options: { ignoreUndefined?: boolean; ownerIds?: IdsType; throwError?: boolean } = {},
36
40
  processedObjects: any[] = []
37
41
  ) => {
38
42
  const config = {
@@ -55,7 +59,7 @@ export const checkRestricted = (
55
59
  // Array
56
60
  if (Array.isArray(data)) {
57
61
  // Check array items
58
- return data.map((item) => checkRestricted(item, user, options, processedObjects));
62
+ return data.map((item) => checkRestricted(item, user, config, processedObjects));
59
63
  }
60
64
 
61
65
  // Object
@@ -74,18 +78,20 @@ export const checkRestricted = (
74
78
  if (!user || !user.hasRole(roles)) {
75
79
  // Check special role for owner
76
80
  if (user && roles.includes(RoleEnum.OWNER)) {
77
- const userId = user.id.toString();
81
+ const userId = getStringIds(user);
82
+ const ownerIds = config.ownerIds ? getStringIds(config.ownerIds) : null;
78
83
 
79
84
  if (
80
- !data.ownerIds ||
81
- !(
82
- data.ownerIds === userId ||
83
- (Array.isArray(data.ownerIds) &&
84
- data.ownerIds.some((item) => (item.id ? item.id.toString() === userId : item.toString() === userId)))
85
- )
85
+ // No owner IDs
86
+ !ownerIds ||
87
+ // User is not the owner
88
+ !(ownerIds === userId || (Array.isArray(ownerIds) && ownerIds.includes(userId)))
86
89
  ) {
87
90
  // The user does not have the required rights and is not the owner
88
91
  if (config.throwError) {
92
+ if (!config.ownerIds) {
93
+ throw new UnauthorizedException('Lack of ownerIds to verify ownership of ' + propertyKey);
94
+ }
89
95
  throw new UnauthorizedException('Current user is not allowed to set ' + propertyKey);
90
96
  }
91
97
  continue;
@@ -101,7 +107,7 @@ export const checkRestricted = (
101
107
  }
102
108
 
103
109
  // Check property data
104
- data[propertyKey] = checkRestricted(data[propertyKey], user, options, processedObjects);
110
+ data[propertyKey] = checkRestricted(data[propertyKey], user, config, processedObjects);
105
111
  }
106
112
 
107
113
  // Return processed data
@@ -1,8 +1,11 @@
1
- import { BadRequestException } from '@nestjs/common';
1
+ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
2
2
  import { plainToInstance } from 'class-transformer';
3
3
  import { validate } from 'class-validator';
4
4
  import * as _ from 'lodash';
5
5
  import { checkRestricted } from '../decorators/restricted.decorator';
6
+ import { RoleEnum } from '../enums/role.enum';
7
+ import { IdsType } from '../types/ids.type';
8
+ import { getStringIds } from './db.helper';
6
9
 
7
10
  /**
8
11
  * Helper class for inputs
@@ -15,9 +18,9 @@ export default class InputHelper {
15
18
  public static async check(
16
19
  value: any,
17
20
  user: { id: any; hasRole: (roles: string[]) => boolean },
18
- metatype?
21
+ options?: { metatype?: any; ownerIds?: IdsType; roles?: string | string[] }
19
22
  ): Promise<any> {
20
- return check(value, user, metatype);
23
+ return check(value, user, options);
21
24
  }
22
25
 
23
26
  // Standard error function
@@ -179,34 +182,80 @@ export default class InputHelper {
179
182
  export async function check(
180
183
  value: any,
181
184
  user: { id: any; hasRole: (roles: string[]) => boolean },
182
- metatype?
185
+ options?: { metatype?: any; ownerIds?: IdsType; roles?: string | string[]; throwError?: boolean }
183
186
  ): Promise<any> {
187
+ const config = {
188
+ throwError: true,
189
+ ...options,
190
+ };
191
+
192
+ // Check roles
193
+ if (config.roles?.length && config.throwError) {
194
+ let roles = config.roles;
195
+ if (!Array.isArray(roles)) {
196
+ roles = [roles];
197
+ }
198
+ let valid = false;
199
+ if (roles.includes(RoleEnum.USER) && user?.id) {
200
+ valid = true;
201
+ } else if (user.hasRole(roles)) {
202
+ valid = true;
203
+ } else if (roles.includes(RoleEnum.OWNER) && user?.id && config.ownerIds) {
204
+ let ownerIds: string | string[] = getStringIds(config.ownerIds);
205
+ if (!Array.isArray(ownerIds)) {
206
+ ownerIds = [ownerIds];
207
+ }
208
+ valid = ownerIds.includes(getStringIds(user.id));
209
+ }
210
+ if (!valid) {
211
+ throw new UnauthorizedException('Missing rights');
212
+ }
213
+ }
214
+
184
215
  // Return value if it is only a basic type
185
- if (typeof value !== 'object' || !metatype || isBasicType(metatype)) {
216
+ if (typeof value !== 'object') {
186
217
  return value;
187
218
  }
188
219
 
189
- // Convert to metatype
190
- if (!(value instanceof metatype)) {
191
- if ((metatype as any)?.map) {
192
- value = (metatype as any)?.map(value);
193
- } else {
194
- value = plainToInstance(metatype, value);
220
+ // Check array
221
+ if (Array.isArray(value)) {
222
+ for (const [key, item] of Object.entries(value)) {
223
+ value[key] = await check(item, user, config);
224
+ }
225
+ return value;
226
+ }
227
+
228
+ const metatype = config.metatype;
229
+ if (metatype) {
230
+ // Check metatype
231
+ if (isBasicType(metatype)) {
232
+ return value;
233
+ }
234
+
235
+ // Convert to metatype
236
+ if (!(value instanceof metatype)) {
237
+ if ((metatype as any)?.map) {
238
+ value = (metatype as any)?.map(value);
239
+ } else {
240
+ value = plainToInstance(metatype, value);
241
+ }
195
242
  }
196
243
  }
197
244
 
198
245
  // Validate
199
246
  const errors = await validate(value);
200
- if (errors.length > 0) {
247
+ if (errors.length > 0 && config.throwError) {
201
248
  throw new BadRequestException('Validation failed');
202
249
  }
203
250
 
204
251
  // Remove restricted values if roles are missing
205
- value = checkRestricted(value, user);
252
+ value = checkRestricted(value, user, config);
206
253
  return value;
207
254
  }
208
255
 
209
- // Standard error function
256
+ /**
257
+ * Standard error function
258
+ */
210
259
  export function errorFunction(caller: (...params) => any, message = 'Required parameter is missing or invalid') {
211
260
  const err = new Error(message);
212
261
  Error.captureStackTrace(err, caller);
@@ -1,5 +1,6 @@
1
- import { Model } from 'mongoose';
1
+ import { Model, Types } from 'mongoose';
2
2
  import { FieldSelection } from '../types/field-selection.type';
3
+ import { IdsType } from '../types/ids.type';
3
4
 
4
5
  /**
5
6
  * General service options
@@ -8,6 +9,11 @@ export interface ServiceOptions {
8
9
  // All fields are allowed to be compatible as far as possible
9
10
  [key: string]: any;
10
11
 
12
+ // Check rights for input data (see check function in InputHelper)
13
+ // If falsy: input data will not be checked
14
+ // If truly (default): input data will be checked
15
+ checkRights?: boolean;
16
+
11
17
  // Current user to set ownership, check roles and other things
12
18
  currentUser?: {
13
19
  [key: string]: any;
@@ -18,6 +24,12 @@ export interface ServiceOptions {
18
24
  // Field selection for results
19
25
  fieldSelection?: FieldSelection;
20
26
 
27
+ // Overwrites type of input (array items)
28
+ inputType?: new (...params: any[]) => any;
29
+
30
+ // Owner IDs
31
+ ownerIds?: IdsType;
32
+
21
33
  // Process field selection
22
34
  // If {} or not set, then the field selection runs with defaults
23
35
  // If falsy, then the field selection will not be automatically executed
@@ -50,4 +62,10 @@ export interface ServiceOptions {
50
62
 
51
63
  // Whether to publish action via GraphQL subscription
52
64
  pubSub?: boolean;
65
+
66
+ // Overwrites type of result (array items)
67
+ resultType?: new (...params: any[]) => any;
68
+
69
+ // Roles (as string) to check
70
+ roles?: string | string[];
53
71
  }
@@ -78,7 +78,7 @@ export abstract class CoreModel {
78
78
  * Initialize instance with default values instead of undefined
79
79
  * Should be overwritten in child class to organize the defaults
80
80
  */
81
- public init<T extends CoreModel>(...args: any[]): this {
81
+ public init(...args: any[]): this {
82
82
  return this;
83
83
  }
84
84
 
@@ -28,6 +28,6 @@ export class CheckInputPipe implements PipeTransform {
28
28
  const { user }: any = getContextData(this.context);
29
29
 
30
30
  // Check and return
31
- return check(value, user, metatype);
31
+ return check(value, user, { metatype });
32
32
  }
33
33
  }
@@ -24,16 +24,11 @@ export abstract class CrudService<T extends CoreModel = any> extends ModuleServi
24
24
  * Get item by ID
25
25
  */
26
26
  async get(id: string, serviceOptions?: ServiceOptions): Promise<T> {
27
- return this.process(
28
- async (data) => {
29
- const item = await this.mainDbModel.findById(data.input).exec();
30
- if (!item) {
31
- throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
32
- }
33
- return item;
34
- },
35
- { input: id, serviceOptions }
36
- );
27
+ const dbObject = await this.mainDbModel.findById(id).exec();
28
+ if (!dbObject) {
29
+ throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
30
+ }
31
+ return this.process(async () => dbObject, { dbObject, serviceOptions });
37
32
  }
38
33
 
39
34
  /**
@@ -74,15 +69,15 @@ export abstract class CrudService<T extends CoreModel = any> extends ModuleServi
74
69
  * Update item via ID
75
70
  */
76
71
  async update(id: string, input: any, serviceOptions?: ServiceOptions): Promise<T> {
72
+ const dbObject = await this.mainDbModel.findById(id).exec();
73
+ if (!dbObject) {
74
+ throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
75
+ }
77
76
  return this.process(
78
77
  async (data) => {
79
- const item = await this.mainDbModel.findById(id).exec();
80
- if (!item) {
81
- throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
82
- }
83
- return await Object.assign(item, data.input).save();
78
+ return await Object.assign(dbObject, data.input).save();
84
79
  },
85
- { input, serviceOptions }
80
+ { dbObject, input, serviceOptions }
86
81
  );
87
82
  }
88
83
 
@@ -90,16 +85,16 @@ export abstract class CrudService<T extends CoreModel = any> extends ModuleServi
90
85
  * Delete item via ID
91
86
  */
92
87
  async delete(id: string, serviceOptions?: ServiceOptions): Promise<T> {
88
+ const dbObject = await this.mainDbModel.findById(id).exec();
89
+ if (!dbObject) {
90
+ throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
91
+ }
93
92
  return this.process(
94
93
  async (data) => {
95
- const item = await this.mainDbModel.findById(id).exec();
96
- if (!item) {
97
- throw new NotFoundException(`No ${this.mainModelConstructor.name} found with ID: ${id}`);
98
- }
99
94
  await this.mainDbModel.findByIdAndDelete(id).exec();
100
- return item;
95
+ return dbObject;
101
96
  },
102
- { input: id, serviceOptions }
97
+ { dbObject, input: id, serviceOptions }
103
98
  );
104
99
  }
105
100
  }
@@ -1,9 +1,11 @@
1
- import { Document, Model } from 'mongoose';
2
- import { popAndMap } from '../helpers/db.helper';
1
+ import { Document, Model, Types } from 'mongoose';
2
+ import { getStringIds, popAndMap } from '../helpers/db.helper';
3
+ import { check } from '../helpers/input.helper';
3
4
  import { prepareInput, prepareOutput } from '../helpers/service.helper';
4
5
  import { ServiceOptions } from '../interfaces/service-options.interface';
5
6
  import { CoreModel } from '../models/core-model.model';
6
7
  import { FieldSelection } from '../types/field-selection.type';
8
+ import { IdsType } from '../types/ids.type';
7
9
 
8
10
  /**
9
11
  * Module service class to be extended by concrete module services
@@ -30,17 +32,48 @@ export abstract class ModuleService<T extends CoreModel = any> {
30
32
  this.mainModelConstructor = options?.mainModelConstructor;
31
33
  }
32
34
 
35
+ /**
36
+ * Check rights of current user for input
37
+ */
38
+ checkRights(
39
+ input: any,
40
+ currentUser: { id: any; hasRole: (roles: string[]) => boolean },
41
+ options?: {
42
+ metatype?: any;
43
+ ownerIds?: IdsType;
44
+ roles?: string | string[];
45
+ throwError?: boolean;
46
+ }
47
+ ): Promise<any> {
48
+ const config = {
49
+ metatype: this.mainModelConstructor,
50
+ ...options,
51
+ };
52
+ return check(input, currentUser, config);
53
+ }
54
+
55
+ /**
56
+ * Get function to get Object via ID, necessary for checkInput
57
+ */
58
+ abstract get(id: any, ...args: any[]): any;
59
+
33
60
  /**
34
61
  * Run service function with pre- and post-functions
35
62
  */
36
63
  async process(
37
64
  serviceFunc: (options?: { [key: string]: any; input?: any; serviceOptions?: ServiceOptions }) => any,
38
- options?: { [key: string]: any; input?: any; serviceOptions?: ServiceOptions }
65
+ options?: {
66
+ [key: string]: any;
67
+ dbObject?: string | Types.ObjectId | any;
68
+ input?: any;
69
+ serviceOptions?: ServiceOptions;
70
+ }
39
71
  ) {
40
72
  // Configuration with default values
41
73
  const config = {
42
- currentUser: null,
43
- fieldSelection: null,
74
+ checkRights: true,
75
+ dbObject: options?.dbObject,
76
+ input: options?.input,
44
77
  processFieldSelection: {},
45
78
  prepareInput: {},
46
79
  prepareOutput: {},
@@ -50,11 +83,35 @@ export abstract class ModuleService<T extends CoreModel = any> {
50
83
 
51
84
  // Prepare input
52
85
  if (config.prepareInput && this.prepareInput) {
53
- await this.prepareInput(options?.input, config.prepareInput);
86
+ await this.prepareInput(config.input, config.prepareInput);
87
+ }
88
+
89
+ // Get owner IDs
90
+ let ownerIds = undefined;
91
+ if (config.checkRights && this.checkRights) {
92
+ ownerIds = getStringIds(config.ownerIds);
93
+ if (!ownerIds?.length) {
94
+ if (config.dbObject) {
95
+ if (typeof config.dbObject === 'string' || config.dbObject instanceof Types.ObjectId) {
96
+ ownerIds = (await this.get(getStringIds(config.dbObject)))?.ownerIds;
97
+ } else {
98
+ ownerIds = config.dbObject.ownerIds;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // Check rights for input
105
+ if (config.input && config.checkRights && this.checkRights) {
106
+ const opts: any = { ownerIds, roles: config.roles };
107
+ if (config.inputType) {
108
+ opts.metatype = config.resultType;
109
+ }
110
+ config.input = await this.checkRights(config.input, config.currentUser as any, opts);
54
111
  }
55
112
 
56
113
  // Run service function
57
- const result = await serviceFunc(options);
114
+ let result = await serviceFunc(config);
58
115
 
59
116
  // Pop and map main model
60
117
  if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
@@ -67,10 +124,19 @@ export abstract class ModuleService<T extends CoreModel = any> {
67
124
  if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
68
125
  config.prepareOutput.targetModel = null;
69
126
  }
70
- return this.prepareOutput(result, config.prepareOutput);
127
+ result = await this.prepareOutput(result, config.prepareOutput);
128
+ }
129
+
130
+ // Check output rights
131
+ if (config.checkRights && this.checkRights) {
132
+ const opts: any = { ownerIds, roles: config.roles, throwError: false };
133
+ if (config.resultType) {
134
+ opts.metatype = config.resultType;
135
+ }
136
+ result = await this.checkRights(result, config.currentUser as any, opts);
71
137
  }
72
138
 
73
- // Return result without output preparation
139
+ // Return (prepared) result
74
140
  return result;
75
141
  }
76
142
 
@@ -0,0 +1,7 @@
1
+ import { Types } from 'mongoose';
2
+
3
+ export type IdsType =
4
+ | string
5
+ | Types.ObjectId
6
+ | { id?: string | Types.ObjectId; _id?: string | Types.ObjectId }
7
+ | (string | Types.ObjectId | { id?: string | Types.ObjectId; _id?: string | Types.ObjectId })[];
@@ -1,4 +1,4 @@
1
- import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
1
+ import { ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
2
2
  import { Reflector } from '@nestjs/core';
3
3
  import { GqlExecutionContext } from '@nestjs/graphql';
4
4
  import { RoleEnum } from '../../../common/enums/role.enum';