@lenne.tech/nest-server 11.4.5 → 11.4.7

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.4.5",
3
+ "version": "11.4.7",
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",
@@ -1,4 +1,5 @@
1
1
  import { Field, FieldOptions } from '@nestjs/graphql';
2
+ import { TypeMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storages/type-metadata.storage';
2
3
  import { Prop, PropOptions } from '@nestjs/mongoose';
3
4
  import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
4
5
  import { EnumAllowedTypes } from '@nestjs/swagger/dist/interfaces/schema-object-metadata.interface';
@@ -18,7 +19,7 @@ import {
18
19
  ValidateNested,
19
20
  ValidationOptions,
20
21
  } from 'class-validator';
21
- import { GraphQLScalarType } from 'graphql';
22
+ import { GraphQLScalarType, isEnumType } from 'graphql';
22
23
 
23
24
  import { RoleEnum } from '../enums/role.enum';
24
25
  import { Restricted, RestrictedType } from './restricted.decorator';
@@ -27,6 +28,12 @@ import { Restricted, RestrictedType } from './restricted.decorator';
27
28
  // Key: `${className}.${propertyName}`, Value: nested type constructor
28
29
  export const nestedTypeRegistry = new Map<string, any>();
29
30
 
31
+ /**
32
+ * Registry to map enum objects to their names.
33
+ * This is populated when registerEnumType is called or can be manually populated.
34
+ */
35
+ export const enumNameRegistry = new Map<any, string>();
36
+
30
37
  export interface UnifiedFieldOptions {
31
38
  /** Description used for both Swagger & Gql */
32
39
  description?: string;
@@ -166,7 +173,18 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
166
173
  if (opts.enum && opts.enum.enum) {
167
174
  swaggerOpts.enum = opts.enum.enum;
168
175
 
169
- if (opts.enum.enumName) {
176
+ // Set enumName with auto-detection:
177
+ // - If enumName property doesn't exist at all, auto-detect the name
178
+ // - If enumName is explicitly set (even to null/undefined), use that value
179
+ // This allows explicit opts.enum.enumName = undefined to disable auto-detection
180
+ if (!('enumName' in opts.enum)) {
181
+ // Property doesn't exist, try auto-detection
182
+ const autoDetectedName = getEnumName(opts.enum.enum);
183
+ if (autoDetectedName) {
184
+ swaggerOpts.enumName = autoDetectedName;
185
+ }
186
+ } else {
187
+ // Property exists (even if undefined/null), use its value
170
188
  swaggerOpts.enumName = opts.enum.enumName;
171
189
  }
172
190
 
@@ -276,11 +294,60 @@ function getBuiltInValidator(
276
294
  return null;
277
295
  }
278
296
  if (each) {
279
- return (t, k) => decorator(target, k);
297
+ return (_t, k) => decorator(target, k);
280
298
  }
281
299
  return decorator;
282
300
  }
283
301
 
302
+ /**
303
+ * Helper function to extract enum name from an enum object
304
+ * Attempts multiple strategies to find a meaningful name
305
+ */
306
+ function getEnumName(enumObj: any): string | undefined {
307
+ // Check if the enum was registered in our custom registry
308
+ if (enumNameRegistry.has(enumObj)) {
309
+ return enumNameRegistry.get(enumObj);
310
+ }
311
+
312
+ // Check if it's registered in GraphQL TypeMetadataStorage (most common case with registerEnumType)
313
+ try {
314
+ const enumsMetadata = TypeMetadataStorage.getEnumsMetadata();
315
+ const matchingEnum = enumsMetadata.find((metadata) => metadata.ref === enumObj);
316
+ if (matchingEnum && matchingEnum.name) {
317
+ return matchingEnum.name;
318
+ }
319
+ } catch (error) {
320
+ // TypeMetadataStorage might not be initialized yet during bootstrap
321
+ // This is not an error, we just continue with other strategies
322
+ }
323
+
324
+ // Check if it's a GraphQL enum type
325
+ if (isEnumType(enumObj)) {
326
+ return enumObj.name;
327
+ }
328
+
329
+ // Check if the enum object has a name property (some custom implementations)
330
+ if (enumObj && typeof enumObj === 'object' && 'name' in enumObj && typeof enumObj.name === 'string') {
331
+ return enumObj.name;
332
+ }
333
+
334
+ // Check if it's a constructor function with a name
335
+ if (typeof enumObj === 'function' && enumObj.name && enumObj.name !== 'Object') {
336
+ return enumObj.name;
337
+ }
338
+
339
+ // For regular TypeScript enums, try to find the global variable name
340
+ // This is a heuristic approach - not guaranteed to work in all cases
341
+ if (enumObj && typeof enumObj === 'object') {
342
+ // Check constructor name (though this usually returns 'Object' for enums)
343
+ if (enumObj.constructor && enumObj.constructor.name && enumObj.constructor.name !== 'Object') {
344
+ return enumObj.constructor.name;
345
+ }
346
+ }
347
+
348
+ return undefined;
349
+ }
350
+
284
351
  function isGraphQLScalar(type: any): boolean {
285
352
  // CustomScalar check (The CustomScalar interface implements these functions below)
286
353
  return (
@@ -0,0 +1,93 @@
1
+ import { registerEnumType } from '@nestjs/graphql';
2
+
3
+ import { enumNameRegistry } from '../decorators/unified-field.decorator';
4
+
5
+ /**
6
+ * Interface defining options for the registerEnum helper
7
+ */
8
+ export interface RegisterEnumOptions<T extends object = any> {
9
+ /**
10
+ * Description of the enum
11
+ */
12
+ description?: string;
13
+
14
+ /**
15
+ * Whether to register the enum for GraphQL using registerEnumType
16
+ * @default true
17
+ */
18
+ graphql?: boolean;
19
+
20
+ /**
21
+ * Name of the enum (required)
22
+ */
23
+ name: string;
24
+
25
+ /**
26
+ * Whether to register the enum in the enumNameRegistry for Swagger/REST
27
+ * @default true
28
+ */
29
+ swagger?: boolean;
30
+
31
+ /**
32
+ * A map of options for the values of the enum (only used for GraphQL)
33
+ */
34
+ valuesMap?: Partial<Record<keyof T, { deprecationReason?: string; description?: string }>>;
35
+ }
36
+
37
+ /**
38
+ * Registers an enum for both GraphQL and Swagger/REST APIs.
39
+ *
40
+ * This is a convenience helper that combines:
41
+ * - `registerEnumType` from @nestjs/graphql (for GraphQL schema)
42
+ * - Manual registration in `enumNameRegistry` (for Swagger/OpenAPI)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * export enum StatusEnum {
47
+ * ACTIVE = 'active',
48
+ * INACTIVE = 'inactive'
49
+ * }
50
+ *
51
+ * // Register for both GraphQL and REST
52
+ * registerEnum(StatusEnum, {
53
+ * name: 'StatusEnum',
54
+ * description: 'User status'
55
+ * });
56
+ *
57
+ * // Register only for REST (no GraphQL)
58
+ * registerEnum(StatusEnum, {
59
+ * name: 'StatusEnum',
60
+ * graphql: false
61
+ * });
62
+ *
63
+ * // Register only for GraphQL (no REST)
64
+ * registerEnum(StatusEnum, {
65
+ * name: 'StatusEnum',
66
+ * swagger: false
67
+ * });
68
+ * ```
69
+ *
70
+ * @param enumRef - The enum reference to register
71
+ * @param options - Registration options
72
+ */
73
+ export function registerEnum<T extends object = any>(enumRef: T, options: RegisterEnumOptions<T>): void {
74
+ const { description, graphql = true, name, swagger = true, valuesMap } = options;
75
+
76
+ if (!name) {
77
+ throw new Error('Enum name is required for registerEnum');
78
+ }
79
+
80
+ // Register for Swagger/REST if enabled
81
+ if (swagger) {
82
+ enumNameRegistry.set(enumRef, name);
83
+ }
84
+
85
+ // Register for GraphQL if enabled
86
+ if (graphql) {
87
+ registerEnumType(enumRef, {
88
+ description,
89
+ name,
90
+ valuesMap,
91
+ });
92
+ }
93
+ }
@@ -91,7 +91,7 @@ async function listMigrations(options: CliOptions) {
91
91
  const stateStore = loadStateStore(options.store);
92
92
 
93
93
  const runner = new MigrationRunner({
94
- migrationsDirectory: options.migrationsDir,
94
+ migrationsDirectory: path.resolve(process.cwd(), options.migrationsDir),
95
95
  stateStore,
96
96
  });
97
97
 
@@ -255,7 +255,7 @@ async function runDown(options: CliOptions) {
255
255
  const stateStore = loadStateStore(options.store);
256
256
 
257
257
  const runner = new MigrationRunner({
258
- migrationsDirectory: options.migrationsDir,
258
+ migrationsDirectory: path.resolve(process.cwd(), options.migrationsDir),
259
259
  stateStore,
260
260
  });
261
261
 
@@ -270,7 +270,7 @@ async function runUp(options: CliOptions) {
270
270
  const stateStore = loadStateStore(options.store);
271
271
 
272
272
  const runner = new MigrationRunner({
273
- migrationsDirectory: options.migrationsDir,
273
+ migrationsDirectory: path.resolve(process.cwd(), options.migrationsDir),
274
274
  stateStore,
275
275
  });
276
276
 
@@ -6,20 +6,64 @@ import * as path from 'path';
6
6
  * Migration helper functions for database operations
7
7
  */
8
8
 
9
+ // Store active connections for auto-cleanup
10
+ const activeConnections = new Set<MongoClient>();
11
+
12
+ // Track if we're in a migration context
13
+ let inMigrationContext = false;
14
+
15
+ /**
16
+ * Mark the start of a migration
17
+ * @internal Used by migration runner
18
+ */
19
+ export const _startMigration = () => {
20
+ inMigrationContext = true;
21
+ };
22
+
23
+ /**
24
+ * Mark the end of a migration and close all connections
25
+ * @internal Used by migration runner
26
+ */
27
+ export const _endMigration = async () => {
28
+ inMigrationContext = false;
29
+ // Close all active connections
30
+ const promises = Array.from(activeConnections).map((client) => client.close());
31
+ activeConnections.clear();
32
+ await Promise.all(promises);
33
+ };
34
+
9
35
  /**
10
36
  * Get database connection
11
37
  *
38
+ * When used in migrations, connections are automatically closed after the migration completes.
39
+ * For manual usage outside migrations, you must close the connection manually.
40
+ *
12
41
  * @param mongoUrl - MongoDB connection URI
13
42
  * @returns Promise with database instance
14
43
  *
15
44
  * @example
16
45
  * ```typescript
46
+ * // In migrations - connection auto-closes after migration
17
47
  * const db = await getDb('mongodb://localhost/mydb');
18
48
  * await db.collection('users').updateMany(...);
49
+ *
50
+ * // Outside migrations - must close manually
51
+ * const db = await getDb('mongodb://localhost/mydb');
52
+ * try {
53
+ * await db.collection('users').updateMany(...);
54
+ * } finally {
55
+ * await db.client.close();
56
+ * }
19
57
  * ```
20
58
  */
21
59
  export const getDb = async (mongoUrl: string): Promise<Db> => {
22
60
  const client: MongoClient = await MongoClient.connect(mongoUrl);
61
+
62
+ // Track connection for auto-cleanup in migrations
63
+ if (inMigrationContext) {
64
+ activeConnections.add(client);
65
+ }
66
+
23
67
  return client.db();
24
68
  };
25
69
 
@@ -106,6 +106,8 @@ export class MigrationRunner {
106
106
  * Run all pending migrations (up)
107
107
  */
108
108
  async up(): Promise<void> {
109
+ const { _endMigration, _startMigration } = await import('./helpers/migration.helper');
110
+
109
111
  const allMigrations = await this.loadMigrationFiles();
110
112
  const state = await this.options.stateStore.loadAsync();
111
113
  const completedMigrations = (state.migrations || []).map((m) => m.title);
@@ -121,23 +123,32 @@ export class MigrationRunner {
121
123
 
122
124
  for (const migration of pendingMigrations) {
123
125
  console.log(`Running migration: ${migration.title}`);
124
- await migration.up();
125
-
126
- // Update state
127
- const newState = await this.options.stateStore.loadAsync();
128
- const migrations = newState.migrations || [];
129
- migrations.push({
130
- timestamp: migration.timestamp,
131
- title: migration.title,
132
- });
133
-
134
- await this.options.stateStore.saveAsync({
135
- lastRun: migration.title,
136
- migrations,
137
- up: () => {},
138
- } as any);
139
126
 
140
- console.log(`✓ Migration completed: ${migration.title}`);
127
+ // Mark start of migration for auto-cleanup
128
+ _startMigration();
129
+
130
+ try {
131
+ await migration.up();
132
+
133
+ // Update state
134
+ const newState = await this.options.stateStore.loadAsync();
135
+ const migrations = newState.migrations || [];
136
+ migrations.push({
137
+ timestamp: migration.timestamp,
138
+ title: migration.title,
139
+ });
140
+
141
+ await this.options.stateStore.saveAsync({
142
+ lastRun: migration.title,
143
+ migrations,
144
+ up: () => {},
145
+ } as any);
146
+
147
+ console.log(`✓ Migration completed: ${migration.title}`);
148
+ } finally {
149
+ // Always close connections, even on error
150
+ await _endMigration();
151
+ }
141
152
  }
142
153
 
143
154
  console.log('All migrations completed successfully');
@@ -147,6 +158,8 @@ export class MigrationRunner {
147
158
  * Rollback the last migration (down)
148
159
  */
149
160
  async down(): Promise<void> {
161
+ const { _endMigration, _startMigration } = await import('./helpers/migration.helper');
162
+
150
163
  const state = await this.options.stateStore.loadAsync();
151
164
  const completedMigrations = state.migrations || [];
152
165
 
@@ -168,17 +181,26 @@ export class MigrationRunner {
168
181
  }
169
182
 
170
183
  console.log(`Rolling back migration: ${migrationToRollback.title}`);
171
- await migrationToRollback.down();
172
184
 
173
- // Update state
174
- const newMigrations = completedMigrations.slice(0, -1);
175
- await this.options.stateStore.saveAsync({
176
- lastRun: newMigrations.length > 0 ? newMigrations[newMigrations.length - 1].title : undefined,
177
- migrations: newMigrations,
178
- up: () => {},
179
- } as any);
185
+ // Mark start of migration for auto-cleanup
186
+ _startMigration();
187
+
188
+ try {
189
+ await migrationToRollback.down();
180
190
 
181
- console.log(`✓ Migration rolled back: ${migrationToRollback.title}`);
191
+ // Update state
192
+ const newMigrations = completedMigrations.slice(0, -1);
193
+ await this.options.stateStore.saveAsync({
194
+ lastRun: newMigrations.length > 0 ? newMigrations[newMigrations.length - 1].title : undefined,
195
+ migrations: newMigrations,
196
+ up: () => {},
197
+ } as any);
198
+
199
+ console.log(`✓ Migration rolled back: ${migrationToRollback.title}`);
200
+ } finally {
201
+ // Always close connections, even on error
202
+ await _endMigration();
203
+ }
182
204
  }
183
205
 
184
206
  /**
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export * from './core/common/helpers/graphql.helper';
37
37
  export * from './core/common/helpers/gridfs.helper';
38
38
  export * from './core/common/helpers/input.helper';
39
39
  export * from './core/common/helpers/model.helper';
40
+ export * from './core/common/helpers/register-enum.helper';
40
41
  export * from './core/common/helpers/scim.helper';
41
42
  export * from './core/common/helpers/service.helper';
42
43
  export * from './core/common/helpers/table.helper';