@lenne.tech/nest-server 8.3.1 → 8.6.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/dist/core/common/decorators/restricted.decorator.d.ts +12 -5
- package/dist/core/common/decorators/restricted.decorator.js +91 -29
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/enums/process-type.enum.d.ts +4 -0
- package/dist/core/common/enums/process-type.enum.js +9 -0
- package/dist/core/common/enums/process-type.enum.js.map +1 -0
- package/dist/core/common/enums/role.enum.d.ts +1 -2
- package/dist/core/common/enums/role.enum.js +0 -1
- package/dist/core/common/enums/role.enum.js.map +1 -1
- package/dist/core/common/helpers/db.helper.d.ts +4 -4
- package/dist/core/common/helpers/db.helper.js +54 -30
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +6 -5
- package/dist/core/common/helpers/input.helper.js +4 -15
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.d.ts +12 -0
- package/dist/core/common/helpers/service.helper.js +42 -3
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/interfaces/prepare-input-options.interface.d.ts +8 -0
- package/dist/core/common/interfaces/prepare-input-options.interface.js +3 -0
- package/dist/core/common/interfaces/prepare-input-options.interface.js.map +1 -0
- package/dist/core/common/interfaces/prepare-output-options.interface.d.ts +7 -0
- package/dist/core/common/interfaces/prepare-output-options.interface.js +3 -0
- package/dist/core/common/interfaces/prepare-output-options.interface.js.map +1 -0
- package/dist/core/common/interfaces/service-options.interface.d.ts +5 -17
- package/dist/core/common/models/core-persistence.model.d.ts +0 -1
- package/dist/core/common/models/core-persistence.model.js +0 -10
- package/dist/core/common/models/core-persistence.model.js.map +1 -1
- package/dist/core/common/services/module.service.d.ts +4 -4
- package/dist/core/common/services/module.service.js +27 -26
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/common/types/require-only-one.type.d.ts +3 -0
- package/dist/core/common/types/require-only-one.type.js +3 -0
- package/dist/core/common/types/require-only-one.type.js.map +1 -0
- package/dist/core/common/types/required-at-least-one.type.d.ts +3 -0
- package/dist/core/common/types/required-at-least-one.type.js +3 -0
- package/dist/core/common/types/required-at-least-one.type.js.map +1 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +5 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +34 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +5 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +34 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -0
- package/dist/core.module.js +0 -5
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/auth/auth.model.js +2 -2
- package/dist/server/modules/auth/auth.model.js.map +1 -1
- package/dist/server/modules/auth/auth.module.js +7 -2
- package/dist/server/modules/auth/auth.module.js.map +1 -1
- package/dist/server/modules/auth/auth.resolver.d.ts +8 -3
- package/dist/server/modules/auth/auth.resolver.js +33 -10
- package/dist/server/modules/auth/auth.resolver.js.map +1 -1
- package/dist/server/modules/auth/auth.service.d.ts +15 -0
- package/dist/server/modules/auth/auth.service.js +71 -0
- package/dist/server/modules/auth/auth.service.js.map +1 -0
- package/dist/server/modules/auth/inputs/auth-sign-in.input.d.ts +3 -0
- package/dist/server/modules/auth/inputs/auth-sign-in.input.js +18 -0
- package/dist/server/modules/auth/inputs/auth-sign-in.input.js.map +1 -0
- package/dist/server/modules/auth/inputs/auth-sign-up.input.d.ts +5 -0
- package/dist/server/modules/auth/inputs/auth-sign-up.input.js +34 -0
- package/dist/server/modules/auth/inputs/auth-sign-up.input.js.map +1 -0
- package/dist/server/modules/user/user.model.d.ts +1 -1
- package/dist/server/modules/user/user.model.js +1 -1
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.resolver.js +12 -11
- package/dist/server/modules/user/user.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.js +1 -5
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/common/decorators/restricted.decorator.ts +150 -53
- package/src/core/common/enums/process-type.enum.ts +7 -0
- package/src/core/common/enums/role.enum.ts +1 -4
- package/src/core/common/helpers/db.helper.ts +70 -56
- package/src/core/common/helpers/input.helper.ts +24 -15
- package/src/core/common/helpers/service.helper.ts +72 -2
- package/src/core/common/interfaces/prepare-input-options.interface.ts +11 -0
- package/src/core/common/interfaces/prepare-output-options.interface.ts +10 -0
- package/src/core/common/interfaces/service-options.interface.ts +8 -22
- package/src/core/common/models/core-persistence.model.ts +0 -11
- package/src/core/common/services/module.service.ts +32 -31
- package/src/core/common/types/require-only-one.type.ts +6 -0
- package/src/core/common/types/required-at-least-one.type.ts +6 -0
- package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +18 -0
- package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +18 -0
- package/src/core.module.ts +1 -19
- package/src/index.ts +5 -0
- package/src/server/modules/auth/auth.model.ts +5 -5
- package/src/server/modules/auth/auth.module.ts +13 -2
- package/src/server/modules/auth/auth.resolver.ts +30 -12
- package/src/server/modules/auth/auth.service.ts +84 -0
- package/src/server/modules/auth/inputs/auth-sign-in.input.ts +10 -0
- package/src/server/modules/auth/inputs/auth-sign-up.input.ts +18 -0
- package/src/server/modules/user/user.model.ts +2 -2
- package/src/server/modules/user/user.resolver.ts +12 -11
- package/src/server/modules/user/user.service.ts +3 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.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",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@typescript-eslint/eslint-plugin": "5.22.0",
|
|
99
99
|
"@typescript-eslint/parser": "5.22.0",
|
|
100
100
|
"coffeescript": "2.7.0",
|
|
101
|
-
"eslint": "8.
|
|
101
|
+
"eslint": "8.15.0",
|
|
102
102
|
"eslint-config-prettier": "8.5.0",
|
|
103
103
|
"find-file-up": "2.0.1",
|
|
104
104
|
"grunt": "1.5.2",
|
|
@@ -107,12 +107,12 @@
|
|
|
107
107
|
"grunt-contrib-watch": "1.1.0",
|
|
108
108
|
"grunt-sync": "0.8.2",
|
|
109
109
|
"husky": "7.0.4",
|
|
110
|
-
"jest": "28.0
|
|
110
|
+
"jest": "28.1.0",
|
|
111
111
|
"pm2": "5.2.0",
|
|
112
112
|
"prettier": "2.6.2",
|
|
113
113
|
"pretty-quick": "3.1.3",
|
|
114
114
|
"supertest": "6.2.3",
|
|
115
|
-
"ts-jest": "28.0.
|
|
115
|
+
"ts-jest": "28.0.2",
|
|
116
116
|
"ts-morph": "14.0.0",
|
|
117
117
|
"ts-node": "10.7.0",
|
|
118
118
|
"tsconfig-paths": "4.0.0",
|
|
@@ -1,47 +1,73 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { UnauthorizedException } from '@nestjs/common';
|
|
3
|
+
import { ProcessType } from '../enums/process-type.enum';
|
|
3
4
|
import { RoleEnum } from '../enums/role.enum';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { getIncludedIds } from '../helpers/db.helper';
|
|
6
|
+
import { RequireAtLeastOne } from '../types/required-at-least-one.type';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Restricted meta key
|
|
9
10
|
*/
|
|
10
11
|
const restrictedMetaKey = Symbol('restricted');
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Restricted type
|
|
15
|
+
*/
|
|
16
|
+
export type RestrictedType = (
|
|
17
|
+
| string
|
|
18
|
+
| RequireAtLeastOne<
|
|
19
|
+
{ memberOf?: string | string[]; roles?: string | string[]; processType?: ProcessType },
|
|
20
|
+
'memberOf' | 'roles'
|
|
21
|
+
>
|
|
22
|
+
)[];
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Decorator for restricted properties
|
|
14
26
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
27
|
+
* The restricted decorator can include roles as strings or object with property `roles`
|
|
28
|
+
* and memberships as objects with property `memberOf`.
|
|
17
29
|
*
|
|
18
|
-
*
|
|
30
|
+
* Roles:
|
|
31
|
+
* If one or more Role(Enum)s are set, the user must have at least one of it in his `role` property.
|
|
32
|
+
*
|
|
33
|
+
* Memberships:
|
|
34
|
+
* If one or more membership objects are set, the ID of the user must be included in one of the
|
|
35
|
+
* properties of the processed item, which is specified by the value of `memberOf`
|
|
36
|
+
* Via processType the restriction can be set for input or output only
|
|
19
37
|
*/
|
|
20
|
-
export const Restricted = (...
|
|
21
|
-
return Reflect.metadata(restrictedMetaKey,
|
|
38
|
+
export const Restricted = (...rolesOrMember: RestrictedType): ClassDecorator & PropertyDecorator => {
|
|
39
|
+
return Reflect.metadata(restrictedMetaKey, rolesOrMember);
|
|
22
40
|
};
|
|
23
41
|
|
|
24
42
|
/**
|
|
25
|
-
* Get restricted
|
|
43
|
+
* Get restricted data for (property of) object
|
|
26
44
|
*/
|
|
27
|
-
export const getRestricted = (object: unknown, propertyKey
|
|
45
|
+
export const getRestricted = (object: unknown, propertyKey?: string): RestrictedType => {
|
|
46
|
+
if (!propertyKey) {
|
|
47
|
+
return Reflect.getMetadata(restrictedMetaKey, object);
|
|
48
|
+
}
|
|
28
49
|
return Reflect.getMetadata(restrictedMetaKey, object, propertyKey);
|
|
29
50
|
};
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Check data for restricted properties (properties with `Restricted` decorator)
|
|
33
|
-
*
|
|
34
|
-
* If restricted roles includes RoleEnum.S_CREATOR, `creator` (createdBy) from current (DB) data must be set in options
|
|
35
|
-
* If restricted roles includes RoleEnum.S_OWNER, `ownerIds` from current (DB) data must be set in options
|
|
54
|
+
* For special Roles and member of group checking the dbObject must be set in options
|
|
36
55
|
*/
|
|
37
56
|
export const checkRestricted = (
|
|
38
57
|
data: any,
|
|
39
58
|
user: { id: any; hasRole: (roles: string[]) => boolean },
|
|
40
|
-
options: {
|
|
59
|
+
options: {
|
|
60
|
+
dbObject?: any;
|
|
61
|
+
ignoreUndefined?: boolean;
|
|
62
|
+
processType?: ProcessType;
|
|
63
|
+
removeUndefinedFromResultArray?: boolean;
|
|
64
|
+
throwError?: boolean;
|
|
65
|
+
} = {},
|
|
41
66
|
processedObjects: any[] = []
|
|
42
67
|
) => {
|
|
43
68
|
const config = {
|
|
44
69
|
ignoreUndefined: true,
|
|
70
|
+
removeUndefinedFromResultArray: true,
|
|
45
71
|
throwError: true,
|
|
46
72
|
...options,
|
|
47
73
|
};
|
|
@@ -60,60 +86,131 @@ export const checkRestricted = (
|
|
|
60
86
|
// Array
|
|
61
87
|
if (Array.isArray(data)) {
|
|
62
88
|
// Check array items
|
|
63
|
-
|
|
89
|
+
let result = data.map((item) => checkRestricted(item, user, config, processedObjects));
|
|
90
|
+
if (!config.throwError && config.removeUndefinedFromResultArray) {
|
|
91
|
+
result = result.filter((item) => item !== undefined);
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
64
94
|
}
|
|
65
95
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
}
|
|
96
|
+
// Check function
|
|
97
|
+
const validateRestricted = (restricted) => {
|
|
98
|
+
let valid = true;
|
|
99
|
+
if (restricted?.length) {
|
|
100
|
+
valid = false;
|
|
72
101
|
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
// Get roles
|
|
103
|
+
const roles: string[] = [];
|
|
104
|
+
restricted.forEach((item) => {
|
|
105
|
+
if (typeof item === 'string') {
|
|
106
|
+
roles.push(item);
|
|
107
|
+
} else if (
|
|
108
|
+
item?.roles?.length &&
|
|
109
|
+
(config.processType && item.processType ? config.processType === item.processType : true)
|
|
110
|
+
) {
|
|
111
|
+
if (Array.isArray(item.roles)) {
|
|
112
|
+
roles.push(...item.roles);
|
|
113
|
+
} else {
|
|
114
|
+
roles.push(item.roles);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
75
118
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
// Check roles
|
|
120
|
+
if (roles.length) {
|
|
121
|
+
// Check roles
|
|
122
|
+
if (
|
|
123
|
+
user?.hasRole(roles) ||
|
|
124
|
+
(user?.id && roles.includes(RoleEnum.S_USER)) ||
|
|
125
|
+
(roles.includes(RoleEnum.S_CREATOR) && getIncludedIds(config.dbObject?.createdBy, user))
|
|
126
|
+
) {
|
|
127
|
+
valid = true;
|
|
83
128
|
}
|
|
129
|
+
}
|
|
84
130
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
if (!valid) {
|
|
132
|
+
// Get groups
|
|
133
|
+
const groups = restricted.filter((item) => {
|
|
134
|
+
return (
|
|
135
|
+
typeof item === 'object' &&
|
|
136
|
+
// Check if object is valid
|
|
137
|
+
item.memberOf.length &&
|
|
138
|
+
// Check if processType is specified and is valid for current process
|
|
139
|
+
(config.processType && item.processType ? config.processType === item.processType : true)
|
|
140
|
+
);
|
|
141
|
+
}) as { memberOf: string | string[] }[];
|
|
142
|
+
|
|
143
|
+
// Check groups
|
|
144
|
+
if (groups.length) {
|
|
145
|
+
// Get members from groups
|
|
146
|
+
const members = [];
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
let properties: string[] = group.memberOf as string[];
|
|
149
|
+
if (!Array.isArray(group.memberOf)) {
|
|
150
|
+
properties = [group.memberOf];
|
|
151
|
+
}
|
|
152
|
+
for (const property of properties) {
|
|
153
|
+
const items = config.dbObject?.[property];
|
|
154
|
+
if (items) {
|
|
155
|
+
if (Array.isArray(items)) {
|
|
156
|
+
members.concat(items);
|
|
157
|
+
} else {
|
|
158
|
+
members.push(items);
|
|
159
|
+
}
|
|
100
160
|
}
|
|
101
|
-
throw new UnauthorizedException('Current user is not allowed to set ' + propertyKey);
|
|
102
161
|
}
|
|
103
|
-
continue;
|
|
104
162
|
}
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
if (
|
|
108
|
-
|
|
163
|
+
|
|
164
|
+
// Check if user is a member
|
|
165
|
+
if (getIncludedIds(members, user)) {
|
|
166
|
+
valid = true;
|
|
109
167
|
}
|
|
110
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if there are no limitations
|
|
171
|
+
if (!roles.length && !groups.length) {
|
|
172
|
+
valid = true;
|
|
111
173
|
}
|
|
112
174
|
}
|
|
113
175
|
}
|
|
176
|
+
return valid;
|
|
177
|
+
};
|
|
114
178
|
|
|
115
|
-
|
|
116
|
-
|
|
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);
|
|
200
|
+
|
|
201
|
+
// Check rights
|
|
202
|
+
if (valid) {
|
|
203
|
+
// Check deep
|
|
204
|
+
data[propertyKey] = checkRestricted(data[propertyKey], user, config, processedObjects);
|
|
205
|
+
} else {
|
|
206
|
+
// Throw error
|
|
207
|
+
if (config.throwError) {
|
|
208
|
+
throw new UnauthorizedException('The current user has no access rights for ' + propertyKey);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Remove property
|
|
212
|
+
delete data[propertyKey];
|
|
213
|
+
}
|
|
117
214
|
}
|
|
118
215
|
|
|
119
216
|
// Return processed data
|
|
@@ -15,7 +15,7 @@ export enum RoleEnum {
|
|
|
15
15
|
// and via ServiceOptions for Resolver methods. This roles should not be integrated into user.roles!
|
|
16
16
|
// ===================================================================================================================
|
|
17
17
|
|
|
18
|
-
// User must be
|
|
18
|
+
// User must be logged in (see context user, e.g. @GraphQLUser)
|
|
19
19
|
S_USER = 's_user',
|
|
20
20
|
|
|
21
21
|
// ===================================================================================================================
|
|
@@ -25,7 +25,4 @@ export enum RoleEnum {
|
|
|
25
25
|
|
|
26
26
|
// User must be the creator of the processed object(s) (see createdBy property of object(s))
|
|
27
27
|
S_CREATOR = 's_creator',
|
|
28
|
-
|
|
29
|
-
// User must be an owner of the processed object(s) (see owners property of object(s))
|
|
30
|
-
S_OWNER = 's_owner',
|
|
31
28
|
}
|
|
@@ -84,7 +84,7 @@ export function addIds(
|
|
|
84
84
|
*/
|
|
85
85
|
export function equalIds(...ids: IdsType[]): boolean {
|
|
86
86
|
if (!ids) {
|
|
87
|
-
return
|
|
87
|
+
return false;
|
|
88
88
|
}
|
|
89
89
|
const compare = getStringIds(ids[0]);
|
|
90
90
|
if (!compare) {
|
|
@@ -93,10 +93,51 @@ export function equalIds(...ids: IdsType[]): boolean {
|
|
|
93
93
|
return ids.every((id) => getStringIds(id) === compare);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Get included ids
|
|
98
|
+
* @param includes IdsType, which should be checked if it contains the ID
|
|
99
|
+
* @param ids IdsType, which should be included
|
|
100
|
+
* @param convert If set the result array will be converted to pure type String array or ObjectId array
|
|
101
|
+
* @return IdsType with IDs which are included, undefined if includes or ids are missing or null if none is included
|
|
102
|
+
*/
|
|
103
|
+
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'string'): string[];
|
|
104
|
+
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'object'): Types.ObjectId[];
|
|
105
|
+
export function getIncludedIds<T = IdsType>(
|
|
106
|
+
includes: IdsType,
|
|
107
|
+
ids: T | IdsType,
|
|
108
|
+
convert?: 'string' | 'object'
|
|
109
|
+
): T[] | null | undefined {
|
|
110
|
+
if (!includes || !ids) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!Array.isArray(includes)) {
|
|
115
|
+
includes = [includes];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!Array.isArray(ids)) {
|
|
119
|
+
ids = [ids];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let result = [];
|
|
123
|
+
const includesStrings = getStringIds(includes);
|
|
124
|
+
for (const id of ids) {
|
|
125
|
+
if (includesStrings.includes(getStringIds(id))) {
|
|
126
|
+
result.push(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (convert) {
|
|
131
|
+
result = convert === 'string' ? getStringIds(result) : getObjectIds(result);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result.length ? result : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
96
137
|
/**
|
|
97
138
|
* Get indexes of IDs in an array
|
|
98
139
|
*/
|
|
99
|
-
export function getIndexesViaIds(ids:
|
|
140
|
+
export function getIndexesViaIds(ids: IdsType, array: IdsType): number[] {
|
|
100
141
|
// Check and prepare parameters
|
|
101
142
|
if (!ids) {
|
|
102
143
|
return [];
|
|
@@ -234,19 +275,41 @@ export function getElementsViaIds<T = any>(
|
|
|
234
275
|
/**
|
|
235
276
|
* Get populate options from GraphQL resolve info
|
|
236
277
|
*/
|
|
237
|
-
export function getPopulateOptions(info: GraphQLResolveInfo,
|
|
278
|
+
export function getPopulateOptions(info: GraphQLResolveInfo, dotSeparatedSelect?: string): PopulateOptions[] {
|
|
238
279
|
const result = [];
|
|
239
280
|
|
|
240
281
|
if (!info?.fieldNodes?.length) {
|
|
241
282
|
return result;
|
|
242
283
|
}
|
|
243
284
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
285
|
+
if (dotSeparatedSelect?.length) {
|
|
286
|
+
for (const fieldNode of info.fieldNodes) {
|
|
287
|
+
const selects = dotSeparatedSelect.split('.');
|
|
288
|
+
const fieldNodeName = selects.shift();
|
|
289
|
+
if (fieldNode?.name?.value === fieldNodeName && fieldNode?.selectionSet?.selections?.length) {
|
|
290
|
+
if (!selects.length) {
|
|
291
|
+
return getPopulatOptionsFromSelections(fieldNode.selectionSet.selections);
|
|
292
|
+
} else {
|
|
293
|
+
let selections = fieldNode.selectionSet.selections;
|
|
294
|
+
do {
|
|
295
|
+
if (!selections?.length) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const select = selects.shift();
|
|
299
|
+
for (const selection of selections) {
|
|
300
|
+
if ((selection as FieldNode)?.name?.value === select) {
|
|
301
|
+
selections = (selection as FieldNode)?.selectionSet?.selections;
|
|
302
|
+
if (!selects.length) {
|
|
303
|
+
return getPopulatOptionsFromSelections(selections);
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} while (selects.length);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
247
311
|
}
|
|
248
312
|
}
|
|
249
|
-
|
|
250
313
|
return result;
|
|
251
314
|
}
|
|
252
315
|
|
|
@@ -301,55 +364,6 @@ export function getJSONClone<T = any>(obj: T): Partial<T> {
|
|
|
301
364
|
return JSON.parse(JSON.stringify(obj));
|
|
302
365
|
}
|
|
303
366
|
|
|
304
|
-
/**
|
|
305
|
-
* Check if ID is included
|
|
306
|
-
* @param includes String, ObjectId or array with both, which should be checked if it contains the ID
|
|
307
|
-
* @param ids String, ObjectId or array with both, which should be included
|
|
308
|
-
* @param convert If set the result array will be converted to pure type String array or ObjectId array
|
|
309
|
-
* @return StringOrObjectId[] Array with IDs which are included or null if none is included
|
|
310
|
-
*/
|
|
311
|
-
export function includesIds(
|
|
312
|
-
includes: StringOrObjectId | StringOrObjectId[],
|
|
313
|
-
ids: StringOrObjectId | StringOrObjectId[],
|
|
314
|
-
convert?: 'string'
|
|
315
|
-
): string[];
|
|
316
|
-
export function includesIds(
|
|
317
|
-
includes: StringOrObjectId | StringOrObjectId[],
|
|
318
|
-
ids: StringOrObjectId | StringOrObjectId[],
|
|
319
|
-
convert?: 'object'
|
|
320
|
-
): Types.ObjectId[];
|
|
321
|
-
export function includesIds<T = StringOrObjectId>(
|
|
322
|
-
includes: StringOrObjectId | StringOrObjectId[] | any | any[],
|
|
323
|
-
ids: T | StringOrObjectId[],
|
|
324
|
-
convert?: 'string' | 'object'
|
|
325
|
-
): T[] | null {
|
|
326
|
-
if (!includes || !ids) {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (!Array.isArray(includes)) {
|
|
331
|
-
includes = [includes];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!Array.isArray(ids)) {
|
|
335
|
-
ids = [ids] as any;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
let result = [];
|
|
339
|
-
const includesStrings = getStringIds(includes);
|
|
340
|
-
for (const id of ids as StringOrObjectId[]) {
|
|
341
|
-
if (includesStrings.includes(getStringIds(id))) {
|
|
342
|
-
result.push(id);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (convert) {
|
|
347
|
-
result = convert === 'string' ? getStringIds(result) : getObjectIds(result);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return result.length ? result : null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
367
|
/**
|
|
354
368
|
* Convert all ObjectIds to strings
|
|
355
369
|
*/
|
|
@@ -3,9 +3,9 @@ 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 { ProcessType } from '../enums/process-type.enum';
|
|
6
7
|
import { RoleEnum } from '../enums/role.enum';
|
|
7
|
-
import {
|
|
8
|
-
import { equalIds, getStringIds } from './db.helper';
|
|
8
|
+
import { equalIds } from './db.helper';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Helper class for inputs
|
|
@@ -18,7 +18,13 @@ export default class InputHelper {
|
|
|
18
18
|
public static async check(
|
|
19
19
|
value: any,
|
|
20
20
|
user: { id: any; hasRole: (roles: string[]) => boolean },
|
|
21
|
-
options?: {
|
|
21
|
+
options?: {
|
|
22
|
+
dbObject?: any;
|
|
23
|
+
metatype?: any;
|
|
24
|
+
processType?: ProcessType;
|
|
25
|
+
roles?: string | string[];
|
|
26
|
+
throwError?: boolean;
|
|
27
|
+
}
|
|
22
28
|
): Promise<any> {
|
|
23
29
|
return check(value, user, options);
|
|
24
30
|
}
|
|
@@ -182,7 +188,13 @@ export default class InputHelper {
|
|
|
182
188
|
export async function check(
|
|
183
189
|
value: any,
|
|
184
190
|
user: { id: any; hasRole: (roles: string[]) => boolean },
|
|
185
|
-
options?: {
|
|
191
|
+
options?: {
|
|
192
|
+
dbObject?: any;
|
|
193
|
+
metatype?: any;
|
|
194
|
+
processType?: ProcessType;
|
|
195
|
+
roles?: string | string[];
|
|
196
|
+
throwError?: boolean;
|
|
197
|
+
}
|
|
186
198
|
): Promise<any> {
|
|
187
199
|
const config = {
|
|
188
200
|
throwError: true,
|
|
@@ -196,18 +208,15 @@ export async function check(
|
|
|
196
208
|
roles = [roles];
|
|
197
209
|
}
|
|
198
210
|
let valid = false;
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
if (
|
|
212
|
+
// check if user is logged in
|
|
213
|
+
(roles.includes(RoleEnum.S_USER) && user?.id) ||
|
|
214
|
+
// check if the user has at least one of the required roles
|
|
215
|
+
user.hasRole(roles) ||
|
|
216
|
+
// check if the user is the creator
|
|
217
|
+
(roles.includes(RoleEnum.S_CREATOR) && equalIds(config.dbObject?.createdBy, user))
|
|
218
|
+
) {
|
|
202
219
|
valid = true;
|
|
203
|
-
} else if (roles.includes(RoleEnum.S_CREATOR) && user?.id && equalIds(user.id, config.creator)) {
|
|
204
|
-
valid = true;
|
|
205
|
-
} else if (roles.includes(RoleEnum.S_OWNER) && user?.id && config.ownerIds) {
|
|
206
|
-
let ownerIds: string | string[] = getStringIds(config.ownerIds);
|
|
207
|
-
if (!Array.isArray(ownerIds)) {
|
|
208
|
-
ownerIds = [ownerIds];
|
|
209
|
-
}
|
|
210
|
-
valid = ownerIds.includes(getStringIds(user.id));
|
|
211
220
|
}
|
|
212
221
|
if (!valid) {
|
|
213
222
|
throw new UnauthorizedException('Missing rights');
|
|
@@ -1,7 +1,12 @@
|
|
|
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';
|
|
6
|
+
import { PrepareInputOptions } from '../interfaces/prepare-input-options.interface';
|
|
7
|
+
import { PrepareOutputOptions } from '../interfaces/prepare-output-options.interface';
|
|
8
|
+
import { ResolveSelector } from '../interfaces/resolve-selector.interface';
|
|
9
|
+
import { ServiceOptions } from '../interfaces/service-options.interface';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Helper class for services
|
|
@@ -54,6 +59,7 @@ export async function prepareInput<T = any>(
|
|
|
54
59
|
clone?: boolean;
|
|
55
60
|
getNewArray?: boolean;
|
|
56
61
|
removeUndefined?: boolean;
|
|
62
|
+
targetModel?: new (...args: any[]) => T;
|
|
57
63
|
} = {}
|
|
58
64
|
): Promise<T> {
|
|
59
65
|
// Configuration
|
|
@@ -86,6 +92,15 @@ export async function prepareInput<T = any>(
|
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
// Map input if target model exist
|
|
96
|
+
if (config.targetModel && !(input instanceof config.targetModel)) {
|
|
97
|
+
if ((config.targetModel as any)?.map) {
|
|
98
|
+
input = await (config.targetModel as any).map(input);
|
|
99
|
+
} else {
|
|
100
|
+
input = plainToInstance(config.targetModel, input);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
// Remove undefined properties to avoid unwanted overwrites
|
|
90
105
|
if (config.removeUndefined) {
|
|
91
106
|
Object.keys(input).forEach((key) => input[key] === undefined && delete input[key]);
|
|
@@ -171,8 +186,12 @@ export async function prepareOutput<T = { [key: string]: any; map: (...args: any
|
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
// Map output if target model exist
|
|
174
|
-
if (config.targetModel) {
|
|
175
|
-
|
|
189
|
+
if (config.targetModel && !(output instanceof config.targetModel)) {
|
|
190
|
+
if ((config.targetModel as any)?.map) {
|
|
191
|
+
output = await (config.targetModel as any).map(output);
|
|
192
|
+
} else {
|
|
193
|
+
output = plainToInstance(config.targetModel, output);
|
|
194
|
+
}
|
|
176
195
|
}
|
|
177
196
|
|
|
178
197
|
// Remove password if exists
|
|
@@ -198,3 +217,54 @@ export async function prepareOutput<T = { [key: string]: any; map: (...args: any
|
|
|
198
217
|
// Return prepared output
|
|
199
218
|
return output;
|
|
200
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Prepare service options
|
|
223
|
+
*/
|
|
224
|
+
export function prepareServiceOptions(
|
|
225
|
+
serviceOptions: ServiceOptions,
|
|
226
|
+
options?: {
|
|
227
|
+
clone?: boolean;
|
|
228
|
+
inputType?: any;
|
|
229
|
+
outputType?: any;
|
|
230
|
+
subFieldSelection?: string;
|
|
231
|
+
prepareInput?: PrepareInputOptions;
|
|
232
|
+
prepareOutput?: PrepareOutputOptions;
|
|
233
|
+
}
|
|
234
|
+
): ServiceOptions {
|
|
235
|
+
// Set default values
|
|
236
|
+
const config = {
|
|
237
|
+
clone: true,
|
|
238
|
+
...options,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Clone
|
|
242
|
+
if (serviceOptions && config.clone) {
|
|
243
|
+
serviceOptions = _.cloneDeep(serviceOptions);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Init if not exists
|
|
247
|
+
serviceOptions = serviceOptions || {};
|
|
248
|
+
|
|
249
|
+
// Set properties
|
|
250
|
+
serviceOptions.inputType = serviceOptions.inputType || options?.inputType;
|
|
251
|
+
serviceOptions.outputType = serviceOptions.outputType || options?.outputType;
|
|
252
|
+
|
|
253
|
+
// Set properties which can deactivate handling when falsy
|
|
254
|
+
if (!serviceOptions.prepareInput && 'prepareInput' in config) {
|
|
255
|
+
serviceOptions.prepareInput = config.prepareInput;
|
|
256
|
+
}
|
|
257
|
+
if (!serviceOptions.prepareOutput && 'prepareOutput' in config) {
|
|
258
|
+
serviceOptions.prepareOutput = config.prepareOutput;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Set subfield selection
|
|
262
|
+
if (config.subFieldSelection) {
|
|
263
|
+
if ((serviceOptions.fieldSelection as ResolveSelector)?.select) {
|
|
264
|
+
(serviceOptions.fieldSelection as ResolveSelector).select += '.' + config.subFieldSelection;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Return (cloned and) prepared service options
|
|
269
|
+
return serviceOptions;
|
|
270
|
+
}
|