@lenne.tech/nest-server 11.4.6 → 11.4.8

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.6",
3
+ "version": "11.4.8",
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",
@@ -61,7 +61,7 @@ export const getRestricted = (object: unknown, propertyKey?: string): Restricted
61
61
  */
62
62
  export const checkRestricted = (
63
63
  data: any,
64
- user: { hasRole: (roles: string[]) => boolean; id: any },
64
+ user: { hasRole: (roles: string[]) => boolean; id: any; verified?: any; verifiedAt?: any },
65
65
  options: {
66
66
  allowCreatorOfParent?: boolean;
67
67
  checkObjectItself?: boolean;
@@ -108,9 +108,9 @@ export const checkRestricted = (
108
108
  // Array
109
109
  if (Array.isArray(data)) {
110
110
  // Check array items
111
- let result = data.map(item => checkRestricted(item, user, config, processedObjects));
111
+ let result = data.map((item) => checkRestricted(item, user, config, processedObjects));
112
112
  if (!config.throwError && config.removeUndefinedFromResultArray) {
113
- result = result.filter(item => item !== undefined);
113
+ result = result.filter((item) => item !== undefined);
114
114
  }
115
115
  return result;
116
116
  }
@@ -134,8 +134,8 @@ export const checkRestricted = (
134
134
  if (typeof item === 'string') {
135
135
  roles.push(item);
136
136
  } else if (
137
- item?.roles?.length
138
- && (config.processType && item.processType ? config.processType === item.processType : true)
137
+ item?.roles?.length &&
138
+ (config.processType && item.processType ? config.processType === item.processType : true)
139
139
  ) {
140
140
  if (Array.isArray(item.roles)) {
141
141
  roles.push(...item.roles);
@@ -156,13 +156,14 @@ export const checkRestricted = (
156
156
 
157
157
  // Check access rights
158
158
  if (
159
- roles.includes(RoleEnum.S_EVERYONE)
160
- || user?.hasRole?.(roles)
161
- || (user?.id && roles.includes(RoleEnum.S_USER))
162
- || (roles.includes(RoleEnum.S_SELF) && equalIds(data, user))
163
- || (roles.includes(RoleEnum.S_CREATOR)
164
- && (('createdBy' in data && equalIds(data.createdBy, user))
165
- || (config.allowCreatorOfParent && !('createdBy' in data) && config.isCreatorOfParent)))
159
+ roles.includes(RoleEnum.S_EVERYONE) ||
160
+ user?.hasRole?.(roles) ||
161
+ (user?.id && roles.includes(RoleEnum.S_USER)) ||
162
+ (roles.includes(RoleEnum.S_SELF) && equalIds(data, user)) ||
163
+ (roles.includes(RoleEnum.S_CREATOR) &&
164
+ (('createdBy' in data && equalIds(data.createdBy, user)) ||
165
+ (config.allowCreatorOfParent && !('createdBy' in data) && config.isCreatorOfParent))) ||
166
+ (roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt))
166
167
  ) {
167
168
  valid = true;
168
169
  }
@@ -172,11 +173,11 @@ export const checkRestricted = (
172
173
  // Get groups
173
174
  const groups = restricted.filter((item) => {
174
175
  return (
175
- typeof item === 'object'
176
+ typeof item === 'object' &&
176
177
  // Check if object is valid
177
- && item.memberOf?.length
178
+ item.memberOf?.length &&
178
179
  // Check if processType is specified and is valid for current process
179
- && (config.processType && item.processType ? config.processType === item.processType : true)
180
+ (config.processType && item.processType ? config.processType === item.processType : true)
180
181
  );
181
182
  }) as { memberOf: string | string[] }[];
182
183
 
@@ -252,8 +253,8 @@ export const checkRestricted = (
252
253
  // Check rights
253
254
  if (valid) {
254
255
  // Check if data is user or user is creator of data (for nested plain objects)
255
- config.isCreatorOfParent
256
- = equalIds(data, user) || ('createdBy' in data ? equalIds(data.createdBy, user) : config.isCreatorOfParent);
256
+ config.isCreatorOfParent =
257
+ equalIds(data, user) || ('createdBy' in data ? equalIds(data.createdBy, user) : config.isCreatorOfParent);
257
258
 
258
259
  // Check deep
259
260
  data[propertyKey] = checkRestricted(data[propertyKey], user, config, processedObjects);
@@ -1,3 +1,4 @@
1
+ /* eslint-disable perfectionist/sort-enums */
1
2
  /**
2
3
  * Enums for Resolver @Role and Model @Restricted decorator and for roles property in ServiceOptions
3
4
  *
@@ -42,9 +43,12 @@ export enum RoleEnum {
42
43
  // Everyone, including users who are not logged in, can access (see context user, e.g. @CurrentUser)
43
44
  S_EVERYONE = 's_everyone',
44
45
 
45
- // No one has access, not even administrators
46
+ // No one has access, not even administrators (regardless of which roles are still set, access will always be denied)
46
47
  S_NO_ONE = 's_no_one',
47
48
 
49
+ // User must be verified (see verified or verifiedAt property of user)
50
+ S_VERIFIED = 's_verified',
51
+
48
52
  // ===================================================================================================================
49
53
  // Special system roles that check rights for DB objects and can be used via @Restricted for Models
50
54
  // (classes and properties) and via ServiceOptions for Resolver methods. These roles should not be integrated in
@@ -209,7 +209,7 @@ export function assignPlain(target: Record<any, any>, ...args: Record<any, any>[
209
209
  */
210
210
  export async function check(
211
211
  value: any,
212
- user: { hasRole: (roles: string[]) => boolean; id: any },
212
+ user: { hasRole: (roles: string[]) => boolean; id: any; verified?: any; verifiedAt?: any },
213
213
  options?: {
214
214
  allowCreatorOfParent?: boolean;
215
215
  dbObject?: any;
@@ -262,7 +262,9 @@ export async function check(
262
262
  (config.allowCreatorOfParent &&
263
263
  config.dbObject &&
264
264
  !('createdBy' in config.dbObject) &&
265
- config.isCreatorOfParent)))
265
+ config.isCreatorOfParent))) ||
266
+ // check if the is verified
267
+ (roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt))
266
268
  ) {
267
269
  valid = true;
268
270
  }
@@ -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
  /**