@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/dist/core/common/decorators/unified-field.decorator.d.ts +1 -0
- package/dist/core/common/decorators/unified-field.decorator.js +10 -1
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- 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 +488 -16
- 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/decorators/unified-field.decorator.ts +18 -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 +657 -18
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.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
|
-
|
|
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
|
|