@lenne.tech/nest-server 11.2.0 → 11.3.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 (34) hide show
  1. package/dist/core/common/decorators/unified-field.decorator.d.ts +2 -0
  2. package/dist/core/common/decorators/unified-field.decorator.js +26 -9
  3. package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
  4. package/dist/core/common/models/core-persistence.model.js +2 -2
  5. package/dist/core/common/models/core-persistence.model.js.map +1 -1
  6. package/dist/core/modules/file/core-file-info.model.js +41 -23
  7. package/dist/core/modules/file/core-file-info.model.js.map +1 -1
  8. package/dist/core/modules/user/core-user.model.js +95 -54
  9. package/dist/core/modules/user/core-user.model.js.map +1 -1
  10. package/dist/main.js +22 -0
  11. package/dist/main.js.map +1 -1
  12. package/dist/server/common/models/persistence.model.js +13 -11
  13. package/dist/server/common/models/persistence.model.js.map +1 -1
  14. package/dist/server/modules/auth/auth.model.js +6 -2
  15. package/dist/server/modules/auth/auth.model.js.map +1 -1
  16. package/dist/server/modules/user/user.controller.d.ts +19 -0
  17. package/dist/server/modules/user/user.controller.js +256 -0
  18. package/dist/server/modules/user/user.controller.js.map +1 -0
  19. package/dist/server/modules/user/user.model.js +37 -24
  20. package/dist/server/modules/user/user.model.js.map +1 -1
  21. package/dist/server/modules/user/user.module.js +2 -1
  22. package/dist/server/modules/user/user.module.js.map +1 -1
  23. package/dist/tsconfig.build.tsbuildinfo +1 -1
  24. package/package.json +27 -28
  25. package/src/core/common/decorators/unified-field.decorator.ts +49 -10
  26. package/src/core/common/models/core-persistence.model.ts +3 -3
  27. package/src/core/modules/file/core-file-info.model.ts +40 -22
  28. package/src/core/modules/user/core-user.model.ts +120 -78
  29. package/src/main.ts +25 -0
  30. package/src/server/common/models/persistence.model.ts +15 -13
  31. package/src/server/modules/auth/auth.model.ts +7 -3
  32. package/src/server/modules/user/user.controller.ts +242 -0
  33. package/src/server/modules/user/user.model.ts +39 -26
  34. package/src/server/modules/user/user.module.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.2.0",
3
+ "version": "11.3.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",
@@ -67,18 +67,18 @@
67
67
  "node": ">= 20"
68
68
  },
69
69
  "dependencies": {
70
- "@apollo/gateway": "2.11.3",
70
+ "@apollo/gateway": "2.12.0",
71
71
  "@getbrevo/brevo": "3.0.1",
72
72
  "@nestjs/apollo": "13.1.0",
73
- "@nestjs/common": "11.1.6",
74
- "@nestjs/core": "11.1.6",
73
+ "@nestjs/common": "11.1.8",
74
+ "@nestjs/core": "11.1.8",
75
75
  "@nestjs/graphql": "13.1.0",
76
76
  "@nestjs/jwt": "11.0.1",
77
77
  "@nestjs/mongoose": "11.0.3",
78
78
  "@nestjs/passport": "11.0.5",
79
- "@nestjs/platform-express": "11.1.6",
79
+ "@nestjs/platform-express": "11.1.8",
80
80
  "@nestjs/schedule": "6.0.1",
81
- "@nestjs/swagger": "11.2.0",
81
+ "@nestjs/swagger": "11.2.1",
82
82
  "@nestjs/terminus": "11.0.0",
83
83
  "apollo-server-core": "3.13.0",
84
84
  "apollo-server-express": "3.13.0",
@@ -89,55 +89,52 @@
89
89
  "cookie-parser": "1.4.7",
90
90
  "dotenv": "17.2.3",
91
91
  "ejs": "3.1.10",
92
- "graphql": "16.11.0",
92
+ "graphql": "16.12.0",
93
93
  "graphql-query-complexity": "1.1.0",
94
94
  "graphql-subscriptions": "3.0.0",
95
95
  "graphql-upload": "15.0.2",
96
96
  "js-sha256": "0.11.1",
97
97
  "json-to-graphql-query": "2.3.0",
98
- "light-my-request": "6.6.0",
99
98
  "lodash": "4.17.21",
100
- "mongodb": "6.20.0",
101
- "mongoose": "8.19.1",
99
+ "mongodb": "7.0.0",
100
+ "mongoose": "8.19.3",
102
101
  "multer": "2.0.2",
103
- "node-mailjet": "6.0.9",
104
- "nodemailer": "7.0.9",
105
- "nodemon": "3.1.10",
102
+ "node-mailjet": "6.0.11",
103
+ "nodemailer": "7.0.10",
106
104
  "passport": "0.7.0",
107
105
  "passport-jwt": "4.0.1",
108
106
  "reflect-metadata": "0.2.2",
109
107
  "rfdc": "1.4.1",
110
- "rimraf": "6.0.1",
111
108
  "rxjs": "7.8.2",
112
109
  "yuml-diagram": "1.2.0"
113
110
  },
