@lenne.tech/nest-server 11.24.1 → 11.24.3

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 (30) hide show
  1. package/.claude/rules/configurable-features.md +1 -0
  2. package/.claude/rules/package-management.md +61 -2
  3. package/CLAUDE.md +77 -0
  4. package/FRAMEWORK-API.md +1 -1
  5. package/dist/core/common/decorators/unified-field.decorator.d.ts +2 -1
  6. package/dist/core/common/decorators/unified-field.decorator.js +60 -13
  7. package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
  8. package/dist/core/common/helpers/register-enum.helper.d.ts +6 -0
  9. package/dist/core/common/helpers/register-enum.helper.js +23 -1
  10. package/dist/core/common/helpers/register-enum.helper.js.map +1 -1
  11. package/dist/core/common/inputs/combined-filter.input.js +1 -1
  12. package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
  13. package/dist/core/common/inputs/single-filter.input.js +1 -1
  14. package/dist/core/common/inputs/single-filter.input.js.map +1 -1
  15. package/dist/core/common/inputs/sort.input.js +1 -1
  16. package/dist/core/common/inputs/sort.input.js.map +1 -1
  17. package/dist/core/common/interfaces/server-options.interface.d.ts +7 -1
  18. package/dist/core/common/services/email.service.d.ts +2 -2
  19. package/dist/core/common/services/email.service.js +7 -0
  20. package/dist/core/common/services/email.service.js.map +1 -1
  21. package/dist/tsconfig.build.tsbuildinfo +1 -1
  22. package/migration-guides/11.24.2-to-11.24.3.md +255 -0
  23. package/package.json +32 -13
  24. package/src/core/common/decorators/unified-field.decorator.ts +173 -23
  25. package/src/core/common/helpers/register-enum.helper.ts +89 -1
  26. package/src/core/common/inputs/combined-filter.input.ts +1 -1
  27. package/src/core/common/inputs/single-filter.input.ts +1 -1
  28. package/src/core/common/inputs/sort.input.ts +1 -1
  29. package/src/core/common/interfaces/server-options.interface.ts +34 -2
  30. package/src/core/common/services/email.service.ts +17 -3
@@ -2,6 +2,18 @@ import { registerEnumType } from '@nestjs/graphql';
2
2
 
3
3
  import { enumNameRegistry } from '../decorators/unified-field.decorator';
4
4
 
5
+ /**
6
+ * Tracks which enum objects have been registered for GraphQL via registerEnum/registerEnums.
7
+ * Used to prevent duplicate registerEnumType() calls which cause schema build errors.
8
+ * Separate from enumNameRegistry (Swagger) because GraphQL registration is a distinct concern.
9
+ *
10
+ * @internal Consumers should use {@link enumNameRegistry} (from unified-field.decorator) to
11
+ * verify Swagger/name registration. This registry is exported for advanced use cases
12
+ * (e.g. checking if an enum is already registered for GraphQL before calling registerEnumType
13
+ * directly), but most projects should not need to interact with it.
14
+ */
15
+ export const graphqlEnumRegistry = new WeakSet<object>();
16
+
5
17
  /**
6
18
  * Interface defining options for the registerEnum helper
7
19
  */
@@ -83,11 +95,87 @@ export function registerEnum<T extends object = any>(enumRef: T, options: Regist
83
95
  }
84
96
 
85
97
  // Register for GraphQL if enabled
