@lenne.tech/nest-server 8.4.0 → 8.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "8.4.0",
3
+ "version": "8.5.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",
@@ -9,6 +9,10 @@ import { RequireAtLeastOne } from '../types/required-at-least-one.type';
9
9
  * Restricted meta key
10
10
  */
11
11
  const restrictedMetaKey = Symbol('restricted');
12
+
13
+ /**
14
+ * Restricted type
15
+ */
12
16
  export type RestrictedType = (
13
17
  | string
14
18
  | RequireAtLeastOne<
@@ -31,14 +35,17 @@ export type RestrictedType = (
31
35
  * properties of the processed item, which is specified by the value of `memberOf`
32
36
  * Via processType the restriction can be set for input or output only
33
37
  */
34
- export const Restricted = (...rolesOrMember: RestrictedType): PropertyDecorator => {
38
+ export const Restricted = (...rolesOrMember: RestrictedType): ClassDecorator & PropertyDecorator => {
35
39
  return Reflect.metadata(restrictedMetaKey, rolesOrMember);
36
40
  };
37
41
 
38
42
  /**
39
- * Get restricted
43
+ * Get restricted data for (property of) object
40
44
  */
41
- export const getRestricted = (object: unknown, propertyKey: string): RestrictedType => {
45
+ export const getRestricted = (object: unknown, propertyKey?: string): RestrictedType => {
46
+ if (!propertyKey) {
47
+ return Reflect.getMetadata(restrictedMetaKey, object);
48
+ }
42
49
  return Reflect.getMetadata(restrictedMetaKey, object, propertyKey);
43
50
  };
44
51
 
@@ -49,11 +56,18 @@ export const getRestricted = (object: unknown, propertyKey: string): RestrictedT
49
56
  export const checkRestricted = (
50
57
  data: any,
51
58
  user: { id: any; hasRole: (roles: string[]) => boolean },
52
- options: { dbObject?: any; ignoreUndefined?: boolean; processType?: ProcessType; throwError?: boolean } = {},
59
+ options: {
60
+ dbObject?: any;
61
+ ignoreUndefined?: boolean;
62
+ processType?: ProcessType;
63
+ removeUndefinedFromResultArray?: boolean;
64
+ throwError?: boolean;
65
+ } = {},
53
66
  processedObjects: any[] = []
54
67
  ) => {
55
68
  const config = {
56
69
  ignoreUndefined: true,
70
+ removeUndefinedFromResultArray: true,
57
71
  throwError: true,
58
72
  ...options,
59
73
  };
@@ -72,18 +86,15 @@ export const checkRestricted = (
72
86
  // Array
73
87
  if (Array.isArray(data)) {
74
88
  // Check array items
75
- return data.map((item) => checkRestricted(item, user, config, processedObjects));
76
- }
77
-
78
- // Object
79
- for (const propertyKey of Object.keys(data)) {
80
- // Check undefined
81
- if (data[propertyKey] === undefined && config.ignoreUndefined) {
82
- continue;
89
+ let result = data.map((item) => checkRestricted(item, user, config, processedObjects));
90
+ if (!config.throwError && config.removeUndefinedFromResultArray) {
91
+ result = result.filter((item) => item !== undefined);
83
92
  }
93
+ return result;
94
+ }
84
95
 
85
- // Check restricted
86
- const restricted = getRestricted(data, propertyKey);
96
+ // Check function
97
+ const validateRestricted = (restricted) => {
87
98
  let valid = true;
88
99
  if (restricted?.length) {
89
100
  valid = false;
@@ -110,6 +121,7 @@ export const checkRestricted = (
110
121
  // Check roles
111
122
  if (
112
123
  user?.hasRole(roles) ||
124
+ (user?.id && roles.includes(RoleEnum.S_USER)) ||
113
125
  (roles.includes(RoleEnum.S_CREATOR) && getIncludedIds(config.dbObject?.createdBy, user))
114
126
  ) {
115
127
  valid = true;
@@ -161,6 +173,30 @@ export const checkRestricted = (
161
173
  }
162
174
  }
163
175
  }
176
+ return valid;
177
+ };
178
+
179
+ // Check object
180
+ const objectRestrictions = getRestricted(data.constructor);
181
+ const objectIsValid = validateRestricted(objectRestrictions);
182
+ if (!objectIsValid) {
183
+ // Throw error
184
+ if (config.throwError) {
185
+ throw new UnauthorizedException('The current user has no access rights for ' + data.constructor?.name);
186
+ }
187
+ return null;
188
+ }
189
+
190
+ // Check properties of object
191
+ for (const propertyKey of Object.keys(data)) {
192
+ // Check undefined
193
+ if (data[propertyKey] === undefined && config.ignoreUndefined) {
194
+ continue;
195
+ }
196
+
197
+ // Check restricted
198
+ const restricted = getRestricted(data, propertyKey);
199
+ const valid = validateRestricted(restricted);
164
200
 
165
201
  // Check rights
166
202
  if (valid) {
@@ -1,5 +1,6 @@
1
1
  import { UnauthorizedException } from '@nestjs/common';
2
2
  import * as bcrypt from 'bcrypt';
3
+ import { plainToInstance } from 'class-transformer';
3
4
  import * as _ from 'lodash';
4
5
  import { RoleEnum } from '../enums/role.enum';
5
6
 
@@ -54,6 +55,7 @@ export async function prepareInput<T = any>(
54
55
  clone?: boolean;
55
56
  getNewArray?: boolean;
56
57
  removeUndefined?: boolean;
58
+ targetModel?: new (...args: any[]) => T;
57
59
  } = {}
58
60
  ): Promise<T> {
59
61
  // Configuration
@@ -86,6 +88,15 @@ export async function prepareInput<T = any>(
86
88
  }
87
89
  }
88
90
 
91
+ // Map input if target model exist
92
+ if (config.targetModel && !(input instanceof config.targetModel)) {
93
+ if ((config.targetModel as any)?.map) {
94
+ input = await (config.targetModel as any).map(input);
95
+ } else {
96
+ input = plainToInstance(config.targetModel, input);
97
+ }
98
+ }
99
+
89
100
  // Remove undefined properties to avoid unwanted overwrites
90
101
  if (config.removeUndefined) {
91
102
  Object.keys(input).forEach((key) => input[key] === undefined && delete input[key]);
@@ -171,8 +182,12 @@ export async function prepareOutput<T = { [key: string]: any; map: (...args: any
171
182
  }
172
183
 
173
184
  // Map output if target model exist
174
- if (config.targetModel) {
175
- output = await (config.targetModel as any).map(output);
185
+ if (config.targetModel && !(output instanceof config.targetModel)) {
186
+ if ((config.targetModel as any)?.map) {
187
+ output = await (config.targetModel as any).map(output);
188
+ } else {
189
+ output = plainToInstance(config.targetModel, output);
190
+ }
176
191
  }
177
192
 
178
193
  // Remove password if exists
@@ -26,6 +26,9 @@ export interface ServiceOptions {
26
26
  // Overwrites type of input (array items)
27
27
  inputType?: new (...params: any[]) => any;
28
28
 
29
+ // Overwrites type of output (array items)
30
+ outputType?: new (...params: any[]) => any;
31
+
29
32
  // Process field selection
30
33
  // If {} or not set, then the field selection runs with defaults
31
34
  // If falsy, then the field selection will not be automatically executed
@@ -59,9 +62,6 @@ export interface ServiceOptions {
59
62
  // Whether to publish action via GraphQL subscription
60
63
  pubSub?: boolean;
61
64
 
62
- // Overwrites type of result (array items)
63
- resultType?: new (...params: any[]) => any;
64
-
65
65
  // Roles (as string) to check
66
66
  roles?: string | string[];
67
67
  }
@@ -84,7 +84,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
84
84
 
85
85
  // Prepare input
86
86
  if (config.prepareInput && this.prepareInput) {
87
- await this.prepareInput(config.input, config.prepareInput);
87
+ const opts = config.prepareInput;
88
+ if (!opts.targetModel && config.inputType) {
89
+ opts.targetModel = config.inputType;
90
+ }
91
+ await this.prepareInput(config.input, opts);
88
92
  }
89
93
 
90
94
  // Get DB object
@@ -101,7 +105,7 @@ export abstract class ModuleService<T extends CoreModel = any> {
101
105
  if (config.input && config.checkRights && this.checkRights) {
102
106
  const opts: any = { dbObject: config.dbObject, processType: ProcessType.INPUT, roles: config.roles };
103
107
  if (config.inputType) {
104
- opts.metatype = config.resultType;
108
+ opts.metatype = config.inputType;
105
109
  }
106
110
  config.input = await this.checkRights(config.input, config.currentUser as any, opts);
107
111
  }
@@ -116,11 +120,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
116
120
 
117
121
  // Prepare output
118
122
  if (config.prepareOutput && this.prepareOutput) {
119
- // Check if mapping is already done by processFieldSelection
120
- if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
121
- config.prepareOutput.targetModel = null;
123
+ const opts = config.prepareOutput;
124
+ if (!opts.targetModel && config.outputType) {
125
+ opts.targetModel = config.outputType;
122
126
  }
123
- result = await this.prepareOutput(result, config.prepareOutput);
127
+ result = await this.prepareOutput(result, opts);
124
128
  }
125
129
 
126
130
  // Check output rights
@@ -131,8 +135,8 @@ export abstract class ModuleService<T extends CoreModel = any> {
131
135
  roles: config.roles,
132
136
  throwError: false,
133
137
  };
134
- if (config.resultType) {
135
- opts.metatype = config.resultType;
138
+ if (config.outputType) {
139
+ opts.metatype = config.outputType;
136
140
  }
137
141
  result = await this.checkRights(result, config.currentUser as any, opts);
138
142
  }
@@ -145,7 +149,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
145
149
  * Prepare input before save
146
150
  */
147
151
  async prepareInput(input: Record<string, any>, options: ServiceOptions = {}) {
148
- return prepareInput(input, options.currentUser, options.prepareInput);
152
+ const config = {
153
+ targetModel: this.mainModelConstructor,
154
+ ...options?.prepareInput,
155
+ };
156
+ return prepareInput(input, options.currentUser, config);
149
157
  }
150
158
 
151
159
  /**