@lenne.tech/nest-server 11.1.4 → 11.1.6
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/translatable.decorator.d.ts +1 -0
- package/dist/core/common/decorators/translatable.decorator.js +17 -0
- package/dist/core/common/decorators/translatable.decorator.js.map +1 -1
- package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -0
- package/dist/core/common/decorators/unified-field.decorator.js +16 -55
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/server/modules/user/inputs/user-create.input.d.ts +1 -0
- package/dist/server/modules/user/inputs/user-create.input.js +17 -0
- package/dist/server/modules/user/inputs/user-create.input.js.map +1 -1
- package/dist/server/modules/user/inputs/user.input.d.ts +1 -0
- package/dist/server/modules/user/inputs/user.input.js +17 -0
- package/dist/server/modules/user/inputs/user.input.js.map +1 -1
- package/dist/server/modules/user/user.model.d.ts +2 -0
- package/dist/server/modules/user/user.model.js +18 -0
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -0
- package/dist/server/modules/user/user.service.js +8 -0
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +5 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/core/common/decorators/translatable.decorator.ts +25 -0
- package/src/core/common/decorators/unified-field.decorator.ts +41 -74
- package/src/server/modules/user/inputs/user-create.input.ts +9 -1
- package/src/server/modules/user/inputs/user.input.ts +9 -1
- package/src/server/modules/user/user.model.ts +14 -0
- package/src/server/modules/user/user.service.ts +9 -0
- package/src/test/test.helper.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.6",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"start:dev:swc": "nest start -b swc -w --type-check",
|
|
40
40
|
"start:local": "NODE_ENV=local nodemon",
|
|
41
41
|
"start:local:swc": "NODE_ENV=local nest start -b swc -w --type-check",
|
|
42
|
-
"test": "
|
|
42
|
+
"test": "npm run test:e2e",
|
|
43
43
|
"test:cov": "NODE_ENV=local jest --coverage",
|
|
44
44
|
"test:debug": "NODE_ENV=local node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
45
45
|
"test:e2e": "NODE_ENV=local jest --config jest-e2e.json --forceExit",
|
|
@@ -22,3 +22,28 @@ export function Translatable(): PropertyDecorator {
|
|
|
22
22
|
Reflect.defineMetadata(TRANSLATABLE_KEY, [...existingProperties, propertyKey], target.constructor);
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
export function updateLanguage<T extends Record<string, any>, K extends readonly (keyof T)[]>(
|
|
27
|
+
language: string,
|
|
28
|
+
input: any,
|
|
29
|
+
oldValue: T,
|
|
30
|
+
translatableFields: string[],
|
|
31
|
+
): T {
|
|
32
|
+
const changedFields: Partial<Pick<T, K[number]>> = {};
|
|
33
|
+
|
|
34
|
+
for (const key of translatableFields) {
|
|
35
|
+
const k = key as keyof T;
|
|
36
|
+
|
|
37
|
+
if (input[k] !== oldValue[k] && input[k] !== undefined) {
|
|
38
|
+
changedFields[k] = input[k];
|
|
39
|
+
input[k] = oldValue[k] as T[typeof k];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
input._translations = input._translations ?? {};
|
|
44
|
+
input._translations[language] = {
|
|
45
|
+
...(input._translations[language] ?? {}),
|
|
46
|
+
...changedFields,
|
|
47
|
+
};
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
@@ -36,7 +36,7 @@ export interface UnifiedFieldOptions {
|
|
|
36
36
|
/** Description used for both Swagger & Gql */
|
|
37
37
|
description?: string;
|
|
38
38
|
/** Enum for class-validator */
|
|
39
|
-
enum?: { enum: EnumAllowedTypes; options?: ValidationOptions };
|
|
39
|
+
enum?: { enum: EnumAllowedTypes; enumName?: string; options?: ValidationOptions };
|
|
40
40
|
/** Example value for swagger api documentation */
|
|
41
41
|
example?: any;
|
|
42
42
|
/** Options for graphql */
|
|
@@ -82,31 +82,26 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
82
82
|
throw new Error(`Array field '${String(propertyKey)}' of '${String(target)}' must have an explicit type`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
if (opts.enum && userType) {
|
|
86
|
-
throw new Error(`Can't set both enum and type of ${String(propertyKey)} in ${target.constructor.name}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
const resolvedTypeFn = (): any => {
|
|
90
|
-
if (opts.enum?.enum) {
|
|
91
|
-
return opts.enum.enum; // Ensure enums are handled directly
|
|
92
|
-
}
|
|
93
86
|
if (userType) {
|
|
94
|
-
if (userType instanceof GraphQLScalarType) {
|
|
87
|
+
if (userType instanceof GraphQLScalarType) {
|
|
88
|
+
// Case if it's a scalar
|
|
95
89
|
return userType;
|
|
96
90
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
&& userType.prototype.constructor === userType
|
|
101
|
-
) { // Case if it's a function
|
|
91
|
+
|
|
92
|
+
if (typeof userType === 'function' && userType.prototype && userType.prototype.constructor === userType) {
|
|
93
|
+
// Case if it's a function
|
|
102
94
|
return userType;
|
|
103
95
|
}
|
|
104
|
-
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// case if its a factory
|
|
105
99
|
return (userType as () => any)();
|
|
106
100
|
} catch {
|
|
107
101
|
return userType;
|
|
108
102
|
}
|
|
109
103
|
}
|
|
104
|
+
|
|
110
105
|
return metadataType;
|
|
111
106
|
};
|
|
112
107
|
|
|
@@ -114,16 +109,24 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
114
109
|
|
|
115
110
|
// Prepare merged options
|
|
116
111
|
const gqlOpts: FieldOptions = { ...opts.gqlOptions };
|
|
117
|
-
const swaggerOpts: ApiPropertyOptions = { ...opts.swaggerApiOptions };
|
|
112
|
+
const swaggerOpts: ApiPropertyOptions & { enumName?: string } = { ...opts.swaggerApiOptions };
|
|
118
113
|
const valOpts: ValidationOptions = { ...opts.validationOptions };
|
|
119
114
|
|
|
120
115
|
// Optionality
|
|
121
116
|
if (opts.isOptional) {
|
|
117
|
+
IsOptional(valOpts)(target, propertyKey);
|
|
118
|
+
|
|
122
119
|
gqlOpts.nullable = true;
|
|
120
|
+
|
|
123
121
|
swaggerOpts.nullable = true;
|
|
122
|
+
swaggerOpts.required = false;
|
|
124
123
|
} else {
|
|
124
|
+
IsNotEmpty()(target, propertyKey);
|
|
125
|
+
|
|
125
126
|
gqlOpts.nullable = false;
|
|
127
|
+
|
|
126
128
|
swaggerOpts.nullable = false;
|
|
129
|
+
swaggerOpts.required = true;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// Description
|
|
@@ -136,18 +139,25 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
136
139
|
swaggerOpts.example = swaggerOpts.example ?? opts.example;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
// Set enum options
|
|
139
143
|
if (opts.enum && opts.enum.enum) {
|
|
140
144
|
swaggerOpts.enum = opts.enum.enum;
|
|
145
|
+
|
|
146
|
+
if (opts.enum.enumName) {
|
|
147
|
+
swaggerOpts.enumName = opts.enum.enumName;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
IsEnum(opts.enum.enum, opts.enum.options)(target, propertyKey);
|
|
141
151
|
}
|
|
152
|
+
|
|
142
153
|
// Array handling
|
|
143
154
|
if (isArrayField) {
|
|
144
155
|
swaggerOpts.isArray = true;
|
|
145
156
|
IsArray(valOpts)(target, propertyKey);
|
|
146
157
|
valOpts.each = true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
IsOptional(valOpts)(target, propertyKey);
|
|
158
|
+
} else {
|
|
159
|
+
swaggerOpts.isArray = false;
|
|
160
|
+
valOpts.each = false;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
// Type function for gql
|
|
@@ -158,54 +168,14 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
158
168
|
// Gql decorator
|
|
159
169
|
Field(gqlTypeFn, gqlOpts)(target, propertyKey);
|
|
160
170
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
164
|
-
return obj;
|
|
165
|
-
}
|
|
166
|
-
const result: any = Array.isArray(obj) ? [] : {};
|
|
167
|
-
for (const key in obj) {
|
|
168
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
169
|
-
const value = (obj as any)[key];
|
|
170
|
-
|
|
171
|
-
if (typeof value === 'object' && value !== null) {
|
|
172
|
-
const cleaned = trimUndefined(value);
|
|
173
|
-
if (Array.isArray(cleaned) ? cleaned.length > 0 : Object.keys(cleaned).length > 0) {
|
|
174
|
-
result[key] = cleaned;
|
|
175
|
-
}
|
|
176
|
-
} else if (value !== undefined) {
|
|
177
|
-
result[key] = value;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
ApiProperty(trimUndefined({
|
|
186
|
-
deprecated: swaggerOpts.deprecated,
|
|
187
|
-
description: swaggerOpts.description,
|
|
188
|
-
enum: swaggerOpts.enum,
|
|
189
|
-
example: swaggerOpts.example,
|
|
190
|
-
examples: swaggerOpts.examples,
|
|
191
|
-
isArray: swaggerOpts.isArray,
|
|
192
|
-
nullable: swaggerOpts.nullable,
|
|
193
|
-
pattern: swaggerOpts.pattern,
|
|
194
|
-
type: () => resolvedTypeFn(),
|
|
195
|
-
}))(target, propertyKey);
|
|
171
|
+
// Swagger decorator
|
|
172
|
+
ApiProperty(swaggerOpts)(target, propertyKey);
|
|
196
173
|
|
|
197
174
|
// Conditional validation
|
|
198
175
|
if (opts.validateIf) {
|
|
199
176
|
ValidateIf(opts.validateIf)(target, propertyKey);
|
|
200
177
|
}
|
|
201
178
|
|
|
202
|
-
// isOptional validation
|
|
203
|
-
if (opts.isOptional) {
|
|
204
|
-
IsOptional()(target, propertyKey);
|
|
205
|
-
} else {
|
|
206
|
-
IsNotEmpty()(target, propertyKey);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
179
|
// Completely skip validation if its any
|
|
210
180
|
if (opts.validator) {
|
|
211
181
|
opts.validator(valOpts).forEach(d => d(target, propertyKey));
|
|
@@ -216,13 +186,8 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
216
186
|
}
|
|
217
187
|
}
|
|
218
188
|
|
|
219
|
-
// Enum validation
|
|
220
|
-
if (opts.enum) {
|
|
221
|
-
IsEnum(opts.enum.enum, opts.enum.options)(target, propertyKey);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
189
|
if (!opts.isAny) {
|
|
225
|
-
|
|
190
|
+
// Check if it's a primitive, if not apply transform
|
|
226
191
|
if (!isPrimitive(baseType) && !opts.enum && !isGraphQLScalar(baseType)) {
|
|
227
192
|
Type(() => baseType)(target, propertyKey);
|
|
228
193
|
ValidateNested({ each: isArrayField })(target, propertyKey);
|
|
@@ -262,12 +227,14 @@ function getBuiltInValidator(
|
|
|
262
227
|
|
|
263
228
|
function isGraphQLScalar(type: any): boolean {
|
|
264
229
|
// CustomScalar check (The CustomScalar interface implements these functions below)
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
230
|
+
return (
|
|
231
|
+
(type
|
|
232
|
+
&& typeof type === 'function'
|
|
233
|
+
&& typeof type.prototype?.serialize === 'function'
|
|
234
|
+
&& typeof type.prototype?.parseValue === 'function'
|
|
235
|
+
&& typeof type.prototype?.parseLiteral === 'function')
|
|
236
|
+
|| type instanceof GraphQLScalarType
|
|
237
|
+
);
|
|
271
238
|
}
|
|
272
239
|
|
|
273
240
|
function isPrimitive(fn: any): boolean {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { InputType } from '@nestjs/graphql';
|
|
1
|
+
import { Field, InputType } from '@nestjs/graphql';
|
|
2
|
+
import { IsOptional } from 'class-validator';
|
|
2
3
|
|
|
3
4
|
import { Restricted } from '../../../../core/common/decorators/restricted.decorator';
|
|
4
5
|
import { RoleEnum } from '../../../../core/common/enums/role.enum';
|
|
@@ -11,4 +12,11 @@ import { CoreUserCreateInput } from '../../../../core/modules/user/inputs/core-u
|
|
|
11
12
|
@Restricted(RoleEnum.ADMIN)
|
|
12
13
|
export class UserCreateInput extends CoreUserCreateInput {
|
|
13
14
|
// Extend UserCreateInput here
|
|
15
|
+
@Field(() => String, {
|
|
16
|
+
description: 'Job Title of the user',
|
|
17
|
+
nullable: true,
|
|
18
|
+
})
|
|
19
|
+
@IsOptional()
|
|
20
|
+
@Restricted(RoleEnum.ADMIN)
|
|
21
|
+
jobTitle?: string = undefined;
|
|
14
22
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { InputType } from '@nestjs/graphql';
|
|
1
|
+
import { Field, InputType } from '@nestjs/graphql';
|
|
2
|
+
import { IsOptional } from 'class-validator';
|
|
2
3
|
|
|
3
4
|
import { Restricted } from '../../../../core/common/decorators/restricted.decorator';
|
|
4
5
|
import { RoleEnum } from '../../../../core/common/enums/role.enum';
|
|
@@ -11,4 +12,11 @@ import { CoreUserInput } from '../../../../core/modules/user/inputs/core-user.in
|
|
|
11
12
|
@Restricted(RoleEnum.ADMIN)
|
|
12
13
|
export class UserInput extends CoreUserInput {
|
|
13
14
|
// Extend UserInput here
|
|
15
|
+
@Field(() => String, {
|
|
16
|
+
description: 'Job Title of the user',
|
|
17
|
+
nullable: true,
|
|
18
|
+
})
|
|
19
|
+
@IsOptional()
|
|
20
|
+
@Restricted(RoleEnum.ADMIN)
|
|
21
|
+
jobTitle?: string = undefined;
|
|
14
22
|
}
|
|
@@ -4,6 +4,7 @@ import { IsEmail, IsOptional } from 'class-validator';
|
|
|
4
4
|
import { Document, Schema } from 'mongoose';
|
|
5
5
|
|
|
6
6
|
import { Restricted } from '../../../core/common/decorators/restricted.decorator';
|
|
7
|
+
import { Translatable } from '../../../core/common/decorators/translatable.decorator';
|
|
7
8
|
import { RoleEnum } from '../../../core/common/enums/role.enum';
|
|
8
9
|
import { CoreUserModel } from '../../../core/modules/user/core-user.model';
|
|
9
10
|
import { PersistenceModel } from '../../common/models/persistence.model';
|
|
@@ -60,6 +61,15 @@ export class User extends CoreUserModel implements PersistenceModel {
|
|
|
60
61
|
@Restricted(RoleEnum.S_EVERYONE)
|
|
61
62
|
override roles: string[] = undefined;
|
|
62
63
|
|
|
64
|
+
@Field(() => String, {
|
|
65
|
+
description: 'Job title of user',
|
|
66
|
+
nullable: true,
|
|
67
|
+
})
|
|
68
|
+
@Prop()
|
|
69
|
+
@Restricted(RoleEnum.S_EVERYONE)
|
|
70
|
+
@Translatable()
|
|
71
|
+
jobTitle?: string = undefined;
|
|
72
|
+
|
|
63
73
|
/**
|
|
64
74
|
* ID of the user who updated the object
|
|
65
75
|
*
|
|
@@ -73,6 +83,10 @@ export class User extends CoreUserModel implements PersistenceModel {
|
|
|
73
83
|
@Restricted(RoleEnum.S_USER)
|
|
74
84
|
updatedBy: string = undefined;
|
|
75
85
|
|
|
86
|
+
@Prop({ default: {}, type: Schema.Types.Mixed })
|
|
87
|
+
@Restricted(RoleEnum.S_EVERYONE)
|
|
88
|
+
_translations?: Record<string, Partial<User>> = undefined;
|
|
89
|
+
|
|
76
90
|
// ===================================================================================================================
|
|
77
91
|
// Methods
|
|
78
92
|
// ===================================================================================================================
|
|
@@ -4,6 +4,7 @@ import fs = require('fs');
|
|
|
4
4
|
import { PubSub } from 'graphql-subscriptions';
|
|
5
5
|
import { Model } from 'mongoose';
|
|
6
6
|
|
|
7
|
+
import { getTranslatablePropertyKeys, updateLanguage } from '../../../core/common/decorators/translatable.decorator';
|
|
7
8
|
import { ServiceOptions } from '../../../core/common/interfaces/service-options.interface';
|
|
8
9
|
import { ConfigService } from '../../../core/common/services/config.service';
|
|
9
10
|
import { EmailService } from '../../../core/common/services/email.service';
|
|
@@ -62,6 +63,14 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
|
|
|
62
63
|
return user;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
override async update(id: string, input: UserInput, serviceOptions?: ServiceOptions): Promise<User> {
|
|
67
|
+
const dbObject = await super.get(id, serviceOptions);
|
|
68
|
+
if (serviceOptions.language && serviceOptions.language !== 'de') {
|
|
69
|
+
input = updateLanguage(serviceOptions.language, input, dbObject as UserInput, getTranslatablePropertyKeys(User));
|
|
70
|
+
}
|
|
71
|
+
return super.update(id, input, serviceOptions);
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
/**
|
|
66
75
|
* Request password reset mail
|
|
67
76
|
*/
|
package/src/test/test.helper.ts
CHANGED
|
@@ -83,6 +83,11 @@ export interface TestGraphQLOptions {
|
|
|
83
83
|
*/
|
|
84
84
|
countOfSubscriptionMessages?: number;
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Language selected by user
|
|
88
|
+
*/
|
|
89
|
+
language?: string;
|
|
90
|
+
|
|
86
91
|
/**
|
|
87
92
|
* Output information in the console
|
|
88
93
|
*/
|
|
@@ -225,6 +230,7 @@ export class TestHelper {
|
|
|
225
230
|
const config = {
|
|
226
231
|
convertEnums: true,
|
|
227
232
|
countOfSubscriptionMessages: 1,
|
|
233
|
+
language: null,
|
|
228
234
|
log: false,
|
|
229
235
|
logError: false,
|
|
230
236
|
prepareArguments: true,
|
|
@@ -234,7 +240,7 @@ export class TestHelper {
|
|
|
234
240
|
};
|
|
235
241
|
|
|
236
242
|
// Init vars
|
|
237
|
-
const { log, logError, statusCode, token, variables } = config;
|
|
243
|
+
const { language, log, logError, statusCode, token, variables } = config;
|
|
238
244
|
|
|
239
245
|
// Init
|
|
240
246
|
let query = '';
|
|
@@ -337,6 +343,10 @@ export class TestHelper {
|
|
|
337
343
|
requestConfig.headers = { authorization: `Bearer ${token}` };
|
|
338
344
|
}
|
|
339
345
|
|
|
346
|
+
if (language) {
|
|
347
|
+
requestConfig.headers = { ...requestConfig.headers, 'accept-language': language };
|
|
348
|
+
}
|
|
349
|
+
|
|
340
350
|
// Get response
|
|
341
351
|
const response = await this.getResponse(token, requestConfig, statusCode, log, logError, variables);
|
|
342
352
|
|