86
- if (graphql) {
98
+ if (graphql && !graphqlEnumRegistry.has(enumRef)) {
87
99
  registerEnumType(enumRef, {
88
100
  description,
89
101
  name,
90
102
  valuesMap,
91
103
  });
104
+ graphqlEnumRegistry.add(enumRef);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Options for {@link registerEnums} bulk registration.
110
+ *
111
+ * Controls which API layers the enums are registered for (GraphQL and/or Swagger/REST).
112
+ * When omitted, enums are registered for both layers.
113
+ */
114
+ export interface RegisterEnumsOptions {
115
+ /**
116
+ * Whether to register enums for GraphQL using registerEnumType.
117
+ * @default true
118
+ */
119
+ graphql?: boolean;
120
+
121
+ /**
122
+ * Whether to register enums in the enumNameRegistry for Swagger/REST.
123
+ * When enabled, enum names are auto-detected by {@link UnifiedField} decorators
124
+ * for the `enumName` property in Swagger/OpenAPI schemas.
125
+ * @default true
126
+ */
127
+ swagger?: boolean;
128
+ }
129
+
130
+ /**
131
+ * Bulk-register all enums from a barrel-export namespace object.
132
+ *
133
+ * Uses the **export key names** as enum names — no manual name repetition
134
+ * needed. Call this once per project, typically in your module setup.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * // src/server/common/enums/index.ts (barrel file)
139
+ * export { ContactStatusEnum } from './contact-status.enum';
140
+ * export { IndustryEnum } from './industry.enum';
141
+ * export { NotifyViaEnum } from './notify-via.enum';
142
+ *
143
+ * // src/server/server.module.ts (one line)
144
+ * import * as Enums from './common/enums';
145
+ * registerEnums(Enums);
146
+ *
147
+ * // Result: ContactStatusEnum, IndustryEnum, NotifyViaEnum are all
148
+ * // registered for both GraphQL and Swagger auto-detection.
149
+ * ```
150
+ *
151
+ * Only plain objects with string/number values (enum-like objects) are
152
+ * registered. Non-enum exports (functions, classes, strings) are skipped.
153
+ *
154
+ * @param namespace - The barrel-export namespace object (`import * as X from '...'`)
155
+ * @param options - Optional: control GraphQL/Swagger registration
156
+ */
157
+ export function registerEnums(namespace: Record<string, any>, options?: RegisterEnumsOptions): void {
158
+ const { graphql = true, swagger = true } = options ?? {};
159
+
160
+ for (const [name, value] of Object.entries(namespace)) {
161
+ // Skip non-objects (functions, strings, numbers, etc.)
162
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
+ continue;
164
+ }
165
+
166
+ // Skip if already registered for all requested targets
167
+ const alreadySwagger = !swagger || enumNameRegistry.has(value);
168
+ const alreadyGraphql = !graphql || graphqlEnumRegistry.has(value);
169
+ if (alreadySwagger && alreadyGraphql) {
170
+ continue;
171
+ }
172
+
173
+ // Verify it looks like an enum: all own values are strings or numbers
174
+ const vals = Object.values(value);
175
+ if (vals.length === 0 || !vals.every((v) => typeof v === 'string' || typeof v === 'number')) {
176
+ continue;
177
+ }
178
+
179
+ registerEnum(value, { graphql, name, swagger });
92
180
  }
93
181
  }
@@ -18,7 +18,7 @@ export class CombinedFilterInput extends CoreInput {
18
18
  */
19
19
  @UnifiedField({
20
20
  description: 'Logical Operator to combine filters',
21
- enum: { enum: LogicalOperatorEnum },
21
+ enum: LogicalOperatorEnum,
22
22
  roles: RoleEnum.S_EVERYONE,
23
23
  })
24
24
  logicalOperator: LogicalOperatorEnum = undefined;
@@ -58,7 +58,7 @@ export class SingleFilterInput extends CoreInput {
58
58
  */
59
59
  @UnifiedField({
60
60
  description: '[Comparison operator](https://docs.mongodb.com/manual/reference/operator/query-comparison/)',
61
- enum: { enum: ComparisonOperatorEnum },
61
+ enum: ComparisonOperatorEnum,
62
62
  roles: RoleEnum.S_EVERYONE,
63
63
  })
64
64
  operator: ComparisonOperatorEnum = undefined;
@@ -26,7 +26,7 @@ export class SortInput extends CoreInput {
26
26
  */
27
27
  @UnifiedField({
28
28
  description: 'SortInput order of the field',
29
- enum: { enum: SortOrderEnum },
29
+ enum: SortOrderEnum,
30
30
  roles: RoleEnum.S_EVERYONE,
31
31
  })
32
32
  order: SortOrderEnum = undefined;
@@ -10,7 +10,12 @@ import { MongoosePingCheckSettings } from '@nestjs/terminus/dist/health-indicato
10
10
  import { DiskHealthIndicatorOptions } from '@nestjs/terminus/dist/health-indicator/disk/disk-health-options.type';
11
11
  import compression from 'compression';
12
12
  import { CollationOptions } from 'mongodb';
13
+ import type * as JSONTransport from 'nodemailer/lib/json-transport';
14
+ import type * as SendmailTransport from 'nodemailer/lib/sendmail-transport';
15
+ import type * as SESTransport from 'nodemailer/lib/ses-transport';
16
+ import type * as SMTPPool from 'nodemailer/lib/smtp-pool';
13
17
  import * as SMTPTransport from 'nodemailer/lib/smtp-transport';
18
+ import type * as StreamTransport from 'nodemailer/lib/stream-transport';
14
19
 
15
20
  import { Falsy } from '../types/falsy.type';
16
21
  import { IPermissions } from '../../modules/permissions/interfaces/permissions.interface';
@@ -24,6 +29,29 @@ import { MailjetOptions } from './mailjet-options.interface';
24
29
  */
25
30
  export type BetterAuthFieldType = 'boolean' | 'date' | 'json' | 'number' | 'number[]' | 'string' | 'string[]';
26
31
 
32
+ /**
33
+ * All transport configurations accepted by `nodemailer.createTransport()`.
34
+ *
35
+ * Covers every built-in transport nodemailer ships with. `JSONTransport` is
36
+ * particularly useful in CI / e2e tests: `{ jsonTransport: true }` serializes
37
+ * outgoing mail to a JSON string and returns a valid response without any
38
+ * network I/O — no SMTP server, no credentials, no flakiness.
39
+ */
40
+ export type MailTransportOptions =
41
+ | JSONTransport
42
+ | JSONTransport.Options
43
+ | SendmailTransport
44
+ | SendmailTransport.Options
45
+ | SESTransport
46
+ | SESTransport.Options
47
+ | SMTPPool
48
+ | SMTPPool.Options
49
+ | SMTPTransport
50
+ | SMTPTransport.Options
51
+ | StreamTransport
52
+ | StreamTransport.Options
53
+ | string;
54
+
27
55
  /**
28
56
  * Interface for Auth configuration
29
57
  *
@@ -1147,9 +1175,13 @@ export interface IServerOptions {
1147
1175
  passwordResetLink?: string;
1148
1176
 
1149
1177
  /**
1150
- * SMTP configuration for nodemailer
1178
+ * Mail transport configuration for nodemailer.
1179
+ *
1180
+ * Accepts any transport type supported by `nodemailer.createTransport()`.
1181
+ * See {@link MailTransportOptions} for the full list including
1182
+ * `JSONTransport` / `{ jsonTransport: true }` for CI and e2e tests.
1151
1183
  */
1152
- smtp?: SMTPTransport | SMTPTransport.Options | string;
1184
+ smtp?: MailTransportOptions;
1153
1185
 
1154
1186
  /**
1155
1187
  * Verification link for email
@@ -1,11 +1,10 @@
1
- import type SMTPPool = require('nodemailer/lib/smtp-pool');
2
-
3
1
  import { createHash } from 'crypto';
4
2
  import { Injectable, OnModuleDestroy } from '@nestjs/common';
5
3
  import nodemailer = require('nodemailer');
6
4
  import { Attachment } from 'nodemailer/lib/mailer';
7
5
 
8
6
  import { isNonEmptyString, isTrue, returnFalse } from '../helpers/input.helper';
7
+ import { MailTransportOptions } from '../interfaces/server-options.interface';
9
8
  import { ConfigService } from './config.service';
10
9
  import { TemplateService } from './template.service';
11
10
 
@@ -49,7 +48,7 @@ export class EmailService implements OnModuleDestroy {
49
48
  htmlTemplate?: string;
50
49
  senderEmail?: string;
51
50
  senderName?: string;
52
- smtp?: SMTPPool | SMTPPool.Options;
51
+ smtp?: MailTransportOptions;
53
52
  templateData?: { [key: string]: any };
54
53
  text?: string;
55
54
  textTemplate?: string;
@@ -90,6 +89,21 @@ export class EmailService implements OnModuleDestroy {
90
89
  isNonEmptyString(html);
91
90
  }
92
91
 
92
+ // Guard: JSONTransport silently discards all mail — block in production / staging
93
+ // to prevent accidental misconfiguration that causes password-reset, 2FA, and
94
+ // verification emails to vanish without error.
95
+ // Uses truthy check (not strict === true) because nodemailer activates
96
+ // JSONTransport for any truthy value of options.jsonTransport.
97
+ const env = this.configService.getFastButReadOnly<string>('env');
98
+ if (env === 'production' || env === 'staging') {
99
+ if (typeof smtp === 'object' && smtp !== null && !!(smtp as Record<string, unknown>).jsonTransport) {
100
+ throw new Error(
101
+ 'JSONTransport (jsonTransport: true) is not permitted in production/staging environments. ' +
102
+ 'It silently discards all outgoing email. Check email.smtp in your config.',
103
+ );
104
+ }
105
+ }
106
+
93
107
  // Reuse transporter if SMTP config hasn't changed (avoids creating new connections per email)
94
108
  // Use hash instead of raw JSON to avoid keeping credentials as a string in memory
95
109
  const smtpKey = createHash('sha256').update(JSON.stringify(smtp)).digest('hex');