@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.1.0",
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,test}/**/*.{ts,js}' --cache",
26
- "lint:fix": "eslint '{src,apps,libs,test}/**/*.{ts,js}' --fix --cache",
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, ApiPropertyOptional, ApiPropertyOptions } from '@nestjs/swagger';
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
- const ApiDec = opts.isOptional ? ApiPropertyOptional : ApiProperty;
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
- ApiDec({
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
- const translation = output._translations?.[options.language];
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
 
@@ -116,6 +116,8 @@ export class User extends CoreUserModel implements PersistenceModel {
116
116
  this.createdBy = null;
117
117
  this.updatedAt = null;
118
118
  this.updatedBy = null;
119
+ this.createdTs = null;
120
+ this.updatedAt = null;
119
121
  }
120
122
 
121
123
  // Return prepared user