@lenne.tech/nest-server 11.4.3 → 11.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.4.3",
3
+ "version": "11.4.5",
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",
@@ -7,6 +7,7 @@ import {
7
7
  IsArray,
8
8
  IsBoolean,
9
9
  IsDate,
10
+ IsDefined,
10
11
  IsEnum,
11
12
  IsNotEmpty,
12
13
  IsNumber,
@@ -22,6 +23,10 @@ import { GraphQLScalarType } from 'graphql';
22
23
  import { RoleEnum } from '../enums/role.enum';
23
24
  import { Restricted, RestrictedType } from './restricted.decorator';
24
25
 
26
+ // Registry to store nested type information for validation
27
+ // Key: `${className}.${propertyName}`, Value: nested type constructor
28
+ export const nestedTypeRegistry = new Map<string, any>();
29
+
25
30
  export interface UnifiedFieldOptions {
26
31
  /** Description used for both Swagger & Gql */
27
32
  description?: string;
@@ -128,6 +133,8 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
128
133
  swaggerOpts.nullable = true;
129
134
  swaggerOpts.required = false;
130
135
  } else {
136
+ // Use IsDefined to ensure field is present, then IsNotEmpty to ensure it's not empty
137
+ IsDefined()(target, propertyKey);
131
138
  IsNotEmpty()(target, propertyKey);
132
139
 
133
140
  gqlOpts.nullable = false;
@@ -209,10 +216,20 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
209
216
  }
210
217
 
211
218
  if (!opts.isAny) {
219
+ // Special handling for Date: needs @Type transformation even though it's "primitive"
220
+ // This allows ISO date strings to be transformed to Date objects before validation
221
+ if (baseType === Date) {
222
+ Type(() => Date)(target, propertyKey);
223
+ }
212
224
  // Check if it's a primitive, if not apply transform
213
- if (!isPrimitive(baseType) && !opts.enum && !isGraphQLScalar(baseType)) {
225
+ else if (!isPrimitive(baseType) && !opts.enum && !isGraphQLScalar(baseType)) {
214
226
  Type(() => baseType)(target, propertyKey);
215
227
  ValidateNested({ each: isArrayField })(target, propertyKey);
228
+
229
+ // Store nested type info in registry for use in MapAndValidatePipe
230
+ const className = target.constructor.name;
231
+ const registryKey = `${className}.${String(propertyKey)}`;
232
+ nestedTypeRegistry.set(registryKey, baseType);
216
233
  }
217
234
  }
218
235
 
@@ -197,9 +197,9 @@ export function assignPlain(target: Record<any, any>, ...args: Record<any, any>[
197
197
  target,
198
198
  ...args.map(
199
199
  // Prepare records
200
- item =>
200
+ (item) =>
201
201
  // Return item if not an object or cloned record with undefined properties removed
202
- !item ? item : filterProperties(clone(item, { circles: false }), prop => prop !== undefined),
202
+ !item ? item : filterProperties(clone(item, { circles: false }), (prop) => prop !== undefined),
203
203
  ),
204
204
  );
205
205
  }
@@ -249,20 +249,20 @@ export async function check(
249
249
  // Check access
250
250
  if (
251
251
  // check if any user, including users who are not logged in, can access
252
- roles.includes(RoleEnum.S_EVERYONE)
252
+ roles.includes(RoleEnum.S_EVERYONE) ||
253
253
  // check if user is logged in
254
- || (roles.includes(RoleEnum.S_USER) && user?.id)
254
+ (roles.includes(RoleEnum.S_USER) && user?.id) ||
255
255
  // check if the user has at least one of the required roles
256
- || user?.hasRole?.(roles)
256
+ user?.hasRole?.(roles) ||
257
257
  // check if the user is herself / himself
258
- || (roles.includes(RoleEnum.S_SELF) && equalIds(config.dbObject, user))
258
+ (roles.includes(RoleEnum.S_SELF) && equalIds(config.dbObject, user)) ||
259
259
  // check if the user is the creator
260
- || (roles.includes(RoleEnum.S_CREATOR)
261
- && ((config.dbObject && 'createdBy' in config.dbObject && equalIds(config.dbObject.createdBy, user))
262
- || (config.allowCreatorOfParent
263
- && config.dbObject
264
- && !('createdBy' in config.dbObject)
265
- && config.isCreatorOfParent)))
260
+ (roles.includes(RoleEnum.S_CREATOR) &&
261
+ ((config.dbObject && 'createdBy' in config.dbObject && equalIds(config.dbObject.createdBy, user)) ||
262
+ (config.allowCreatorOfParent &&
263
+ config.dbObject &&
264
+ !('createdBy' in config.dbObject) &&
265
+ config.isCreatorOfParent)))
266
266
  ) {
267
267
  valid = true;
268
268
  }
@@ -299,7 +299,7 @@ export async function check(
299
299
  if ((metatype as any)?.map) {
300
300
  value = (metatype as any)?.map(value);
301
301
  } else {
302
- value = plainToInstance(metatype, value);
302
+ value = plainToInstanceClean(metatype, value);
303
303
  }
304
304
  }
305
305
  }
@@ -435,7 +435,7 @@ export function filterProperties<T = Record<string, any>>(
435
435
  filterFunction: (value?: any, key?: string, obj?: T) => boolean,
436
436
  ): Partial<T> {
437
437
  return Object.keys(obj)
438
- .filter(key => filterFunction(obj[key], key, obj))
438
+ .filter((key) => filterFunction(obj[key], key, obj))
439
439
  .reduce((res, key) => Object.assign(res, { [key]: obj[key] }), {});
440
440
  }
441
441
 
@@ -546,12 +546,12 @@ export function isFalse(parameter: any, falseFunction: (...params) => any = erro
546
546
  * Check if parameter is a valid file
547
547
  */
548
548
  export function isFile(parameter: any, falseFunction: (...params) => any = errorFunction): boolean {
549
- return parameter !== null
550
- && typeof parameter !== 'undefined'
551
- && parameter.name
552
- && parameter.path
553
- && parameter.type
554
- && parameter.size > 0
549
+ return parameter !== null &&
550
+ typeof parameter !== 'undefined' &&
551
+ parameter.name &&
552
+ parameter.path &&
553
+ parameter.type &&
554
+ parameter.size > 0
555
555
  ? true
556
556
  : falseFunction(isFile);
557
557
  }
@@ -589,10 +589,10 @@ export function isLower(
589
589
  * Check if parameter is a non empty array
590
590
  */
591
591
  export function isNonEmptyArray(parameter: any, falseFunction: (...params) => any = errorFunction): boolean {
592
- return parameter !== null
593
- && typeof parameter !== 'undefined'
594
- && parameter.constructor === Array
595
- && parameter.length > 0
592
+ return parameter !== null &&
593
+ typeof parameter !== 'undefined' &&
594
+ parameter.constructor === Array &&
595
+ parameter.length > 0
596
596
  ? true
597
597
  : falseFunction(isNonEmptyArray);
598
598
  }
@@ -601,10 +601,10 @@ export function isNonEmptyArray(parameter: any, falseFunction: (...params) => an
601
601
  * Check if parameter is a non empty object
602
602
  */
603
603
  export function isNonEmptyObject(parameter: any, falseFunction: (...params) => any = errorFunction): boolean {
604
- return parameter !== null
605
- && typeof parameter !== 'undefined'
606
- && parameter.constructor === Object
607
- && Object.keys(parameter).length !== 0
604
+ return parameter !== null &&
605
+ typeof parameter !== 'undefined' &&
606
+ parameter.constructor === Object &&
607
+ Object.keys(parameter).length !== 0
608
608
  ? true
609
609
  : falseFunction(isNonEmptyObject);
610
610
  }
@@ -696,13 +696,51 @@ export function mergePlain(target: Record<any, any>, ...args: Record<any, any>[]
696
696
  target,
697
697
  ...args.map(
698
698
  // Prepare records
699
- item =>
699
+ (item) =>
700
700
  // Return item if not an object or cloned record with undefined properties removed
701
- !item ? item : filterProperties(clone(item, { circles: false }), prop => prop !== undefined),
701
+ !item ? item : filterProperties(clone(item, { circles: false }), (prop) => prop !== undefined),
702
702
  ),
703
703
  );
704
704
  }
705
705
 
706
+ /**
707
+ * Transforms a plain object to an instance of the specified class and removes properties
708
+ * that did not exist in the source and have undefined as class default value.
709
+ *
710
+ * This function handles the special case where:
711
+ * - Property does not exist in source AND class default is undefined → property is deleted
712
+ * - Property exists in source (even with undefined value) → property is kept
713
+ * - Property has non-undefined class default → property is kept
714
+ *
715
+ * @param cls The class to instantiate
716
+ * @param plain The plain object to transform
717
+ * @returns Instance of cls with cleaned properties
718
+ */
719
+ export function plainToInstanceClean<T>(cls: new (...args: any[]) => T, plain: any): T {
720
+ if (!plain || typeof plain !== 'object' || Array.isArray(plain)) {
721
+ return plain;
722
+ }
723
+
724
+ // Transform to instance
725
+ const instance = plainToInstance(cls, plain);
726
+
727
+ // Get class defaults to check which properties have undefined as initial value
728
+ const classDefaults = new cls();
729
+
730
+ // Remove properties that did not exist in source and have undefined as class default
731
+ for (const key in instance) {
732
+ if (
733
+ Object.prototype.hasOwnProperty.call(instance, key) &&
734
+ !Object.prototype.hasOwnProperty.call(plain, key) &&
735
+ classDefaults[key] === undefined
736
+ ) {
737
+ delete instance[key];
738
+ }
739
+ }
740
+
741
+ return instance;
742
+ }
743
+
706
744
  /**
707
745
  * Helper to avoid very slow merge of serviceOptions
708
746
  */
@@ -759,21 +797,21 @@ export function processDeep(
759
797
 
760
798
  // Process array
761
799
  if (Array.isArray(data)) {
762
- return func(data.map(item => processDeep(item, func, { processedObjects, specialClasses })));
800
+ return func(data.map((item) => processDeep(item, func, { processedObjects, specialClasses })));
763
801
  }
764
802
 
765
803
  // Process object
766
804
  if (typeof data === 'object') {
767
805
  if (
768
- specialFunctions.find(sF => typeof data[sF] === 'function')
769
- || specialProperties.find(sP => Object.getOwnPropertyNames(data).includes(sP))
806
+ specialFunctions.find((sF) => typeof data[sF] === 'function') ||
807
+ specialProperties.find((sP) => Object.getOwnPropertyNames(data).includes(sP))
770
808
  ) {
771
809
  return func(data);
772
810
  }
773
811
  for (const specialClass of specialClasses) {
774
812
  if (
775
- (typeof specialClass === 'string' && specialClass === data.constructor?.name)
776
- || (typeof specialClass !== 'string' && data instanceof specialClass)
813
+ (typeof specialClass === 'string' && specialClass === data.constructor?.name) ||
814
+ (typeof specialClass !== 'string' && data instanceof specialClass)
777
815
  ) {
778
816
  return func(data);
779
817
  }
@@ -818,7 +856,7 @@ export function removePropertiesDeep(
818
856
 
819
857
  // Process array
820
858
  if (Array.isArray(data)) {
821
- return data.map(item => removePropertiesDeep(item, properties, { processedObjects }));
859
+ return data.map((item) => removePropertiesDeep(item, properties, { processedObjects }));
822
860
  }
823
861
 
824
862
  // Process object
@@ -1,7 +1,6 @@
1
- import { plainToInstance } from 'class-transformer';
2
1
  import { Types } from 'mongoose';
3
2
 
4
- import { clone } from './input.helper';
3
+ import { clone, plainToInstanceClean } from './input.helper';
5
4
 
6
5
  /**
7
6
  * Helper class for models
@@ -142,7 +141,7 @@ export function mapClasses<T = Record<string, any>>(
142
141
  if (targetClass.map) {
143
142
  arr.push(targetClass.map(item));
144
143
  } else {
145
- arr.push(plainToInstance(targetClass, item));
144
+ arr.push(plainToInstanceClean(targetClass, item));
146
145
  }
147
146
  } else {
148
147
  arr.push(item);
@@ -162,7 +161,7 @@ export function mapClasses<T = Record<string, any>>(
162
161
  if (targetClass.map) {
163
162
  target[prop] = targetClass.map(value);
164
163
  } else {
165
- target[prop] = plainToInstance(targetClass, value) as any;
164
+ target[prop] = plainToInstanceClean(targetClass, value) as any;
166
165
  }
167
166
  }
168
167
 
@@ -229,7 +228,7 @@ export async function mapClassesAsync<T = Record<string, any>>(
229
228
  if (targetClass.map) {
230
229
  arr.push(await targetClass.map(item));
231
230
  } else {
232
- arr.push(plainToInstance(targetClass, item));
231
+ arr.push(plainToInstanceClean(targetClass, item));
233
232
  }
234
233
  } else {
235
234
  arr.push(item);
@@ -249,7 +248,7 @@ export async function mapClassesAsync<T = Record<string, any>>(
249
248
  if (targetClass.map) {
250
249
  target[prop] = await targetClass.map(value);
251
250
  } else {
252
- target[prop] = plainToInstance(targetClass, value) as any;
251
+ target[prop] = plainToInstanceClean(targetClass, value) as any;
253
252
  }
254
253
  }
255
254
 
@@ -351,12 +350,12 @@ export function prepareMap<T = Record<string, any>>(
351
350
  // Update properties
352
351
  for (const key of Object.keys(target)) {
353
352
  if (
354
- (!['_id', 'id'].includes(key) || config.mapId)
355
- && source[key] !== undefined
356
- && (config.funcAllowed || typeof (source[key] !== 'function'))
353
+ (!['_id', 'id'].includes(key) || config.mapId) &&
354
+ source[key] !== undefined &&
355
+ (config.funcAllowed || typeof (source[key] !== 'function'))
357
356
  ) {
358
- result[key]
359
- = source[key] !== 'function' && config.cloneDeep
357
+ result[key] =
358
+ source[key] !== 'function' && config.cloneDeep
360
359
  ? clone(source[key], { circles: config.circles, proto: config.proto })
361
360
  : source[key];
362
361
  } else if (key === 'id' && !config.mapId) {
@@ -1,6 +1,5 @@
1
1
  import { UnauthorizedException } from '@nestjs/common';
2
2
  import bcrypt = require('bcrypt');
3
- import { plainToInstance } from 'class-transformer';
4
3
  import { sha256 } from 'js-sha256';
5
4
  import _ = require('lodash');
6
5
  import { Types } from 'mongoose';
@@ -12,7 +11,7 @@ import { ResolveSelector } from '../interfaces/resolve-selector.interface';
12
11
  import { ServiceOptions } from '../interfaces/service-options.interface';
13
12
  import { ConfigService } from '../services/config.service';
14
13
  import { getStringIds } from './db.helper';
15
- import { clone, processDeep } from './input.helper';
14
+ import { clone, plainToInstanceClean, processDeep } from './input.helper';
16
15
 
17
16
  /**
18
17
  * Helper class for services
@@ -137,7 +136,7 @@ export async function prepareInput<T = any>(
137
136
  if ((config.targetModel as any)?.map) {
138
137
  input = await (config.targetModel as any).map(input);
139
138
  } else {
140
- input = plainToInstance(config.targetModel, input);
139
+ input = plainToInstanceClean(config.targetModel, input);
141
140
  }
142
141
  }
143
142
 
@@ -238,7 +237,7 @@ export async function prepareOutput<T = { [key: string]: any; map: (...args: any
238
237
  if ((config.targetModel as any)?.map) {
239
238
  output = await (config.targetModel as any).map(output);
240
239
  } else {
241
- output = plainToInstance(config.targetModel, output);
240
+ output = plainToInstanceClean(config.targetModel, output);
242
241
  }
243
242
  }
244
243