@lenne.tech/nest-server 11.17.0 → 11.19.0

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.
Files changed (105) hide show
  1. package/dist/config.env.js +2 -2
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/response-model.decorator.d.ts +3 -0
  4. package/dist/core/common/decorators/response-model.decorator.js +8 -0
  5. package/dist/core/common/decorators/response-model.decorator.js.map +1 -0
  6. package/dist/core/common/helpers/db.helper.js +2 -2
  7. package/dist/core/common/helpers/db.helper.js.map +1 -1
  8. package/dist/core/common/helpers/filter.helper.js +3 -3
  9. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  10. package/dist/core/common/helpers/input.helper.js +2 -2
  11. package/dist/core/common/helpers/input.helper.js.map +1 -1
  12. package/dist/core/common/helpers/interceptor.helper.d.ts +3 -0
  13. package/dist/core/common/helpers/interceptor.helper.js +84 -0
  14. package/dist/core/common/helpers/interceptor.helper.js.map +1 -0
  15. package/dist/core/common/helpers/service.helper.d.ts +1 -0
  16. package/dist/core/common/helpers/service.helper.js +1 -0
  17. package/dist/core/common/helpers/service.helper.js.map +1 -1
  18. package/dist/core/common/interceptors/check-security.interceptor.d.ts +2 -0
  19. package/dist/core/common/interceptors/check-security.interceptor.js +43 -1
  20. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  21. package/dist/core/common/interceptors/response-model.interceptor.d.ts +13 -0
  22. package/dist/core/common/interceptors/response-model.interceptor.js +107 -0
  23. package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -0
  24. package/dist/core/common/interceptors/translate-response.interceptor.d.ts +8 -0
  25. package/dist/core/common/interceptors/translate-response.interceptor.js +85 -0
  26. package/dist/core/common/interceptors/translate-response.interceptor.js.map +1 -0
  27. package/dist/core/common/interfaces/server-options.interface.d.ts +14 -0
  28. package/dist/core/common/middleware/request-context.middleware.d.ts +5 -0
  29. package/dist/core/common/middleware/request-context.middleware.js +29 -0
  30. package/dist/core/common/middleware/request-context.middleware.js.map +1 -0
  31. package/dist/core/common/pipes/map-and-validate.pipe.js +2 -2
  32. package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
  33. package/dist/core/common/plugins/complexity.plugin.d.ts +2 -2
  34. package/dist/core/common/plugins/mongoose-audit-fields.plugin.d.ts +1 -0
  35. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js +51 -0
  36. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js.map +1 -0
  37. package/dist/core/common/plugins/mongoose-password.plugin.d.ts +4 -0
  38. package/dist/core/common/plugins/mongoose-password.plugin.js +69 -0
  39. package/dist/core/common/plugins/mongoose-password.plugin.js.map +1 -0
  40. package/dist/core/common/plugins/mongoose-role-guard.plugin.d.ts +1 -0
  41. package/dist/core/common/plugins/mongoose-role-guard.plugin.js +80 -0
  42. package/dist/core/common/plugins/mongoose-role-guard.plugin.js.map +1 -0
  43. package/dist/core/common/services/config.service.js +2 -2
  44. package/dist/core/common/services/config.service.js.map +1 -1
  45. package/dist/core/common/services/model-registry.service.d.ts +8 -0
  46. package/dist/core/common/services/model-registry.service.js +20 -0
  47. package/dist/core/common/services/model-registry.service.js.map +1 -0
  48. package/dist/core/common/services/module.service.d.ts +2 -0
  49. package/dist/core/common/services/module.service.js +36 -1
  50. package/dist/core/common/services/module.service.js.map +1 -1
  51. package/dist/core/common/services/request-context.service.d.ts +18 -0
  52. package/dist/core/common/services/request-context.service.js +32 -0
  53. package/dist/core/common/services/request-context.service.js.map +1 -0
  54. package/dist/core/modules/auth/guards/auth.guard.js +2 -2
  55. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  56. package/dist/core/modules/better-auth/core-better-auth.resolver.js +4 -2
  57. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  58. package/dist/core/modules/error-code/core-error-code.controller.js +1 -1
  59. package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -1
  60. package/dist/core/modules/system-setup/core-system-setup.controller.js +1 -1
  61. package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -1
  62. package/dist/core.module.js +36 -0
  63. package/dist/core.module.js.map +1 -1
  64. package/dist/index.d.ts +10 -0
  65. package/dist/index.js +10 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/server/modules/error-code/error-code.controller.js +1 -1
  68. package/dist/server/modules/error-code/error-code.controller.js.map +1 -1
  69. package/dist/server/modules/file/file-info.model.d.ts +12 -12
  70. package/dist/server/modules/user/user.model.d.ts +33 -33
  71. package/dist/tsconfig.build.tsbuildinfo +1 -1
  72. package/package.json +35 -30
  73. package/src/config.env.ts +2 -2
  74. package/src/core/common/decorators/response-model.decorator.ts +31 -0
  75. package/src/core/common/helpers/db.helper.ts +2 -2
  76. package/src/core/common/helpers/filter.helper.ts +3 -3
  77. package/src/core/common/helpers/input.helper.ts +2 -2
  78. package/src/core/common/helpers/interceptor.helper.ts +132 -0
  79. package/src/core/common/helpers/service.helper.ts +1 -1
  80. package/src/core/common/interceptors/check-security.interceptor.ts +44 -1
  81. package/src/core/common/interceptors/response-model.interceptor.ts +135 -0
  82. package/src/core/common/interceptors/translate-response.interceptor.ts +104 -0
  83. package/src/core/common/interfaces/server-options.interface.ts +162 -2
  84. package/src/core/common/middleware/request-context.middleware.ts +25 -0
  85. package/src/core/common/pipes/map-and-validate.pipe.ts +2 -2
  86. package/src/core/common/plugins/complexity.plugin.ts +2 -2
  87. package/src/core/common/plugins/mongoose-audit-fields.plugin.ts +74 -0
  88. package/src/core/common/plugins/mongoose-password.plugin.ts +100 -0
  89. package/src/core/common/plugins/mongoose-role-guard.plugin.ts +150 -0
  90. package/src/core/common/services/config.service.ts +2 -2
  91. package/src/core/common/services/model-registry.service.ts +25 -0
  92. package/src/core/common/services/module.service.ts +91 -1
  93. package/src/core/common/services/request-context.service.ts +69 -0
  94. package/src/core/modules/auth/guards/auth.guard.ts +2 -2
  95. package/src/core/modules/better-auth/core-better-auth.resolver.ts +4 -2
  96. package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +8 -8
  97. package/src/core/modules/error-code/core-error-code.controller.ts +3 -3
  98. package/src/core/modules/error-code/interfaces/error-code.interfaces.ts +1 -1
  99. package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +5 -5
  100. package/src/core/modules/system-setup/README.md +9 -9
  101. package/src/core/modules/system-setup/core-system-setup.controller.ts +1 -1
  102. package/src/core.module.ts +55 -4
  103. package/src/index.ts +10 -0
  104. package/src/server/modules/error-code/README.md +5 -5
  105. package/src/server/modules/error-code/error-code.controller.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.17.0",
