@lenne.tech/nest-server 11.1.0 → 11.1.2

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.2",
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) {
@@ -319,6 +319,11 @@ export async function check(
319
319
  * Check if input is a valid Date format and return a new Date
320
320
  */
321
321
  export function checkAndGetDate(input: any): Date {
322
+ // Get timestamp from string
323
+ if (typeof input === 'string' && /^\d+$/.test(input)) {
324
+ input = parseInt(input, 10);
325
+ }
326
+
322
327
  // Create date from value
323
328
  const date = new Date(input);
324
329
 
@@ -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
+ }
@@ -36,6 +36,38 @@ export abstract class CorePersistenceModel extends CoreModel {
36
36
  return new Types.ObjectId(this.id);
37
37
  }
38
38
 
39
+ /**
40
+ * Getter for created date as Unix timestamp
41
+ */
42
+ @UnifiedField({
43
+ description: 'Created date (Unix timestamp)',
44
+ isOptional: true,
45
+ roles: RoleEnum.S_EVERYONE,
46
+ swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Number },
47
+ })
48
+ get createdTs(): number {
49
+ if (this.createdAt instanceof Date) {
50
+ return this.createdAt.getTime();
51
+ }
52
+ return this.createdAt;
53
+ }
54
+
55
+ /**
56
+ * Getter for updated date as Unix timestamp
57
+ */
58
+ @UnifiedField({
59
+ description: 'Updated date (Unix timestamp)',
60
+ isOptional: true,
61
+ roles: RoleEnum.S_EVERYONE,
62
+ swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
63
+ })
64
+ get updatedTs(): number {
65
+ if (this.updatedAt instanceof Date) {
66
+ return this.updatedAt.getTime();
67
+ }
68
+ return this.updatedAt;
69
+ }
70
+
39
71
  // ===========================================================================
40
72
  // Properties
41
73
  // ===========================================================================
@@ -53,7 +85,7 @@ export abstract class CorePersistenceModel extends CoreModel {
53
85
  /**
54
86
  * Created date, is set automatically by mongoose
55
87
  */
56
- @Prop({ onCreate: () => new Date() })
88
+ @Prop()
57
89
  @UnifiedField({
58
90
  description: 'Created date',
59
91
  isOptional: true,
@@ -66,7 +98,7 @@ export abstract class CorePersistenceModel extends CoreModel {
66
98
  /**
67
99
  * Updated date is set automatically by mongoose
68
100
  */
69
- @Prop({ onUpdate: () => new Date() })
101
+ @Prop()
70
102
  @UnifiedField({
71
103
  description: 'Updated date',
72
104
  isOptional: true,