114
111
  "devDependencies": {
115
112
  "@babel/plugin-proposal-private-methods": "7.18.6",
116
- "@compodoc/compodoc": "1.1.31",
117
- "@lenne.tech/eslint-config-ts": "2.1.3",
113
+ "@compodoc/compodoc": "1.1.32",
114
+ "@lenne.tech/eslint-config-ts": "2.1.4",
118
115
  "@nestjs/cli": "11.0.10",
119
116
  "@nestjs/schematics": "11.0.9",
120
- "@nestjs/testing": "11.1.6",
121
- "@swc/cli": "0.7.8",
122
- "@swc/core": "1.13.5",
117
+ "@nestjs/testing": "11.1.8",
118
+ "@swc/cli": "0.7.9",
119
+ "@swc/core": "1.15.0",
123
120
  "@swc/jest": "0.2.39",
124
121
  "@types/compression": "1.8.1",
125
- "@types/cookie-parser": "1.4.9",
122
+ "@types/cookie-parser": "1.4.10",
126
123
  "@types/ejs": "3.1.5",
127
124
  "@types/express": "4.17.21",
128
125
  "@types/jest": "30.0.0",
129
126
  "@types/lodash": "4.17.20",
130
127
  "@types/multer": "2.0.0",
131
- "@types/node": "24.7.1",
132
- "@types/nodemailer": "7.0.2",
128
+ "@types/node": "24.10.0",
129
+ "@types/nodemailer": "7.0.3",
133
130
  "@types/passport": "1.0.17",
134
131
  "@types/supertest": "6.0.3",
135
- "@typescript-eslint/eslint-plugin": "8.46.0",
136
- "@typescript-eslint/parser": "8.46.0",
132
+ "@typescript-eslint/eslint-plugin": "8.46.3",
133
+ "@typescript-eslint/parser": "8.46.3",
137
134
  "coffeescript": "2.7.0",
138
- "eslint": "9.37.0",
135
+ "eslint": "9.39.1",
139
136
  "eslint-config-prettier": "10.1.8",
140
- "eslint-plugin-unused-imports": "4.2.0",
137
+ "eslint-plugin-unused-imports": "4.3.0",
141
138
  "find-file-up": "2.0.1",
142
139
  "grunt": "1.6.1",
143
140
  "grunt-bg-shell": "2.3.3",
@@ -146,14 +143,16 @@
146
143
  "grunt-sync": "0.8.2",
147
144
  "husky": "9.1.7",
148
145
  "jest": "30.2.0",
146
+ "nodemon": "3.1.10",
149
147
  "npm-watch": "0.13.0",
150
148
  "pm2": "6.0.13",
151
149
  "prettier": "3.6.2",
152
150
  "pretty-quick": "4.2.2",
151
+ "rimraf": "6.1.0",
153
152
  "supertest": "7.1.4",
154
153
  "ts-jest": "29.4.5",
155
154
  "ts-loader": "9.5.4",
156
- "ts-morph": "27.0.0",
155
+ "ts-morph": "27.0.2",
157
156
  "ts-node": "10.9.2",
158
157
  "tsconfig-paths": "4.2.0",
159
158
  "typescript": "5.9.3",
@@ -164,7 +163,7 @@
164
163
  "flat": "5.0.2",
165
164
  "mime": "2.6.0"
166
165
  },
