@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.
Files changed (30) hide show
  1. package/dist/core/common/decorators/translatable.decorator.d.ts +1 -0
  2. package/dist/core/common/decorators/translatable.decorator.js +17 -0
  3. package/dist/core/common/decorators/translatable.decorator.js.map +1 -1
  4. package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -0
  5. package/dist/core/common/decorators/unified-field.decorator.js +16 -55
  6. package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
  7. package/dist/server/modules/user/inputs/user-create.input.d.ts +1 -0
  8. package/dist/server/modules/user/inputs/user-create.input.js +17 -0
  9. package/dist/server/modules/user/inputs/user-create.input.js.map +1 -1
  10. package/dist/server/modules/user/inputs/user.input.d.ts +1 -0
  11. package/dist/server/modules/user/inputs/user.input.js +17 -0
  12. package/dist/server/modules/user/inputs/user.input.js.map +1 -1
  13. package/dist/server/modules/user/user.model.d.ts +2 -0
  14. package/dist/server/modules/user/user.model.js +18 -0
  15. package/dist/server/modules/user/user.model.js.map +1 -1
  16. package/dist/server/modules/user/user.service.d.ts +1 -0
  17. package/dist/server/modules/user/user.service.js +8 -0
  18. package/dist/server/modules/user/user.service.js.map +1 -1
  19. package/dist/test/test.helper.d.ts +1 -0
  20. package/dist/test/test.helper.js +5 -1
  21. package/dist/test/test.helper.js.map +1 -1
  22. package/dist/tsconfig.build.tsbuildinfo +1 -1
  23. package/package.json +2 -2
  24. package/src/core/common/decorators/translatable.decorator.ts +25 -0
  25. package/src/core/common/decorators/unified-field.decorator.ts +41 -74
  26. package/src/server/modules/user/inputs/user-create.input.ts +9 -1
  27. package/src/server/modules/user/inputs/user.input.ts +9 -1
  28. package/src/server/modules/user/user.model.ts +14 -0
  29. package/src/server/modules/user/user.service.ts +9 -0
  30. 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.4",
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": "NODE_ENV=local jest",
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) { // Case if it's a scalar
87
+ if (userType instanceof GraphQLScalarType) {
88
+ // Case if it's a scalar
95
89
  return userType;
96
90
  }
97
- if (
98
- typeof userType === 'function'
99
- && userType.prototype
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
- try { // case if its a factory
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
- if (opts.isOptional) {
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
- // Trims keys with 'undefined' properties.
162
- function trimUndefined<T>(obj: T): Partial<T> {
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
- // Check if it's a primitive, if not apply transform
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 type
266
- && typeof type === 'function'
267
- && typeof type.prototype?.serialize === 'function'
268
- && typeof type.prototype?.parseValue === 'function'
269
- && typeof type.prototype?.parseLiteral === 'function'
270
- || type instanceof GraphQLScalarType;
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
  */
@@ -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