@lenne.tech/nest-server 11.1.0 → 11.1.1
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.js +26 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +31 -10
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/models/core-persistence.model.d.ts +2 -0
- package/dist/core/common/models/core-persistence.model.js +22 -0
- package/dist/core/common/models/core-persistence.model.js.map +1 -1
- package/dist/server/modules/user/user.model.js +2 -0
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/core/common/decorators/unified-field.decorator.ts +30 -4
- package/src/core/common/helpers/service.helper.ts +37 -11
- package/src/core/common/models/core-persistence.model.ts +19 -0
- package/src/server/modules/user/user.model.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.1",
|
|
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",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"docs:ci": "ts-node ./scripts/init-server.ts && npm run docs:bootstrap && compodoc -p tsconfig.json",
|
|
23
23
|
"format": "prettier --write 'src/**/*.ts'",
|
|
24
24
|
"format:staged": "pretty-quick --staged",
|
|
25
|
-
"lint": "eslint '{src,apps,libs,
|
|
26
|
-
"lint:fix": "eslint '{src,apps,libs,
|
|
25
|
+
"lint": "eslint '{src,apps,libs,tests}/**/*.{ts,js}' --cache",
|
|
26
|
+
"lint:fix": "eslint '{src,apps,libs,tests}/**/*.{ts,js}' --fix --cache",
|
|
27
27
|
"prestart:prod": "npm run build",
|
|
28
28
|
"reinit": "rimraf package-lock.json && rimraf node_modules && npm i && npm run lint && npm run test:e2e && npm run test:ci && npm run build",
|
|
29
29
|
"reinit:clean": "rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i && npm run test:e2e && npm run build",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Field, FieldOptions } from '@nestjs/graphql';
|
|
2
|
-
import { ApiProperty,
|
|
2
|
+
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
|
|
3
3
|
import { EnumAllowedTypes } from '@nestjs/swagger/dist/interfaces/schema-object-metadata.interface';
|
|
4
4
|
import { Type } from 'class-transformer';
|
|
5
5
|
import {
|
|
@@ -78,6 +78,10 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
78
78
|
throw new Error(`Array field '${String(propertyKey)}' of '${String(target)}' must have an explicit type`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (opts.enum && userType) {
|
|
82
|
+
throw new Error(`Can't set both enum and type of ${String(propertyKey)} in ${target.constructor.name}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
81
85
|
const resolvedTypeFn = (): any => {
|
|
82
86
|
if (opts.enum?.enum) {
|
|
83
87
|
return opts.enum.enum; // Ensure enums are handled directly
|
|
@@ -150,9 +154,31 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
150
154
|
// Gql decorator
|
|
151
155
|
Field(gqlTypeFn, gqlOpts)(target, propertyKey);
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
// Trims keys with 'undefined' properties.
|
|
158
|
+
function trimUndefined<T>(obj: T): Partial<T> {
|
|
159
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
160
|
+
return obj;
|
|
161
|
+
}
|
|
162
|
+
const result: any = Array.isArray(obj) ? [] : {};
|
|
163
|
+
for (const key in obj) {
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
165
|
+
const value = (obj as any)[key];
|
|
166
|
+
|
|
167
|
+
if (typeof value === 'object' && value !== null) {
|
|
168
|
+
const cleaned = trimUndefined(value);
|
|
169
|
+
if (Array.isArray(cleaned) ? cleaned.length > 0 : Object.keys(cleaned).length > 0) {
|
|
170
|
+
result[key] = cleaned;
|
|
171
|
+
}
|
|
172
|
+
} else if (value !== undefined) {
|
|
173
|
+
result[key] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
154
180
|
|
|
155
|
-
|
|
181
|
+
ApiProperty(trimUndefined({
|
|
156
182
|
deprecated: swaggerOpts.deprecated,
|
|
157
183
|
description: swaggerOpts.description,
|
|
158
184
|
enum: swaggerOpts.enum,
|
|
@@ -162,7 +188,7 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
162
188
|
nullable: swaggerOpts.nullable,
|
|
163
189
|
pattern: swaggerOpts.pattern,
|
|
164
190
|
type: () => resolvedTypeFn(),
|
|
165
|
-
})(target, propertyKey);
|
|
191
|
+
}))(target, propertyKey);
|
|
166
192
|
|
|
167
193
|
// Conditional validation
|
|
168
194
|
if (opts.validateIf) {
|
|
@@ -5,7 +5,6 @@ import { sha256 } from 'js-sha256';
|
|
|
5
5
|
import _ = require('lodash');
|
|
6
6
|
import { Types } from 'mongoose';
|
|
7
7
|
|
|
8
|
-
import { getTranslatablePropertyKeys } from '../decorators/translatable.decorator';
|
|
9
8
|
import { RoleEnum } from '../enums/role.enum';
|
|
10
9
|
import { PrepareInputOptions } from '../interfaces/prepare-input-options.interface';
|
|
11
10
|
import { PrepareOutputOptions } from '../interfaces/prepare-output-options.interface';
|
|
@@ -276,16 +275,7 @@ export async function prepareOutput<T = { [key: string]: any; map: (...args: any
|
|
|
276
275
|
|
|
277
276
|
// Add translated values of current selected language if _translations object exists
|
|
278
277
|
if (config.targetModel && config.language && typeof output === 'object' && '_translations' in output) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (typeof translation === 'object') {
|
|
282
|
-
const keys = getTranslatablePropertyKeys(config.targetModel);
|
|
283
|
-
for (const key of keys) {
|
|
284
|
-
if (translation[key] != null) {
|
|
285
|
-
output[key] = translation[key];
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
278
|
+
applyTranslationsRecursively(output, config.language);
|
|
289
279
|
}
|
|
290
280
|
|
|
291
281
|
// Return prepared output
|
|
@@ -346,3 +336,39 @@ export function prepareServiceOptions(
|
|
|
346
336
|
// Return (cloned and) prepared service options
|
|
347
337
|
return serviceOptions;
|
|
348
338
|
}
|
|
339
|
+
|
|
340
|
+
function applyTranslationsRecursively(obj: any, language: string, visited: WeakSet<object> = new WeakSet()) {
|
|
341
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (visited.has(obj)) {
|
|
345
|
+
return;
|
|
346
|
+
} // Cycle detected -> cancel
|
|
347
|
+
|
|
348
|
+
visited.add(obj);
|
|
349
|
+
|
|
350
|
+
// If _translations exists
|
|
351
|
+
if ('_translations' in obj && typeof obj._translations === 'object') {
|
|
352
|
+
const translation = obj._translations?.[language];
|
|
353
|
+
if (typeof translation === 'object') {
|
|
354
|
+
for (const key in translation) {
|
|
355
|
+
if (translation[key] != null) {
|
|
356
|
+
obj[key] = translation[key];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Recursive over all properties
|
|
363
|
+
for (const key of Object.keys(obj)) {
|
|
364
|
+
const value = obj[key];
|
|
365
|
+
|
|
366
|
+
if (Array.isArray(value)) {
|
|
367
|
+
for (const item of value) {
|
|
368
|
+
applyTranslationsRecursively(item, language, visited);
|
|
369
|
+
}
|
|
370
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
371
|
+
applyTranslationsRecursively(value, language, visited);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -63,6 +63,14 @@ export abstract class CorePersistenceModel extends CoreModel {
|
|
|
63
63
|
})
|
|
64
64
|
createdAt: Date = undefined;
|
|
65
65
|
|
|
66
|
+
@Prop({ onCreate: () => Date.now() })
|
|
67
|
+
@UnifiedField({
|
|
68
|
+
description: 'Created date (Unix timestamp)',
|
|
69
|
+
roles: RoleEnum.S_EVERYONE,
|
|
70
|
+
swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
|
|
71
|
+
})
|
|
72
|
+
createdTs: number = undefined;
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Updated date is set automatically by mongoose
|
|
68
76
|
*/
|
|
@@ -76,6 +84,14 @@ export abstract class CorePersistenceModel extends CoreModel {
|
|
|
76
84
|
})
|
|
77
85
|
updatedAt: Date = undefined;
|
|
78
86
|
|
|
87
|
+
@Prop({ onUpdate: () => Date.now() })
|
|
88
|
+
@UnifiedField({
|
|
89
|
+
description: 'Updated date (Unix timestamp)',
|
|
90
|
+
roles: RoleEnum.S_EVERYONE,
|
|
91
|
+
swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
|
|
92
|
+
})
|
|
93
|
+
updatedTs: number = undefined;
|
|
94
|
+
|
|
79
95
|
// ===========================================================================
|
|
80
96
|
// Methods
|
|
81
97
|
// ===========================================================================
|
|
@@ -87,6 +103,9 @@ export abstract class CorePersistenceModel extends CoreModel {
|
|
|
87
103
|
super.init();
|
|
88
104
|
this.createdAt = this.createdAt === undefined ? new Date() : this.createdAt;
|
|
89
105
|
this.updatedAt = this.updatedAt === undefined ? this.createdAt : this.updatedAt;
|
|
106
|
+
|
|
107
|
+
this.createdTs = this.createdTs === undefined ? Date.now() : this.createdTs;
|
|
108
|
+
this.updatedTs = this.updatedTs === undefined ? this.createdTs : this.updatedTs;
|
|
90
109
|
return this;
|
|
91
110
|
}
|
|
92
111
|
|