@lenne.tech/nest-server 11.4.2 → 11.4.4
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/helpers/input.helper.d.ts +1 -0
- package/dist/core/common/helpers/input.helper.js +50 -34
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/model.helper.js +9 -10
- package/dist/core/common/helpers/model.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +2 -3
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/pipes/map-and-validate.pipe.js +26 -7
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/helpers/input.helper.ts +74 -36
- package/src/core/common/helpers/model.helper.ts +10 -11
- package/src/core/common/helpers/service.helper.ts +3 -4
- package/src/core/common/pipes/map-and-validate.pipe.ts +32 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.4.
|
|
3
|
+
"version": "11.4.4",
|
|
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",
|
|
@@ -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
|
-
|
|
254
|
+
(roles.includes(RoleEnum.S_USER) && user?.id) ||
|
|
255
255
|
// check if the user has at least one of the required roles
|
|
256
|
-
|
|
256
|
+
user?.hasRole?.(roles) ||
|
|
257
257
|
// check if the user is herself / himself
|
|
258
|
-
|
|
258
|
+
(roles.includes(RoleEnum.S_SELF) && equalIds(config.dbObject, user)) ||
|
|
259
259
|
// check if the user is the creator
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 =
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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] =
|
|
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(
|
|
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] =
|
|
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
|
-
|
|
356
|
-
|
|
353
|
+
(!['_id', 'id'].includes(key) || config.mapId) &&
|
|
354
|
+
source[key] !== undefined &&
|
|
355
|
+
(config.funcAllowed || typeof (source[key] !== 'function'))
|
|
357
356
|
) {
|
|
358
|
-
result[key]
|
|
359
|
-
|
|
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 =
|
|
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 =
|
|
240
|
+
output = plainToInstanceClean(config.targetModel, output);
|
|
242
241
|
}
|
|
243
242
|
}
|
|
244
243
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
|
|
2
|
-
import { plainToInstance } from 'class-transformer';
|
|
3
2
|
import { validate, ValidationError } from 'class-validator';
|
|
3
|
+
import { inspect } from 'util';
|
|
4
4
|
|
|
5
|
-
import { isBasicType } from '../helpers/input.helper';
|
|
5
|
+
import { isBasicType, plainToInstanceClean } from '../helpers/input.helper';
|
|
6
6
|
|
|
7
7
|
// Debug mode can be enabled via environment variable: DEBUG_VALIDATION=true
|
|
8
8
|
const DEBUG_VALIDATION = process.env.DEBUG_VALIDATION === 'true';
|
|
@@ -20,7 +20,7 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
20
20
|
type: metadata.type,
|
|
21
21
|
});
|
|
22
22
|
console.debug('Input value type:', typeof value);
|
|
23
|
-
console.debug('Input value:',
|
|
23
|
+
console.debug('Input value:', inspect(value, { colors: true, depth: 3 }));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (!value || typeof value !== 'object' || !metatype || isBasicType(metatype)) {
|
|
@@ -40,11 +40,11 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
40
40
|
value = (metatype as any)?.map(value);
|
|
41
41
|
} else {
|
|
42
42
|
if (DEBUG_VALIDATION) {
|
|
43
|
-
console.debug('Using
|
|
43
|
+
console.debug('Using plainToInstanceClean to transform to:', metatype.name);
|
|
44
44
|
}
|
|
45
|
-
value =
|
|
45
|
+
value = plainToInstanceClean(metatype, value);
|
|
46
46
|
if (DEBUG_VALIDATION) {
|
|
47
|
-
console.debug('Transformed value:',
|
|
47
|
+
console.debug('Transformed value:', inspect(value, { colors: true, depth: 3 }));
|
|
48
48
|
console.debug('Transformed value instance of:', value?.constructor?.name);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -80,6 +80,7 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const result = {};
|
|
83
|
+
const errorSummary: string[] = [];
|
|
83
84
|
|
|
84
85
|
const processErrors = (errorList: ValidationError[], parentKey = '') => {
|
|
85
86
|
errorList.forEach((e) => {
|
|
@@ -89,6 +90,11 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
89
90
|
processErrors(e.children, key);
|
|
90
91
|
} else {
|
|
91
92
|
result[key] = e.constraints;
|
|
93
|
+
// Build error summary without exposing values
|
|
94
|
+
if (e.constraints) {
|
|
95
|
+
const constraintTypes = Object.keys(e.constraints).join(', ');
|
|
96
|
+
errorSummary.push(`${key} (${constraintTypes})`);
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
});
|
|
94
100
|
};
|
|
@@ -97,12 +103,30 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
97
103
|
|
|
98
104
|
if (DEBUG_VALIDATION) {
|
|
99
105
|
console.debug('\nProcessed validation result:');
|
|
100
|
-
console.debug(
|
|
106
|
+
console.debug(inspect(result, { colors: true, depth: 5 }));
|
|
101
107
|
console.debug('Result is empty:', Object.keys(result).length === 0);
|
|
108
|
+
console.debug('Error summary:', errorSummary);
|
|
102
109
|
console.debug('=== End Debug ===\n');
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
// Create meaningful error message without exposing sensitive values
|
|
113
|
+
let errorMessage = 'Validation failed';
|
|
114
|
+
if (errorSummary.length > 0) {
|
|
115
|
+
const fieldCount = errorSummary.length;
|
|
116
|
+
const fieldWord = fieldCount === 1 ? 'field' : 'fields';
|
|
117
|
+
errorMessage = `Validation failed for ${fieldCount} ${fieldWord}: ${errorSummary.join('; ')}`;
|
|
118
|
+
} else if (errors.length > 0) {
|
|
119
|
+
// Handle case where there are validation errors but no constraints (nested errors only)
|
|
120
|
+
const topLevelProperties = errors.map((e) => e.property).join(', ');
|
|
121
|
+
errorMessage = `Validation failed for properties: ${topLevelProperties} (nested validation errors)`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Throw with message and validation errors (backward compatible structure)
|
|
125
|
+
// Add message property to result object for better error messages
|
|
126
|
+
throw new BadRequestException({
|
|
127
|
+
message: errorMessage,
|
|
128
|
+
...result,
|
|
129
|
+
});
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
if (DEBUG_VALIDATION) {
|