167
- "ts-morph": "27.0.0"
166
+ "ts-morph": "27.0.2"
168
167
  },
169
168
  "jest": {
170
169
  "collectCoverage": true,
@@ -1,4 +1,5 @@
1
1
  import { Field, FieldOptions } from '@nestjs/graphql';
2
+ import { Prop, PropOptions } from '@nestjs/mongoose';
2
3
  import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
3
4
  import { EnumAllowedTypes } from '@nestjs/swagger/dist/interfaces/schema-object-metadata.interface';
4
5
  import { Type } from 'class-transformer';
@@ -38,6 +39,8 @@ export interface UnifiedFieldOptions {
38
39
  isArray?: boolean;
39
40
  /** Default: false */
40
41
  isOptional?: boolean;
42
+ /** Whether to apply Mongoose @Prop decorator. Optional, used for database models. Default: false */
43
+ mongoose?: boolean | PropOptions;
41
44
  /** Restricted roles */
42
45
  roles?: RestrictedType | RoleEnum | RoleEnum[];
43
46
  /** Options for swagger api documentation */
@@ -48,8 +51,12 @@ export interface UnifiedFieldOptions {
48
51
  *
49
52
  * Enums should be defined via the enum option.
50
53
  *
54
+ * For array fields, you can use either:
55
+ * - `type: () => ItemType` (recommended, decorator adds array wrapping automatically)
56
+ * - `type: () => [ItemType]` (also supported, decorator extracts ItemType to avoid double-nesting)
57
+ *
51
58
  * Supports:
52
- * - A factory function that returns the type: `() => MyType`
59
+ * - A factory function that returns the type: `() => MyType` or `() => [MyType]`
53
60
  * - A GraphQL scalar: `GraphQLScalarType`
54
61
  * - A class constructor: `MyClass`
55
62
  * */
@@ -96,7 +103,16 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
96
103
  return metadataType;
97
104
  };
98
105
 
99
- const baseType = resolvedTypeFn();
106
+ // Resolve the type and extract item type if user provided array syntax
107
+ const resolvedType = resolvedTypeFn();
108
+
109
+ // If this is an array field and the user provided type: () => [ItemType],
110
+ // extract the ItemType to avoid double-nesting (e.g., [[ItemType]])
111
+ // We check: isArrayField (should be array) && Array.isArray (is actually an array) && length === 1 (GraphQL array syntax)
112
+ const baseType =
113
+ isArrayField && Array.isArray(resolvedType) && resolvedType.length === 1
114
+ ? resolvedType[0] // Extract item type from [ItemType]
115
+ : resolvedType; // Use as-is
100
116
 
101
117
  // Prepare merged options
102
118
  const gqlOpts: FieldOptions = { ...opts.gqlOptions };
@@ -161,8 +177,14 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
161
177
  }
162
178
 
163
179
  // Type function for gql
180
+ // We need to keep the factory pattern (calling resolvedTypeFn inside the arrow function)
181
+ // to support circular references. But we also need to extract array item types to avoid double-nesting.
164
182
  const gqlTypeFn = isArrayField
165
- ? () => [opts.enum?.enum || opts.gqlType || resolvedTypeFn()]
183
+ ? () => {
184
+ const resolved = opts.enum?.enum || opts.gqlType || resolvedTypeFn();
185
+ // Extract item type if user provided [ItemType] syntax to avoid [[ItemType]]
186
+ return [Array.isArray(resolved) && resolved.length === 1 ? resolved[0] : resolved];
187
+ }
166
188
  : () => opts.enum?.enum || opts.gqlType || resolvedTypeFn();
167
189
 
168
190
  // Gql decorator
@@ -178,7 +200,7 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
178
200
 
179
201
  // Completely skip validation if its any
180
202
  if (opts.validator) {
181
- opts.validator(valOpts).forEach(d => d(target, propertyKey));
203
+ opts.validator(valOpts).forEach((d) => d(target, propertyKey));
182
204
  } else if (!opts.isAny) {
183
205
  const validator = getBuiltInValidator(baseType, valOpts, isArrayField, target);
184
206
  if (validator) {
@@ -199,6 +221,23 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
199
221
  const rolesArr = Array.isArray(opts.roles) ? opts.roles : [opts.roles];
200
222
  Restricted(...rolesArr)(target, propertyKey);
201
223
  }
224
+
225
+ // Mongoose @Prop decorator (optional)
226
+ if (opts.mongoose) {
227
+ const propOptions: any = typeof opts.mongoose === 'object' ? opts.mongoose : {};
228
+
229
+ // Set type for Prop if not already defined in propOptions
230
+ if (typeof propOptions === 'object' && !Array.isArray(propOptions) && !propOptions.type && baseType) {
231
+ propOptions.type = baseType;
232
+ }
233
+
234
+ // Apply array type if needed
235
+ if (typeof propOptions === 'object' && !Array.isArray(propOptions) && isArrayField && !propOptions.type) {
236
+ propOptions.type = [baseType];
237
+ }
238
+
239
+ Prop(propOptions)(target, propertyKey);
240
+ }
202
241
  };
203
242
  }
