@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/dist/core/common/decorators/unified-field.decorator.d.ts +1 -0
- package/dist/core/common/decorators/unified-field.decorator.js +40 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/helpers/register-enum.helper.d.ts +11 -0
- package/dist/core/common/helpers/register-enum.helper.js +22 -0
- package/dist/core/common/helpers/register-enum.helper.js.map +1 -0
- package/dist/core/modules/migrate/cli/migrate-cli.js +3 -3
- package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -0
- package/dist/core/modules/migrate/helpers/migration.helper.js +17 -1
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.js +35 -21
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.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 +70 -3
- package/src/core/common/helpers/register-enum.helper.ts +93 -0
- package/src/core/modules/migrate/cli/migrate-cli.ts +3 -3
- package/src/core/modules/migrate/helpers/migration.helper.ts +44 -0
- package/src/core/modules/migrate/migration-runner.ts +47 -25
- package/src/index.ts +1 -0
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.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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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';
|