3
+ "version": "11.19.0",
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",
@@ -75,33 +75,33 @@
75
75
  "dependencies": {
76
76
  "@apollo/server": "5.4.0",
77
77
  "@as-integrations/express5": "1.1.2",
78
- "@better-auth/passkey": "1.4.18",
78
+ "@better-auth/passkey": "1.5.4",
79
79
  "@getbrevo/brevo": "3.0.1",
80
80
  "@nestjs/apollo": "13.2.4",
81
- "@nestjs/common": "11.1.13",
82
- "@nestjs/core": "11.1.13",
81
+ "@nestjs/common": "11.1.16",
82
+ "@nestjs/core": "11.1.16",
83
83
  "@nestjs/graphql": "13.2.4",
84
84
  "@nestjs/jwt": "11.0.2",
85
85
  "@nestjs/mongoose": "11.0.4",
86
86
  "@nestjs/passport": "11.0.5",
87
- "@nestjs/platform-express": "11.1.13",
87
+ "@nestjs/platform-express": "11.1.16",
88
88
  "@nestjs/schedule": "6.1.1",
89
89
  "@nestjs/swagger": "11.2.6",
90
- "@nestjs/terminus": "11.0.0",
91
- "@nestjs/websockets": "11.1.13",
90
+ "@nestjs/terminus": "11.1.1",
91
+ "@nestjs/websockets": "11.1.16",
92
92
  "@tus/file-store": "2.0.0",
93
93
  "@tus/server": "2.3.0",
94
- "apollo-server-core": "3.13.0",
94
+
95
95
  "bcrypt": "6.0.0",
96
- "better-auth": "1.4.18",
96
+ "better-auth": "1.5.4",
97
97
  "class-transformer": "0.5.1",
98
98
  "class-validator": "0.14.3",
99
99
  "compression": "1.8.1",
100
100
  "cookie-parser": "1.4.7",
101
- "dotenv": "17.2.4",
102
- "ejs": "4.0.1",
101
+ "dotenv": "17.3.1",
102
+ "ejs": "5.0.1",
103
103
  "express": "5.2.1",
104
- "graphql": "16.12.0",
104
+ "graphql": "16.13.1",
105
105
  "graphql-query-complexity": "1.1.0",
106
106
  "graphql-subscriptions": "3.0.0",
107
107
  "graphql-upload": "15.0.2",
@@ -109,8 +109,8 @@
109
109
  "json-to-graphql-query": "2.3.0",
110
110
  "lodash": "4.17.23",
111
111
  "mongodb": "7.0.0",
112
- "mongoose": "9.1.6",
113
- "multer": "2.0.2",
112
+ "mongoose": "9.2.4",
113
+ "multer": "2.1.1",
114
114
  "node-mailjet": "6.0.11",
115
115
  "nodemailer": "8.0.1",
116
116
  "passport": "0.7.0",
@@ -124,32 +124,31 @@
124
124
  "@compodoc/compodoc": "1.2.1",
125
125
  "@nestjs/cli": "11.0.16",
126
126
  "@nestjs/schematics": "11.0.9",
127
- "@nestjs/testing": "11.1.13",
128
- "@swc/cli": "0.7.10",
129
- "@swc/core": "1.15.11",
127
+ "@nestjs/testing": "11.1.16",
128
+ "@swc/cli": "0.8.0",
129
+ "@swc/core": "1.15.18",
130
130
  "@types/compression": "1.8.1",
131
131
  "@types/cookie-parser": "1.4.10",
132
132
  "@types/ejs": "3.1.5",
133
- "@types/express": "4.17.21",
134
- "@types/lodash": "4.17.23",
135
- "@types/multer": "2.0.0",
136
- "@types/node": "25.2.2",
137
- "@types/nodemailer": "7.0.9",
133
+ "@types/express": "5.0.6",
134
+ "@types/lodash": "4.17.24",
135
+ "@types/multer": "2.1.0",
136
+ "@types/node": "25.3.5",
137
+ "@types/nodemailer": "7.0.11",
138
138
  "@types/passport": "1.0.17",
139
- "@types/supertest": "6.0.3",
139
+ "@types/supertest": "7.2.0",
140
140
  "@vitest/coverage-v8": "4.0.18",
141
141
  "@vitest/ui": "4.0.18",
142
142
  "ansi-colors": "4.1.3",
143
143
  "find-file-up": "2.0.1",
144
144
  "husky": "9.1.7",
145
- "nodemon": "3.1.11",
145
+ "nodemon": "3.1.14",
146
146
  "npm-watch": "0.13.0",
147
147
  "otpauth": "9.5.0",
148
- "oxfmt": "0.28.0",
149
- "oxlint": "1.43.0",
150
- "rimraf": "6.1.2",
148
+ "oxfmt": "0.36.0",
149
+ "oxlint": "1.51.0",
150
+ "rimraf": "6.1.3",
151
151
  "supertest": "7.2.2",
152
- "ts-loader": "9.5.4",
153
152
  "ts-morph": "27.0.2",
154
153
  "ts-node": "10.9.2",
155
154
  "tsconfig-paths": "4.2.0",
@@ -158,7 +157,7 @@
158
157
  "unplugin-swc": "1.5.9",
159
158
  "vite": "7.3.1",
160
159
  "vite-plugin-node": "7.0.0",
161
- "vite-tsconfig-paths": "6.1.0",
160
+ "vite-tsconfig-paths": "6.1.1",
162
161
  "vitest": "4.0.18"
163
162
  },