204
243
 
@@ -228,12 +267,12 @@ function getBuiltInValidator(
228
267
  function isGraphQLScalar(type: any): boolean {
229
268
  // CustomScalar check (The CustomScalar interface implements these functions below)
230
269
  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
270
+ (type &&
271
+ typeof type === 'function' &&
272
+ typeof type.prototype?.serialize === 'function' &&
273
+ typeof type.prototype?.parseValue === 'function' &&
274
+ typeof type.prototype?.parseLiteral === 'function') ||
275
+ type instanceof GraphQLScalarType
237
276
  );
238
277
  }
239
278
 
@@ -1,5 +1,5 @@
1
1
  import { ObjectType } from '@nestjs/graphql';
2
- import { Prop, Schema } from '@nestjs/mongoose';
2
+ import { Schema } from '@nestjs/mongoose';
3
3
  import { Types } from 'mongoose';
4
4
 
5
5
  import { Restricted } from '../decorators/restricted.decorator';
@@ -85,10 +85,10 @@ export abstract class CorePersistenceModel extends CoreModel {
85
85
  /**
86
86
  * Created date, is set automatically by mongoose
87
87
  */
88
- @Prop()
89
88
  @UnifiedField({
90
89
  description: 'Created date',
91
90
  isOptional: true,
91
+ mongoose: true,
92
92
  roles: RoleEnum.S_EVERYONE,
93
93
  swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
94
94
  type: Date,
@@ -98,10 +98,10 @@ export abstract class CorePersistenceModel extends CoreModel {
98
98
  /**
99
99
  * Updated date is set automatically by mongoose
100
100
  */
101
- @Prop()
102
101
  @UnifiedField({
103
102
  description: 'Updated date',
104
103
  isOptional: true,
104
+ mongoose: true,
105
105
  roles: RoleEnum.S_EVERYONE,
106
106
  swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
107
107
  type: Date,
@@ -1,8 +1,8 @@
1
- import { Field, ObjectType } from '@nestjs/graphql';
2
- import { Prop } from '@nestjs/mongoose';
1
+ import { ObjectType } from '@nestjs/graphql';
3
2
  import { Types } from 'mongoose';
4
3
 
5
4
  import { Restricted } from '../../common/decorators/restricted.decorator';
5
+ import { UnifiedField } from '../../common/decorators/unified-field.decorator';
6
6
  import { RoleEnum } from '../../common/enums/role.enum';
7
7
  import { CoreModel } from '../../common/models/core-model.model';
8
8
 
@@ -25,37 +25,55 @@ export class CoreFileInfo extends CoreModel {
25
25
  // Properties
26
26
  // ===========================================================================
27
27
 
28
- @Field(() => String, { description: 'ID of the file' })
29
- @Restricted(RoleEnum.S_EVERYONE)
28
+ @UnifiedField({
29
+ description: 'ID of the file',
30
+ roles: RoleEnum.S_EVERYONE,
31
+ type: () => String,
32
+ })
30
33
  id: string = undefined;
31
34
 
32
- @Field(() => Number, {
35
+ @UnifiedField({
33
36
  description:
34
- 'The size of each chunk in bytes. GridFS divides the document into chunks of size chunkSize, '
35
- + 'except for the last, which is only as large as needed. The default size is 255 kilobytes (kB)',
36
- nullable: true,
37
+ 'The size of each chunk in bytes. GridFS divides the document into chunks of size chunkSize, ' +
38
+ 'except for the last, which is only as large as needed. The default size is 255 kilobytes (kB)',
39
+ isOptional: true,
40
+ mongoose: { required: false, type: Number },
41
+ roles: RoleEnum.S_EVERYONE,
42
+ type: () => Number,
37
43
  })
38
- @Prop({ required: false, type: Number })
39
- @Restricted(RoleEnum.S_EVERYONE)
40
44
  chunkSize: number = undefined;
41
45
 
42
- @Field(() => String, { description: 'Content type', nullable: true })
43
- @Prop({ required: false, type: String })
44
- @Restricted(RoleEnum.S_EVERYONE)
46
+ @UnifiedField({
47
+ description: 'Content type',
48
+ isOptional: true,
49
+ mongoose: { required: false, type: String },
50
+ roles: RoleEnum.S_EVERYONE,
51
+ })
45
52
  contentType?: string = undefined;
46
53
 
47
- @Field(() => String, { description: 'Name of the file', nullable: true })
48
- @Prop({ required: false, type: String })
49
- @Restricted(RoleEnum.S_EVERYONE)
54
+ @UnifiedField({
55
+ description: 'Name of the file',
56
+ isOptional: true,
57
+ mongoose: { required: false, type: String },
58
+ roles: RoleEnum.S_EVERYONE,
59
+ })
50
60
  filename?: string = undefined;
51
61
 
52
- @Field(() => Number, { description: 'The size of the document in bytes', nullable: true })
53
- @Prop({ required: false, type: Number })
54
- @Restricted(RoleEnum.S_EVERYONE)
62
+ @UnifiedField({
63
+ description: 'The size of the document in bytes',
64
+ isOptional: true,
65
+ mongoose: { required: false, type: Number },
66
+ roles: RoleEnum.S_EVERYONE,
67
+ type: () => Number,
68
+ })
55
69
  length: number = undefined;
56
70
 
57
- @Field(() => Date, { description: 'The date the file was first stored', nullable: true })
58
- @Prop({ required: false, type: Date })
59
- @Restricted(RoleEnum.S_EVERYONE)
71
+ @UnifiedField({
72
+ description: 'The date the file was first stored',
73
+ isOptional: true,
74
+ mongoose: { required: false, type: Date },
75
+ roles: RoleEnum.S_EVERYONE,
76
+ type: () => Date,
77
+ })
60
78
  uploadDate: Date = undefined;
61
79
  }
@@ -1,10 +1,10 @@
1
- import { Field, ObjectType } from '@nestjs/graphql';
1
+ import { ObjectType } from '@nestjs/graphql';
2
2
  import { Schema as MongooseSchema, Prop, raw } from '@nestjs/mongoose';
3
3
  import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
4
- import { IsOptional } from 'class-validator';
5
4
  import { Document } from 'mongoose';
6
5
 
7
6
  import { Restricted } from '../../common/decorators/restricted.decorator';
7
+ import { UnifiedField } from '../../common/decorators/unified-field.decorator';
8
8
  import { RoleEnum } from '../../common/enums/role.enum';
9
9
  import { CorePersistenceModel } from '../../common/models/core-persistence.model';
10
10
  import { CoreTokenData } from '../auth/interfaces/core-token-data.interface';
@@ -26,67 +26,78 @@ export abstract class CoreUserModel extends CorePersistenceModel {
26
26
  /**
27
27
  * E-Mail address of the user
28
28
  */
29
- @ApiProperty()
30
- @Field({ description: 'Email of the user', nullable: true })
31
- @Prop({ index: true, lowercase: true, trim: true })
32
- @Restricted(RoleEnum.S_EVERYONE)
29
+ @UnifiedField({
30
+ description: 'Email of the user',
31
+ isOptional: true,
32
+ mongoose: { index: true, lowercase: true, trim: true },
33
+ roles: RoleEnum.S_EVERYONE,
34
+ })
33
35
  email: string = undefined;
34
36
 
35
37
  /**
36
38
  * First name of the user
37
39
  */
38
- @ApiProperty()
39
- @Field({ description: 'First name of the user', nullable: true })
40
- @IsOptional()
41
- @Prop()
42
- @Restricted(RoleEnum.S_EVERYONE)
40
+ @UnifiedField({
41
+ description: 'First name of the user',
42
+ isOptional: true,
43
+ mongoose: true,
44
+ roles: RoleEnum.S_EVERYONE,
45
+ })
43
46
  firstName: string = undefined;
44
47
 
45
48
  /**
46
49
  * Last name of the user
47
50
  */
48
- @ApiProperty()
49
- @Field({ description: 'Last name of the user', nullable: true })
50
- @IsOptional()
51
- @Prop()
52
- @Restricted(RoleEnum.S_EVERYONE)
51
+ @UnifiedField({
52
+ description: 'Last name of the user',
53
+ isOptional: true,
54
+ mongoose: true,
55
+ roles: RoleEnum.S_EVERYONE,
56
+ })
53
57
  lastName: string = undefined;
54
58
 
55
59
  /**
56
60
  * Password of the user
57
61
  */
58
- @ApiProperty()
59
- @Prop()
60
- @Restricted(RoleEnum.S_NO_ONE)
62
+ @UnifiedField({
63
+ isOptional: true,
64
+ mongoose: true,
65
+ roles: RoleEnum.S_NO_ONE,
66
+ })
61
67
  password: string = undefined;
62
68
 
63
69
  /**
64
70
  * Roles of the user
65
71
  */
66
- @ApiProperty()
67
- @Field(() => [String], { description: 'Roles of the user', nullable: true })
68
- @IsOptional()
69
- @Prop([String])
70
- @Restricted(RoleEnum.S_EVERYONE)
72
+ @UnifiedField({
73
+ description: 'Roles of the user',
74
+ isArray: true,
75
+ isOptional: true,
76
+ mongoose: [String],
77
+ roles: RoleEnum.S_EVERYONE,
78
+ type: () => String,
79
+ })
71
80
  roles: string[] = undefined;
72
81
 
73
82
  /**
74
83
  * Username of the user
75
84
  */
76
- @ApiProperty()
77
- @Field({ description: 'Username of the user', nullable: true })
78
- @IsOptional()
79
- @Prop()
80
- @Restricted(RoleEnum.S_EVERYONE)
85
+ @UnifiedField({
86
+ description: 'Username of the user',
87
+ isOptional: true,
88
+ mongoose: true,
89
+ roles: RoleEnum.S_EVERYONE,
90
+ })
81
91
  username: string = undefined;
82
92
 
83
93
  /**
84
94
  * Password reset token of the user
85
95
  */
86
- @ApiProperty()
87
- @IsOptional()
88
- @Prop()
89
- @Restricted(RoleEnum.S_NO_ONE)
96
+ @UnifiedField({
97
+ isOptional: true,
98
+ mongoose: true,
99
+ roles: RoleEnum.S_NO_ONE,
100
+ })
90
101
  passwordResetToken: string = undefined;
91
102
 
92
103
  /**
@@ -96,30 +107,41 @@ export abstract class CoreUserModel extends CorePersistenceModel {
96
107
  */
97
108
  @ApiProperty({ isArray: true })
98
109
  @ApiProperty({
99
- additionalProperties: {
110
+ additionalProperties: {
100
111
  properties: {
101
- deviceDescription: { description: 'Description of the device from which the token was generated', nullable: true, type: 'string' },
102
- deviceId: { description: 'ID of the device from which the token was generated', nullable: true, type: 'string' },
103
- tokenId: { description: 'Token ID to make sure that there is only one RefreshToken for each device', nullable: false, type: 'string' },
112
+ deviceDescription: {
113
+ description: 'Description of the device from which the token was generated',
114
+ nullable: true,
115
+ type: 'string',
116
+ },
117
+ deviceId: {
118
+ description: 'ID of the device from which the token was generated',
119
+ nullable: true,
120
+ type: 'string',
121
+ },
122
+ tokenId: {
123
+ description: 'Token ID to make sure that there is only one RefreshToken for each device',
124
+ nullable: false,
125
+ type: 'string',
126
+ },
104
127
  },
105
128
  type: 'object',
106
129
  },
107
130
  description: 'Refresh tokens for devices (key: Token, value: TokenData)',
108
131
  example: {
109
- '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb': {
110
- deviceDescription: null,
111
- deviceId: '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb',
112
- tokenId: '50937407-4282-480e-8679-14ecc113f9c7',
113
- },
114
- 'e9e60a3e-2004-479f-8e79-13a0d1981d76': {
115
- deviceDescription: null,
116
- deviceId: 'e9e60a3e-2004-479f-8e79-13a0d1981d76',
117
- tokenId: '0604aa59-4fc8-4848-9fe7-c12d9cdf6ec0',
118
- },
132
+ '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb': {
133
+ deviceDescription: null,
134
+ deviceId: '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb',
135
+ tokenId: '50937407-4282-480e-8679-14ecc113f9c7',
136
+ },
137
+ 'e9e60a3e-2004-479f-8e79-13a0d1981d76': {
138
+ deviceDescription: null,
139
+ deviceId: 'e9e60a3e-2004-479f-8e79-13a0d1981d76',
140
+ tokenId: '0604aa59-4fc8-4848-9fe7-c12d9cdf6ec0',
119
141
  },
142
+ },
120
143
  type: 'object',
121
144
  })
122
- @IsOptional()
123
145
  @Prop(raw({}))
124
146
  @Restricted(RoleEnum.S_NO_ONE)
125
147
  refreshTokens: Record<string, CoreTokenData> = undefined;
@@ -130,30 +152,44 @@ export abstract class CoreUserModel extends CorePersistenceModel {
130
152
  */
131
153
  @ApiProperty()
132
154
  @ApiProperty({
133
- additionalProperties: {
155
+ additionalProperties: {
134
156
  properties: {
135
- createdAt: { description: 'Token Created At', example: 1740037703939, format: 'int64', nullable: true, type: 'number' },
136
- deviceId: { description: 'ID of the device from which the token was generated', nullable: true, type: 'string' },
137
- tokenId: { description: 'Token ID to make sure that there is only one RefreshToken for each device', nullable: false, type: 'string' },
157
+ createdAt: {
158
+ description: 'Token Created At',
159
+ example: 1740037703939,
160
+ format: 'int64',
161
+ nullable: true,
162
+ type: 'number',
163
+ },
164
+ deviceId: {
165
+ description: 'ID of the device from which the token was generated',
166
+ nullable: true,
167
+ type: 'string',
168
+ },
169
+ tokenId: {
170
+ description: 'Token ID to make sure that there is only one RefreshToken for each device',
171
+ nullable: false,
172
+ type: 'string',
173
+ },
138
174
  },
139
175
  type: 'object',
140
176
  },
141
177
  description: 'Temporary token for parallel requests during the token refresh process',
142
- example: { // 👈 Add explicit example keys
143
- '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb': {
144
- createdAt: 1740037703939,
145
- deviceId: '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb',
146
- tokenId: '50937407-4282-480e-8679-14ecc113f9c7',
147
- },
148
- 'f83ae5f6-90bf-4b4e-b318-651e0eaa67ae': {
149
- createdAt: 1740037703940,
150
- deviceId: 'f83ae5f6-90bf-4b4e-b318-651e0eaa67ae',
151
- tokenId: '4f0dc3c5-e74e-41f4-9bd9-642869462c1e',
152
- },
178
+ example: {
179
+ // 👈 Add explicit example keys
180
+ '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb': {
181
+ createdAt: 1740037703939,
182
+ deviceId: '49b5c7d6-94ae-4efe-b377-9b50d1a9c2cb',
183
+ tokenId: '50937407-4282-480e-8679-14ecc113f9c7',
153
184
  },
185
+ 'f83ae5f6-90bf-4b4e-b318-651e0eaa67ae': {
186
+ createdAt: 1740037703940,
187
+ deviceId: 'f83ae5f6-90bf-4b4e-b318-651e0eaa67ae',
188
+ tokenId: '4f0dc3c5-e74e-41f4-9bd9-642869462c1e',
189
+ },
190
+ },
154
191
  type: 'object',
155
192
  })
156
- @IsOptional()
157
193
  @Prop(raw({}))
158
194
  @Restricted(RoleEnum.S_NO_ONE)
159
195
  tempTokens: Record<string, { createdAt: number; deviceId: string; tokenId: string }> = undefined;
@@ -161,28 +197,34 @@ export abstract class CoreUserModel extends CorePersistenceModel {
161
197
  /**
162
198
  * Verification token of the user
163
199
  */
164
- @ApiProperty()
165
- @IsOptional()
166
- @Prop()
167
- @Restricted(RoleEnum.S_NO_ONE)
200
+ @UnifiedField({
201
+ isOptional: true,
202
+ mongoose: true,
203
+ roles: RoleEnum.S_NO_ONE,
204
+ })
168
205
  verificationToken: string = undefined;
169
206
 
170
207
  /**
171
208
  * Verification of the user
172
209
  */
173
- @ApiProperty()
174
- @Field(() => Boolean, { description: 'Verification state of the user', nullable: true })
175
- @Prop({ type: Boolean })
176
- @Restricted(RoleEnum.S_EVERYONE)
210
+ @UnifiedField({
211
+ description: 'Verification state of the user',
212
+ isOptional: true,
213
+ mongoose: { type: Boolean },
214
+ roles: RoleEnum.S_EVERYONE,
215
+ type: () => Boolean,
216
+ })
177
217
  verified: boolean = undefined;
178
218
 
179
219
  /**
180
220
  * Verification date
181
221
  */
182
- @ApiProperty()
183
- @Field({ description: 'Verified date', nullable: true })
184
- @Prop()
185
- @Restricted(RoleEnum.S_EVERYONE)
222
+ @UnifiedField({
223
+ description: 'Verified date',
224
+ isOptional: true,
225
+ mongoose: true,
226
+ roles: RoleEnum.S_EVERYONE,
227
+ })
186
228
  verifiedAt: Date = undefined;
187
229
 
188
230
  // ===================================================================================================================
@@ -199,7 +241,7 @@ export abstract class CoreUserModel extends CorePersistenceModel {
199
241
  if (!this.roles || this.roles.length < 1) {
200
242
  return false;
201
243
  }
202
- return !roles || roles.length < 1 ? true : this.roles.some(role => roles.includes(role));
244
+ return !roles || roles.length < 1 ? true : this.roles.some((role) => roles.includes(role));
203
245
  }
204
246
 
205
247
  /**
@@ -212,7 +254,7 @@ export abstract class CoreUserModel extends CorePersistenceModel {
212
254
  if (!this.roles || this.roles.length < 1) {
213
255
  return false;
214
256
  }
215
- return !roles ? true : roles.every(role => this.roles.includes(role));
257
+ return !roles ? true : roles.every((role) => this.roles.includes(role));
216
258
  }
217
259
 
218
260
  /**