164
163
  "main": "dist/index.js",
@@ -178,7 +177,13 @@
178
177
  "packageManager": "pnpm@10.29.2",
179
178
  "pnpm": {
180
179
  "overrides": {
181
- "qs": "6.14.2"
180
+ "minimatch@<3.1.4": "3.1.4",
181
+ "minimatch@>=9.0.0 <9.0.7": "9.0.7",
182
+ "minimatch@>=10.0.0 <10.2.3": "10.2.4",
183
+ "rollup@>=4.0.0 <4.59.0": "4.59.0",
184
+ "serialize-javascript@<=7.0.2": "7.0.4",
185
+ "ajv@<6.14.0": "6.14.0",
186
+ "ajv@>=7.0.0-alpha.0 <8.18.0": "8.18.0"
182
187
  },
183
188
  "onlyBuiltDependencies": [
184
189
  "bcrypt",
package/src/config.env.ts CHANGED
@@ -123,7 +123,7 @@ const config: { [env: string]: IServerOptions } = {
123
123
  collation: {
124
124
  locale: 'de',
125
125
  },
126
- modelDocumentation: true,
126
+ modelDocumentation: false,
127
127
  uri: 'mongodb://127.0.0.1/nest-server-ci',
128
128
  },
129
129
  permissions: true,
@@ -369,7 +369,7 @@ const config: { [env: string]: IServerOptions } = {
369
369
  collation: {
370
370
  locale: 'de',
371
371
  },
372
- modelDocumentation: true,
372
+ modelDocumentation: false,
373
373
  uri: 'mongodb://127.0.0.1/nest-server-e2e',
374
374
  },
375
375
  permissions: true,
@@ -0,0 +1,31 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ import { CoreModel } from '../models/core-model.model';
4
+
5
+ /**
6
+ * Metadata key used to store the explicit response model class on a handler.
7
+ * Shared between the @ResponseModel() decorator and interceptor.helper.ts.
8
+ */
9
+ export const RESPONSE_MODEL_KEY = 'response_model_class';
10
+
11
+ /**
12
+ * Decorator to explicitly specify the model class for automatic response conversion.
13
+ *
14
+ * In most cases this is NOT needed because the type is resolved automatically:
15
+ * - GraphQL: from @Query/@Mutation return type metadata
16
+ * - REST: from @ApiOkResponse({ type: Model }) / @ApiCreatedResponse({ type: Model })
17
+ *
18
+ * Use this decorator only when automatic resolution fails or when no Swagger
19
+ * decorators are present on a REST endpoint.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * @ResponseModel(User)
24
+ * @Get(':id')
25
+ * async getUser(@Param('id') id: string): Promise<User> {
26
+ * return this.userService.mainDbModel.findById(id).exec();
27
+ * }
28
+ * ```
29
+ */
30
+ export const ResponseModel = (modelClass: new (...args: any[]) => CoreModel) =>
31
+ SetMetadata(RESPONSE_MODEL_KEY, modelClass);
@@ -626,8 +626,8 @@ export async function setPopulates<T = Document | Query<any, any>>(
626
626
 
627
627
  // Query => Chaining
628
628
  if (queryOrDocument instanceof Query) {
629
- for (const options of populateOptions) {
630
- queryOrDocument = (queryOrDocument as any).populate(options);
629
+ for (const populateOption of populateOptions) {
630
+ queryOrDocument = (queryOrDocument as any).populate(populateOption);
631
631
  }
632
632
 
633
633
  // Document => Non chaining
@@ -177,7 +177,7 @@ export function generateFilterQuery<T = any>(
177
177
  // Process single filter
178
178
  if (filter.singleFilter) {
179
179
  // Init variables
180
- const { convertToObjectId, isReference, not, options } = filter.singleFilter;
180
+ const { convertToObjectId, isReference, not, options: filterOptions } = filter.singleFilter;
181
181
  let field = filter.singleFilter.field;
182
182
  let value = filter.singleFilter.value;
183
183
 
@@ -232,11 +232,11 @@ export function generateFilterQuery<T = any>(
232
232
  result[field] = not
233
233
  ? {
234
234
  $not: {
235
- $options: options || '',
235
+ $options: filterOptions || '',
236
236
  $regex: new RegExp(value),
237
237
  },
238
238
  }
239
- : { $options: options || '', $regex: new RegExp(value) };
239
+ : { $options: filterOptions || '', $regex: new RegExp(value) };
240
240
  break;
241
241
  }
242
242
  }
@@ -388,9 +388,9 @@ export function clone(object: any, options?: { checkResult?: boolean; circles?:
388
388
  throw new Error('Cloned object differs from original object', { cause: e });
389
389
  }
390
390
  return clonedWithCircles;
391
- } catch (e) {
391
+ } catch (innerError) {
392
392
  if (config.debug) {
393
- console.debug(e, 'rfcd with circles did not work => automatic use of _.clone!');
393
+ console.debug(innerError, 'rfcd with circles did not work => automatic use of _.clone!');
394
394
  }
395
395
  return _.cloneDeep(object);
396
396
  }
@@ -0,0 +1,132 @@
1
+ import { ExecutionContext } from '@nestjs/common';
2
+ import { GqlExecutionContext } from '@nestjs/graphql';
3
+
4
+ import { RESPONSE_MODEL_KEY } from '../decorators/response-model.decorator';
5
+ import { CoreModel } from '../models/core-model.model';
6
+
7
+ // Cache: handler function → resolved model class (or null)
8
+ // Handler references are stable per route, so the cache is bounded by the number of routes.
9
+ const resolvedModelCache = new Map<Function, (new (...args: any[]) => CoreModel) | null>();
10
+
11
+ /**
12
+ * Resolve the expected model class for a handler's response.
13
+ * Results are cached per handler function for zero-cost subsequent lookups.
14
+ *
15
+ * Priority:
16
+ * 1. Explicit @ResponseModel(ModelClass) decorator
17
+ * 2. GraphQL TypeMetadataStorage lookup (automatic for @Query/@Mutation)
18
+ * 3. Swagger @ApiOkResponse / @ApiCreatedResponse type (automatic for REST)
19
+ * 4. null (no auto-mapping)
20
+ */
21
+ export function resolveResponseModelClass(context: ExecutionContext): (new (...args: any[]) => CoreModel) | null {
22
+ const handler = context.getHandler();
23
+
24
+ // Return cached result (hit after first request per route)
25
+ if (resolvedModelCache.has(handler)) {
26
+ return resolvedModelCache.get(handler);
27
+ }
28
+
29
+ const result = resolveResponseModelClassUncached(context, handler);
30
+ resolvedModelCache.set(handler, result);
31
+ return result;
32
+ }
33
+
34
+ function resolveResponseModelClassUncached(
35
+ context: ExecutionContext,
36
+ handler: Function,
37
+ ): (new (...args: any[]) => CoreModel) | null {
38
+ // 1. Explicit @ResponseModel decorator
39
+ const explicit = Reflect.getMetadata(RESPONSE_MODEL_KEY, handler);
40
+ if (explicit) {
41
+ return explicit;
42
+ }
43
+
44
+ // 2. GraphQL TypeMetadataStorage lookup
45
+ try {
46
+ const gqlContext = GqlExecutionContext.create(context);
47
+ const info = gqlContext.getInfo?.();
48
+ if (info) {
49
+ return resolveFromGraphQlMetadata(context);
50
+ }
51
+ } catch {
52
+ // Not a GraphQL context
53
+ }
54
+
55
+ // 3. Swagger @ApiOkResponse / @ApiCreatedResponse type (for REST controllers)
56
+ const swaggerType = resolveFromSwaggerMetadata(handler);
57
+ if (swaggerType) {
58
+ return swaggerType;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Resolve model class from GraphQL @Query/@Mutation metadata
66
+ */
67
+ function resolveFromGraphQlMetadata(context: ExecutionContext): (new (...args: any[]) => CoreModel) | null {
68
+ try {
69
+ // Uses @nestjs/graphql internal path to access TypeMetadataStorage — the only way to resolve
70
+ // @Query/@Mutation return types at runtime. This path has been stable since @nestjs/graphql v10.
71
+ // Wrapped in try/catch so a path change in a future version degrades gracefully (returns null).
72
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
73
+ const { TypeMetadataStorage } = require('@nestjs/graphql/dist/schema-builder/storages/type-metadata.storage');
74
+
75
+ const handler = context.getHandler();
76
+ const target = context.getClass();
77
+ const methodName = handler.name;
78
+
79
+ // Search queries and mutations
80
+ const allMetadata = [...TypeMetadataStorage.getQueriesMetadata(), ...TypeMetadataStorage.getMutationsMetadata()];
81
+
82
+ const meta = allMetadata.find((m: any) => m.target === target && m.methodName === methodName);
83
+
84
+ if (meta?.typeFn) {
85
+ const resolvedType = meta.typeFn();
86
+ if (resolvedType && typeof resolvedType === 'function' && isCoreModelSubclass(resolvedType)) {
87
+ return resolvedType as new (...args: any[]) => CoreModel;
88
+ }
89
+ }
90
+ } catch {
91
+ // TypeMetadataStorage not available or other issue
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Resolve model class from Swagger @ApiOkResponse / @ApiCreatedResponse metadata.
99
+ * Reads the `type` from the response metadata stored by @nestjs/swagger decorators.
100
+ */
101
+ function resolveFromSwaggerMetadata(handler: Function): (new (...args: any[]) => CoreModel) | null {
102
+ try {
103
+ const SWAGGER_API_RESPONSE_KEY = 'swagger/apiResponse';
104
+ const responses = Reflect.getMetadata(SWAGGER_API_RESPONSE_KEY, handler);
105
+ if (!responses || typeof responses !== 'object') {
106
+ return null;
107
+ }
108
+
109
+ // Check common success status codes (200 OK, 201 Created)
110
+ for (const statusCode of [200, 201]) {
111
+ const responseMeta = responses[statusCode];
112
+ if (responseMeta?.type && typeof responseMeta.type === 'function' && isCoreModelSubclass(responseMeta.type)) {
113
+ return responseMeta.type as new (...args: any[]) => CoreModel;
114
+ }
115
+ }
116
+ } catch {
117
+ // Swagger not available or metadata not found
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ function isCoreModelSubclass(cls: Function): boolean {
124
+ let proto = cls.prototype;
125
+ while (proto) {
126
+ if (proto.constructor === CoreModel || proto.constructor?.name === 'CoreModel') {
127
+ return true;
128
+ }
129
+ proto = Object.getPrototypeOf(proto);
130
+ }
131
+ return false;
132
+ }
@@ -336,7 +336,7 @@ export function prepareServiceOptions(
336
336
  return serviceOptions;
337
337
  }
338
338
 
339
- function applyTranslationsRecursively(obj: any, language: string, visited: WeakSet<object> = new WeakSet()) {
339
+ export function applyTranslationsRecursively(obj: any, language: string, visited: WeakSet<object> = new WeakSet()) {
340
340
  if (typeof obj !== 'object' || obj === null) {
341
341
  return;
342
342
  }
@@ -15,6 +15,8 @@ export class CheckSecurityInterceptor implements NestInterceptor {
15
15
  config = {
16
16
  debug: false,
17
17
  noteCheckedObjects: true,
18
+ removeSecretFields: true,
19
+ secretFields: ['password', 'verificationToken', 'passwordResetToken', 'refreshTokens', 'tempTokens'],
18
20
  };
19
21
 
20
22
  constructor(private readonly configService: ConfigService) {
@@ -22,6 +24,11 @@ export class CheckSecurityInterceptor implements NestInterceptor {
22
24
  if (typeof configuration === 'object') {
23
25
  this.config = { ...this.config, ...configuration };
24
26
  }
27
+ // Allow overriding secretFields from security config
28
+ const globalSecretFields = this.configService.getFastButReadOnly('security.secretFields');
29
+ if (Array.isArray(globalSecretFields)) {
30
+ this.config.secretFields = globalSecretFields;
31
+ }
25
32
  }
26
33
 
27
34
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
@@ -99,8 +106,44 @@ export class CheckSecurityInterceptor implements NestInterceptor {
99
106
  );
100
107
  };
101
108
 
109
+ // Fallback: Remove known secret fields regardless of model type (recursive into plain objects)
110
+ const isPlainLike = (val: any): boolean => {
111
+ if (!val || typeof val !== 'object' || Array.isArray(val)) return false;
112
+ // Skip Streams, Buffers, Dates, RegExps and other special objects
113
+ if (typeof val.pipe === 'function') return false;
114
+ if (Buffer.isBuffer(val)) return false;
115
+ if (val instanceof Date || val instanceof RegExp) return false;
116
+ const proto = Object.getPrototypeOf(val);
117
+ return proto === null || proto === Object.prototype || typeof val.constructor === 'function';
118
+ };
119
+ const removeSecrets = (data: any) => {
120
+ if (!this.config.removeSecretFields || !data || typeof data !== 'object') {
121
+ return data;
122
+ }
123
+ if (Array.isArray(data)) {
124
+ data.forEach(removeSecrets);
125
+ return data;
126
+ }
127
+ if (!isPlainLike(data)) {
128
+ return data;
129
+ }
130
+ for (const field of this.config.secretFields) {
131
+ if (field in data && data[field] !== undefined) {
132
+ data[field] = undefined;
133
+ }
134
+ }
135
+ // Recurse into nested plain objects
136
+ for (const key of Object.keys(data)) {
137
+ const value = data[key];
138
+ if (value && typeof value === 'object' && !this.config.secretFields.includes(key)) {
139
+ removeSecrets(value);
140
+ }
141
+ }
142
+ return data;
143
+ };
144
+
102
145
  // Check response
103
- const result = next.handle().pipe(map(check));
146
+ const result = next.handle().pipe(map(check), map(removeSecrets));
104
147
  if (this.config.debug && Date.now() - start >= (typeof this.config.debug === 'number' ? this.config.debug : 100)) {
105
148
  console.warn(
106
149
  `Duration for CheckResponseInterceptor is too long: ${Date.now() - start}ms`,
@@ -0,0 +1,135 @@
1
+ import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
2
+ import { Observable } from 'rxjs';
3
+ import { map } from 'rxjs/operators';
4
+
5
+ import { resolveResponseModelClass } from '../helpers/interceptor.helper';
6
+ import { CoreModel } from '../models/core-model.model';
7
+ import { ConfigService } from '../services/config.service';
8
+ import { ModelRegistry } from '../services/model-registry.service';
9
+
10
+ /**
11
+ * Interceptor that automatically converts plain objects and Mongoose documents
12
+ * to CoreModel instances, enabling securityCheck() and @Restricted metadata.
13
+ *
14
+ * This is the safety net that ensures output security even when developers
15
+ * bypass CrudService.process() and use direct Mongoose queries.
16
+ *
17
+ * Execution order on response (NestJS runs APP_INTERCEPTOR in reverse registration order):
18
+ * 1. ResponseModelInterceptor (registered last → runs first on response)
19
+ * 2. CheckSecurityInterceptor (processes securityCheck())
20
+ * 3. CheckResponseInterceptor (filters @Restricted fields)
21
+ */
22
+ @Injectable()
23
+ export class ResponseModelInterceptor implements NestInterceptor {
24
+ private readonly logger = new Logger(ResponseModelInterceptor.name);
25
+ private config = {
26
+ debug: false,
27
+ };
28
+
29
+ constructor(private readonly configService: ConfigService) {
30
+ const configuration = this.configService.getFastButReadOnly('security.responseModelInterceptor');
31
+ if (typeof configuration === 'object') {
32
+ this.config = { ...this.config, ...configuration };
33
+ }
34
+ }
35
+
36
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
37
+ const modelClass = resolveResponseModelClass(context);
38
+
39
+ return next.handle().pipe(
40
+ map((data) => {
41
+ if (!modelClass || data === null || data === undefined || typeof data !== 'object') {
42
+ return data;
43
+ }
44
+
45
+ // Already the correct model instance
46
+ if (data instanceof modelClass) {
47
+ return data;
48
+ }
49
+
50
+ // Already processed by another interceptor
51
+ if (data._objectAlreadyCheckedForRestrictions) {
52
+ return data;
53
+ }
54
+
55
+ return this.convertToModel(data, modelClass, context);
56
+ }),
57
+ );
58
+ }
59
+
60
+ private convertToModel(data: any, modelClass: new (...args: any[]) => CoreModel, context: ExecutionContext): any {
61
+ // Scalars/primitives pass through
62
+ if (typeof data !== 'object' || data === null) {
63
+ return data;
64
+ }
65
+
66
+ // Array of items
67
+ if (Array.isArray(data)) {
68
+ return data.map((item) => this.convertSingleItem(item, modelClass, context));
69
+ }
70
+
71
+ // Paginated wrapper objects from CrudService (e.g. { items: [...], totalCount, pagination })
72
+ // Identified by 'items' array + pagination markers to avoid false positives on models with 'items' field
73
+ if (Array.isArray(data.items) && ('totalCount' in data || 'pagination' in data)) {
74
+ data.items = data.items.map((item: any) => this.convertSingleItem(item, modelClass, context));
75
+ return data;
76
+ }
77
+
78
+ // Single object
79
+ return this.convertSingleItem(data, modelClass, context);
80
+ }
81
+
82
+ private convertSingleItem(item: any, modelClass: new (...args: any[]) => CoreModel, context: ExecutionContext): any {
83
+ if (item === null || item === undefined || typeof item !== 'object') {
84
+ return item;
85
+ }
86
+
87
+ // Already the correct type
88
+ if (item instanceof modelClass) {
89
+ return item;
90
+ }
91
+
92
+ // Already processed
93
+ if (item._objectAlreadyCheckedForRestrictions) {
94
+ return item;
95
+ }
96
+
97
+ // Convert Mongoose document to plain object first
98
+ const plain = typeof item.toObject === 'function' ? item.toObject() : item;
99
+
100
+ try {
101
+ const mapped = (modelClass as any).map(plain);
102
+ if (this.config.debug) {
103
+ const className = context.getClass()?.name;
104
+ const methodName = context.getHandler()?.name;
105
+ this.logger.warn(
106
+ `Auto-converted plain object to ${modelClass.name} in ${className}.${methodName}. Consider using CrudService methods.`,
107
+ );
108
+ }
109
+ return mapped;
110
+ } catch {
111
+ // If mapping fails, try resolving via ModelRegistry using the Mongoose model name
112
+ return this.tryRegistryFallback(plain) || item;
113
+ }
114
+ }
115
+
116
+ private tryRegistryFallback(item: any): any {
117
+ // Mongoose documents have a collection property with modelName
118
+ const modelName = item?.constructor?.modelName || item?.collection?.modelName;
119
+ if (!modelName) {
120
+ return null;
121
+ }
122
+
123
+ const registeredClass = ModelRegistry.getModelClass(modelName);
124
+ if (!registeredClass) {
125
+ return null;
126
+ }
127
+
128
+ try {
129
+ const plain = typeof item.toObject === 'function' ? item.toObject() : item;
130
+ return (registeredClass as any).map(plain);
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,104 @@
1
+ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2
+ import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
3
+ import { Observable } from 'rxjs';
4
+ import { map } from 'rxjs/operators';
5
+
6
+ import { applyTranslationsRecursively } from '../helpers/service.helper';
7
+
8
+ /**
9
+ * Interceptor that automatically applies translations from _translations
10
+ * based on the Accept-Language header of the request.
11
+ *
12
+ * This ensures translations work even when developers bypass CrudService.process()
13
+ * and use direct Mongoose operations.
14
+ *
15
+ * Execution order on response:
16
+ * 1. ResponseModelInterceptor (plain → model)
17
+ * 2. TranslateResponseInterceptor (applies translations) ← THIS
18
+ * 3. CheckSecurityInterceptor (securityCheck())
19
+ * 4. CheckResponseInterceptor (@Restricted fields)
20
+ */
21
+ @Injectable()
22
+ export class TranslateResponseInterceptor implements NestInterceptor {
23
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
24
+ const language = this.getLanguage(context);
25
+
26
+ if (!language) {
27
+ return next.handle();
28
+ }
29
+
30
+ return next.handle().pipe(
31
+ map((data) => {
32
+ if (!data || typeof data !== 'object') {
33
+ return data;
34
+ }
35
+ this.applyTranslations(data, language);
36
+ return data;
37
+ }),
38
+ );
39
+ }
40
+
41
+ private applyTranslations(data: any, language: string): void {
42
+ // Early bailout: skip if no _translations anywhere in the response
43
+ if (!this.hasTranslations(data)) {
44
+ return;
45
+ }
46
+
47
+ if (Array.isArray(data)) {
48
+ for (const item of data) {
49
+ if (item && typeof item === 'object') {
50
+ applyTranslationsRecursively(item, language);
51
+ }
52
+ }
53
+ } else if (data.items && Array.isArray(data.items)) {
54
+ // Wrapper objects (e.g. { items: [...], totalCount })
55
+ for (const item of data.items) {
56
+ if (item && typeof item === 'object') {
57
+ applyTranslationsRecursively(item, language);
58
+ }
59
+ }
60
+ } else {
61
+ applyTranslationsRecursively(data, language);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Quick check if _translations exists at the top level of the response.
67
+ * Avoids expensive recursive traversal when no translations are present.
68
+ */
69
+ private hasTranslations(data: any): boolean {
70
+ if (!data || typeof data !== 'object') {
71
+ return false;
72
+ }
73
+ if (Array.isArray(data)) {
74
+ return data.length > 0 && data[0] && typeof data[0] === 'object' && '_translations' in data[0];
75
+ }
76
+ if (data.items && Array.isArray(data.items)) {
77
+ return (
78
+ data.items.length > 0 && data.items[0] && typeof data.items[0] === 'object' && '_translations' in data.items[0]
79
+ );
80
+ }
81
+ return '_translations' in data;
82
+ }
83
+
84
+ private getLanguage(context: ExecutionContext): string | null {
85
+ // GraphQL context
86
+ try {
87
+ if (context.getType<GqlContextType>() === 'graphql') {
88
+ const gqlContext = GqlExecutionContext.create(context);
89
+ const req = gqlContext.getContext()?.req;
90
+ return req?.headers?.['accept-language'] || null;
91
+ }
92
+ } catch {
93
+ // Not a GraphQL context
94
+ }
95
+
96
+ // HTTP context
97
+ try {
98
+ const req = context.switchToHttp()?.getRequest();
99
+ return req?.headers?.['accept-language'] || null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ }