@lenne.tech/nest-server 11.24.2 → 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.
- package/.claude/rules/configurable-features.md +1 -0
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/common/decorators/unified-field.decorator.d.ts +2 -1
- package/dist/core/common/decorators/unified-field.decorator.js +60 -13
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/helpers/register-enum.helper.d.ts +6 -0
- package/dist/core/common/helpers/register-enum.helper.js +23 -1
- package/dist/core/common/helpers/register-enum.helper.js.map +1 -1
- package/dist/core/common/inputs/combined-filter.input.js +1 -1
- package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
- package/dist/core/common/inputs/single-filter.input.js +1 -1
- package/dist/core/common/inputs/single-filter.input.js.map +1 -1
- package/dist/core/common/inputs/sort.input.js +1 -1
- package/dist/core/common/inputs/sort.input.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +7 -1
- package/dist/core/common/services/email.service.d.ts +2 -2
- package/dist/core/common/services/email.service.js +7 -0
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.24.2-to-11.24.3.md +255 -0
- package/package.json +3 -2
- package/src/core/common/decorators/unified-field.decorator.ts +173 -23
- package/src/core/common/helpers/register-enum.helper.ts +89 -1
- package/src/core/common/inputs/combined-filter.input.ts +1 -1
- package/src/core/common/inputs/single-filter.input.ts +1 -1
- package/src/core/common/inputs/sort.input.ts +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +34 -2
- package/src/core/common/services/email.service.ts +17 -3
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Migration Guide: 11.24.2 → 11.24.3
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | Simplified enum API, `registerEnums()`, expanded mail transports |
|
|
9
|
+
| **Bugfixes** | Optional enum fields no longer rejected as "must be an object" |
|
|
10
|
+
| **Deprecations** | `enum: { enum: MyEnum, enumName: ... }` long form (still works) |
|
|
11
|
+
| **Migration Effort** | ~5 min setup + find-and-replace per project |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start (Minimum — No Code Changes Required)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm install @lenne.tech/nest-server@11.24.3
|
|
19
|
+
pnpm run build
|
|
20
|
+
pnpm test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Everything works as before. You will see **deprecation warnings** for every
|
|
24
|
+
`enum: { enum: ... }` usage at startup. These are informational — no behavior
|
|
25
|
+
changes, no errors. Follow the steps below to migrate and silence them.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What Changed
|
|
30
|
+
|
|
31
|
+
### Bugfix: Optional Enum Validation
|
|
32
|
+
|
|
33
|
+
Fields like `status?: MyEnum` were rejected with `"must be an object"`.
|
|
34
|
+
Fixed — `IsEnum()` is now the authoritative validator for enum fields.
|
|
35
|
+
|
|
36
|
+
### New: Simplified Enum API
|
|
37
|
+
|
|
38
|
+
The `{ enum: MyEnum }` wrapper is no longer needed:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// OLD (deprecated — still works, emits warning)
|
|
42
|
+
@UnifiedField({ enum: { enum: StatusEnum, enumName: 'StatusEnum' } })
|
|
43
|
+
|
|
44
|
+
// NEW (recommended)
|
|
45
|
+
@UnifiedField({ enum: StatusEnum })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### New: `registerEnums()` Bulk Registration
|
|
49
|
+
|
|
50
|
+
One-line setup per project — enables enum name auto-detection so you can
|
|
51
|
+
drop `enumName` entirely:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import * as Enums from './common/enums';
|
|
55
|
+
import { registerEnums } from '@lenne.tech/nest-server';
|
|
56
|
+
registerEnums(Enums);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### New: `isArray` Auto-Propagation
|
|
60
|
+
|
|
61
|
+
Array enum fields no longer need `options: { each: true }`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// OLD
|
|
65
|
+
@UnifiedField({ enum: { enum: X, options: { each: true } }, isArray: true, type: () => X })
|
|
66
|
+
|
|
67
|
+
// NEW
|
|
68
|
+
@UnifiedField({ enum: X, isArray: true, type: () => X })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### New: Expanded Mail Transports
|
|
72
|
+
|
|
73
|
+
`email.smtp` now accepts all nodemailer transports. `{ jsonTransport: true }`
|
|
74
|
+
is useful for CI/e2e tests (no SMTP server needed). A runtime guard prevents
|
|
75
|
+
accidental use in production/staging.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Migration Steps
|
|
80
|
+
|
|
81
|
+
### Step 1: Set Up `registerEnums()` (1 minute)
|
|
82
|
+
|
|
83
|
+
This enables auto-detection of enum names from barrel exports. After this
|
|
84
|
+
step, `enumName` is no longer needed.
|
|
85
|
+
|
|
86
|
+
**Prerequisite:** Your project has a barrel file that re-exports all enums
|
|
87
|
+
(e.g. `src/server/common/enums/index.ts`). If not, create one.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// src/server/common/enums/index.ts
|
|
91
|
+
export { ContactStatusEnum } from './contact-status.enum';
|
|
92
|
+
export { IndustryEnum } from './industry.enum';
|
|
93
|
+
// ... all your enums
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then add one line to your module setup:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// src/server/server.module.ts (or main.ts, before NestFactory.create)
|
|
100
|
+
import * as Enums from './common/enums';
|
|
101
|
+
import { registerEnums } from '@lenne.tech/nest-server';
|
|
102
|
+
|
|
103
|
+
registerEnums(Enums);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**How it works:** `import * as Enums` creates an object where the keys are
|
|
107
|
+
the export names (`{ ContactStatusEnum: {...}, IndustryEnum: {...} }`).
|
|
108
|
+
`registerEnums()` iterates the entries, filters out non-enums, and registers
|
|
109
|
+
each enum for both Swagger and GraphQL under its export name.
|
|
110
|
+
|
|
111
|
+
**Verify:** Start your server — enum names should appear correctly in
|
|
112
|
+
Swagger UI and GraphQL schema. If you see missing enum names, check that
|
|
113
|
+
the enum is exported from the barrel file.
|
|
114
|
+
|
|
115
|
+
### Step 2: Transform Enum Declarations (find-and-replace)
|
|
116
|
+
|
|
117
|
+
Use these regex patterns to transform all `@UnifiedField` enum usages.
|
|
118
|
+
Run them in order.
|
|
119
|
+
|
|
120
|
+
**Pattern A — Remove `options: { each: true }` from array enums:**
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Search: enum: \{ enum: (\w+),\s*enumName: '[^']+',\s*options: \{ each: true \}\s*\}
|
|
124
|
+
Replace: enum: $1
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Pattern B — Long form with enumName → shortcut:**
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Search: enum: \{ enum: (\w+),\s*enumName: '[^']+'\s*\}
|
|
131
|
+
Replace: enum: $1
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Pattern C — Long form without enumName → shortcut:**
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Search: enum: \{ enum: (\w+)\s*\}
|
|
138
|
+
Replace: enum: $1
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**After each pattern:** Build and run tests to verify nothing broke:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pnpm run build && pnpm test
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Step 3: Verify (1 minute)
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Check no long-form patterns remain
|
|
151
|
+
grep -rn "enum: { enum:" src/ --include="*.ts"
|
|
152
|
+
|
|
153
|
+
# Start server — should have zero deprecation warnings
|
|
154
|
+
pnpm start
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Cheat Sheet
|
|
160
|
+
|
|
161
|
+
| Before (deprecated) | After |
|
|
162
|
+
|---------------------|-------|
|
|
163
|
+
| `enum: { enum: X }` | `enum: X` |
|
|
164
|
+
| `enum: { enum: X, enumName: 'X' }` | `enum: X` |
|
|
165
|
+
| `enum: { enum: X, options: { each: true } }, isArray: true` | `enum: X, isArray: true` |
|
|
166
|
+
| `enum: { enum: X, enumName: 'X', options: { each: true } }, isArray: true` | `enum: X, isArray: true` |
|
|
167
|
+
|
|
168
|
+
For cases where you need explicit control:
|
|
169
|
+
|
|
170
|
+
| Need | Solution |
|
|
171
|
+
|------|----------|
|
|
172
|
+
| Custom enum name in schema | `enum: X, enumName: 'CustomName'` |
|
|
173
|
+
| Disable auto-detection | `enum: X, enumName: null` |
|
|
174
|
+
| Skip all validation | `enum: X, isAny: true` |
|
|
175
|
+
| Custom validator | `enum: X, validator: (opts) => [...]` |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Compatibility Notes
|
|
180
|
+
|
|
181
|
+
### Deprecation Warnings
|
|
182
|
+
|
|
183
|
+
After updating, every `enum: { enum: ... }` usage emits a `console.warn`
|
|
184
|
+
at class definition time (server startup). This is intentional — it helps
|
|
185
|
+
you find all places that need migration. The warnings disappear once you
|
|
186
|
+
complete Steps 1–3.
|
|
187
|
+
|
|
188
|
+
If you need to update the package but cannot migrate immediately, the
|
|
189
|
+
warnings are purely informational — no behavior changes.
|
|
190
|
+
|
|
191
|
+
### Existing `options: { each: true }`
|
|
192
|
+
|
|
193
|
+
Explicit `options: { each: true }` continues to work. The auto-propagation
|
|
194
|
+
from `isArray` only applies when `each` is not already set. You can remove
|
|
195
|
+
the explicit option at your own pace.
|
|
196
|
+
|
|
197
|
+
### Custom `isAny` / `validator` Workarounds
|
|
198
|
+
|
|
199
|
+
If you added `isAny: true` or a custom `validator` to work around the
|
|
200
|
+
optional-enum bug, both continue to work. You can safely remove them.
|
|
201
|
+
|
|
202
|
+
### SMTP Configuration
|
|
203
|
+
|
|
204
|
+
All existing `email.smtp` configurations continue to work unchanged. The
|
|
205
|
+
type is a strict superset of the previous type.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Troubleshooting
|
|
210
|
+
|
|
211
|
+
### "JSONTransport is not permitted in production/staging environments"
|
|
212
|
+
|
|
213
|
+
This new runtime guard triggers when `email.smtp` is `{ jsonTransport: true }`
|
|
214
|
+
in `NODE_ENV=production` or `NODE_ENV=staging`. JSONTransport silently
|
|
215
|
+
discards all mail — use a real transport in production.
|
|
216
|
+
|
|
217
|
+
### Enum name missing in Swagger/GraphQL after migration
|
|
218
|
+
|
|
219
|
+
Ensure the enum is exported from your barrel file and `registerEnums()` is
|
|
220
|
+
called before NestJS bootstraps. Check with:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { enumNameRegistry } from '@lenne.tech/nest-server';
|
|
224
|
+
console.log(enumNameRegistry.get(MyEnum)); // Should print the enum name
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Optional enum fields still rejected
|
|
228
|
+
|
|
229
|
+
Verify: (1) `enum:` is set on the field, (2) the field type is the enum
|
|
230
|
+
type, (3) you're running 11.24.3+ (`pnpm list @lenne.tech/nest-server`).
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Module Documentation
|
|
235
|
+
|
|
236
|
+
| Module / File | Documentation |
|
|
237
|
+
|---------------|---------------|
|
|
238
|
+
| `@UnifiedField` decorator | [`src/core/common/decorators/unified-field.decorator.ts`](../src/core/common/decorators/unified-field.decorator.ts) — JSDoc on `UnifiedFieldOptions.enum` and `enumName` |
|
|
239
|
+
| `registerEnum` / `registerEnums` | [`src/core/common/helpers/register-enum.helper.ts`](../src/core/common/helpers/register-enum.helper.ts) — JSDoc with `@example` blocks |
|
|
240
|
+
| `MailTransportOptions` type | [`src/core/common/interfaces/server-options.interface.ts`](../src/core/common/interfaces/server-options.interface.ts) — all nodemailer transport types |
|
|
241
|
+
| `FRAMEWORK-API.md` | [`FRAMEWORK-API.md`](../FRAMEWORK-API.md) — auto-generated API reference (v11.24.3) |
|
|
242
|
+
|
|
243
|
+
### New Public Exports (via `src/index.ts`)
|
|
244
|
+
|
|
245
|
+
- `registerEnums(namespace, options?)` — bulk-register all enums from a barrel-export namespace
|
|
246
|
+
- `graphqlEnumRegistry` — WeakSet tracking GraphQL-registered enums (advanced use only)
|
|
247
|
+
- `MailTransportOptions` — union type of all nodemailer transport configurations
|
|
248
|
+
- `RegisterEnumsOptions` — options interface for `registerEnums()`
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## References
|
|
253
|
+
|
|
254
|
+
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter)
|
|
255
|
+
- [nodemailer transports](https://nodemailer.com/transports/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.24.
|
|
3
|
+
"version": "11.24.3",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "rimraf dist && nest build && pnpm run build:copy-types && pnpm run build:copy-templates && pnpm run build:add-type-references && pnpm run build:framework-api",
|
|
18
|
-
"build:framework-api": "
|
|
18
|
+
"build:framework-api": "tsx scripts/generate-framework-api.ts",
|
|
19
19
|
"build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
|
|
20
20
|
"build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
|
|
21
21
|
"build:add-type-references": "node scripts/add-type-references.js",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"rimraf": "6.1.3",
|
|
153
153
|
"ts-node": "10.9.2",
|
|
154
154
|
"tsconfig-paths": "4.2.0",
|
|
155
|
+
"tsx": "4.21.0",
|
|
155
156
|
"tus-js-client": "4.3.1",
|
|
156
157
|
"typescript": "5.9.3",
|
|
157
158
|
"unplugin-swc": "1.5.9",
|
|
@@ -117,8 +117,47 @@ export interface UnifiedFieldOptions {
|
|
|
117
117
|
* - undefined (default): Normal @UnifiedField behavior
|
|
118
118
|
*/
|
|
119
119
|
exclude?: boolean;
|
|
120
|
-
/**
|
|
121
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Enum for class-validator.
|
|
122
|
+
*
|
|
123
|
+
* **Recommended form:**
|
|
124
|
+
* ```typescript
|
|
125
|
+
* @UnifiedField({ enum: MyEnum })
|
|
126
|
+
* @UnifiedField({ enum: MyEnum, enumName: 'MyEnum' })
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* **Deprecated long form** (still works, emits a deprecation warning):
|
|
130
|
+
* ```typescript
|
|
131
|
+
* @UnifiedField({ enum: { enum: MyEnum, enumName: 'MyEnum' } })
|
|
132
|
+
* ```
|
|
133
|
+
* The long form will be removed in a future MINOR version. Use the shortcut
|
|
134
|
+
* form with the top-level `enumName` property instead.
|
|
135
|
+
*
|
|
136
|
+
* Why not auto-detect from the property type? TypeScript's `emitDecoratorMetadata`
|
|
137
|
+
* reflects enum types as their base primitive in `design:type` — required string
|
|
138
|
+
* enums become `String`, numeric enums become `Number`. Optional enum fields
|
|
139
|
+
* (`status?: MyEnum`) are reflected as `Object` because TS sees the union
|
|
140
|
+
* `MyEnum | undefined`. In all cases the original enum object reference is lost
|
|
141
|
+
* at compile time, so the decorator cannot recover it without an explicit
|
|
142
|
+
* `enum:` option.
|
|
143
|
+
*/
|
|
144
|
+
enum?: EnumAllowedTypes | { enum: EnumAllowedTypes; enumName?: string; options?: ValidationOptions };
|
|
145
|
+
/**
|
|
146
|
+
* Explicit name for the enum in Swagger/GraphQL schema.
|
|
147
|
+
*
|
|
148
|
+
* When set, this name is used in the generated API schema. When omitted,
|
|
149
|
+
* the decorator auto-detects the name via `registerEnum()`, GraphQL metadata,
|
|
150
|
+
* or the enum's runtime name.
|
|
151
|
+
*
|
|
152
|
+
* Set to `null` to explicitly disable auto-detection (swagger receives `undefined`).
|
|
153
|
+
*
|
|
154
|
+
* **Note:** In the deprecated long-form, `enum: { enum: X, enumName: null }` passes
|
|
155
|
+
* `null` directly to swagger (not `undefined`). The top-level `enumName: null` normalizes
|
|
156
|
+
* to `undefined` for consistency. Both forms disable auto-detection effectively.
|
|
157
|
+
*
|
|
158
|
+
* Takes precedence over `enumName` inside a long-form `enum: { ... }` object.
|
|
159
|
+
*/
|
|
160
|
+
enumName?: string | null;
|
|
122
161
|
/** Example value for swagger api documentation */
|
|
123
162
|
example?: any;
|
|
124
163
|
/** Options for graphql */
|
|
@@ -161,10 +200,99 @@ export interface UnifiedFieldOptions {
|
|
|
161
200
|
validator?: (opts: ValidationOptions) => PropertyDecorator[];
|
|
162
201
|
}
|
|
163
202
|
|
|
203
|
+
type NormalizedEnumOptions = { enum: EnumAllowedTypes; enumName?: string; options?: ValidationOptions };
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Detect whether `value` is the long-form `{ enum: MyEnum, ... }` options
|
|
207
|
+
* object. If it is not, we treat `value` itself as the enum (shortcut form).
|
|
208
|
+
*
|
|
209
|
+
* Discrimination strategy (three layers):
|
|
210
|
+
*
|
|
211
|
+
* 1. **Own `enum` key** — a plain TS keyword enum (`enum Foo { ... }`) never
|
|
212
|
+
* has a member named `enum` (reserved keyword). Const-object enums CAN,
|
|
213
|
+
* but it's rare.
|
|
214
|
+
*
|
|
215
|
+
* 2. **Inner value is an object** — in the long form, `value.enum` holds an
|
|
216
|
+
* enum object (typeof 'object'). A const-object enum member named `enum`
|
|
217
|
+
* with a primitive value (string / number) is ruled out here.
|
|
218
|
+
*
|
|
219
|
+
* 3. **Inner value looks like an enum** — all of the inner object's own
|
|
220
|
+
* values must be strings or numbers. This guards against the theoretical
|
|
221
|
+
* case where a const-object enum has `enum: { nested: true }` as a member
|
|
222
|
+
* — that inner object's values are booleans, so it fails this check and
|
|
223
|
+
* the outer object is correctly treated as the shortcut form.
|
|
224
|
+
*/
|
|
225
|
+
function isEnumOptionsObject(value: unknown): value is NormalizedEnumOptions {
|
|
226
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
227
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'enum')) return false;
|
|
228
|
+
const inner = (value as Record<string, unknown>).enum;
|
|
229
|
+
if (typeof inner !== 'object' || inner === null) return false;
|
|
230
|
+
// A valid enum object contains only string or number values (enum members).
|
|
231
|
+
// If the inner object has non-primitive values it cannot be an enum, so the
|
|
232
|
+
// outer object is the enum itself (shortcut form with a member named 'enum').
|
|
233
|
+
const vals = Object.values(inner as Record<string, unknown>);
|
|
234
|
+
return vals.length > 0 && vals.every((v) => typeof v === 'string' || typeof v === 'number');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Detect likely misconfiguration: an object with `enumName` or `options` keys
|
|
239
|
+
* but no `enum` key. This pattern looks like a long-form options object where
|
|
240
|
+
* the required `enum` property was forgotten. Without this guard the object
|
|
241
|
+
* would be silently treated as the enum itself (shortcut form), leading to
|
|
242
|
+
* incorrect validation (e.g. only accepting the string "Status" instead of
|
|
243
|
+
* actual enum members).
|
|
244
|
+
*/
|
|
245
|
+
function warnIfLikelyMisconfiguredEnumOptions(value: unknown, propertyHint: string): void {
|
|
246
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return;
|
|
247
|
+
const obj = value as Record<string, unknown>;
|
|
248
|
+
if (
|
|
249
|
+
!Object.prototype.hasOwnProperty.call(obj, 'enum') &&
|
|
250
|
+
(Object.prototype.hasOwnProperty.call(obj, 'enumName') || Object.prototype.hasOwnProperty.call(obj, 'options'))
|
|
251
|
+
) {
|
|
252
|
+
// eslint-disable-next-line no-console
|
|
253
|
+
console.warn(
|
|
254
|
+
`[@UnifiedField] Probable misconfiguration on "${propertyHint}": the enum option has ` +
|
|
255
|
+
`"enumName" or "options" but no "enum" key. Did you mean { enum: MyEnum, enumName: ... }? ` +
|
|
256
|
+
`The object will be treated as the enum itself (shortcut form), which is likely wrong.`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
164
261
|
export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator {
|
|
262
|
+
// Normalize the shortcut form `enum: MyEnum` into the long form
|
|
263
|
+
// `enum: { enum: MyEnum }` so the rest of the decorator can assume a uniform
|
|
264
|
+
// object shape. Single `isEnumOptionsObject` call, result reused for both variables.
|
|
265
|
+
const isLongForm = opts.enum ? isEnumOptionsObject(opts.enum) : false;
|
|
266
|
+
const normalizedEnum: NormalizedEnumOptions | undefined = opts.enum
|
|
267
|
+
? isLongForm
|
|
268
|
+
? (opts.enum as NormalizedEnumOptions)
|
|
269
|
+
: { enum: opts.enum as EnumAllowedTypes }
|
|
270
|
+
: undefined;
|
|
271
|
+
|
|
272
|
+
// The original long-form object (if provided) — needed to distinguish
|
|
273
|
+
// `enum: { enum: MyEnum, enumName: undefined }` (explicit disable) from
|
|
274
|
+
// `enum: MyEnum` (shortcut: auto-detect).
|
|
275
|
+
const originalEnumOpts = isLongForm ? (opts.enum as NormalizedEnumOptions) : undefined;
|
|
276
|
+
|
|
165
277
|
return (target: any, propertyKey: string | symbol) => {
|
|
166
278
|
const key = String(propertyKey);
|
|
167
279
|
|
|
280
|
+
// Warn if the enum option looks like a long-form object with a missing `enum` key
|
|
281
|
+
if (opts.enum && !originalEnumOpts) {
|
|
282
|
+
warnIfLikelyMisconfiguredEnumOptions(opts.enum, `${target.constructor?.name || '?'}.${key}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Deprecation warning for long-form enum: { enum: MyEnum, ... }
|
|
286
|
+
if (originalEnumOpts) {
|
|
287
|
+
// eslint-disable-next-line no-console
|
|
288
|
+
console.warn(
|
|
289
|
+
`[@UnifiedField] Deprecated long-form enum on "${target.constructor?.name || '?'}.${key}": ` +
|
|
290
|
+
`enum: { enum: ... } is deprecated. Use enum: MyEnum` +
|
|
291
|
+
(originalEnumOpts.enumName ? `, enumName: '${originalEnumOpts.enumName}'` : '') +
|
|
292
|
+
` instead. The long-form will be removed in a future version.`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
168
296
|
if (opts.exclude === true) {
|
|
169
297
|
// ── EXPLICIT EXCLUSION ──
|
|
170
298
|
const excludedKeys: string[] = Reflect.getOwnMetadata(EXCLUDED_FIELD_KEYS, target) || [];
|
|
@@ -282,7 +410,7 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
282
410
|
|
|
283
411
|
// Set type for swagger
|
|
284
412
|
if (baseType) {
|
|
285
|
-
if (
|
|
413
|
+
if (normalizedEnum) {
|
|
286
414
|
swaggerOpts.type = () => String;
|
|
287
415
|
} else {
|
|
288
416
|
swaggerOpts.type = baseType;
|
|
@@ -300,28 +428,34 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
300
428
|
}
|
|
301
429
|
|
|
302
430
|
// Set enum options
|
|
303
|
-
if (
|
|
304
|
-
swaggerOpts.enum =
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
// -
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
431
|
+
if (normalizedEnum && normalizedEnum.enum) {
|
|
432
|
+
swaggerOpts.enum = normalizedEnum.enum;
|
|
433
|
+
|
|
434
|
+
// Resolve enumName with priority chain:
|
|
435
|
+
// 1. Top-level opts.enumName (new recommended form) — highest priority
|
|
436
|
+
// - string → use it
|
|
437
|
+
// - null → explicitly disable auto-detection
|
|
438
|
+
// - undefined → fall through to next level
|
|
439
|
+
// 2. Long-form originalEnumOpts.enumName (deprecated) — backwards compat
|
|
440
|
+
// - present (even if undefined/null) → use it (preserves old semantics)
|
|
441
|
+
// - absent → fall through
|
|
442
|
+
// 3. Auto-detection via registerEnum / GraphQL metadata / runtime name
|
|
443
|
+
if ('enumName' in opts && opts.enumName !== undefined) {
|
|
444
|
+
// Top-level enumName: string → use, null → disable auto-detection
|
|
445
|
+
swaggerOpts.enumName = opts.enumName ?? undefined;
|
|
446
|
+
} else if (originalEnumOpts && 'enumName' in originalEnumOpts) {
|
|
447
|
+
// Deprecated long-form enumName
|
|
448
|
+
swaggerOpts.enumName = originalEnumOpts.enumName;
|
|
449
|
+
} else {
|
|
450
|
+
// Auto-detection
|
|
451
|
+
const autoDetectedName = getEnumName(normalizedEnum.enum);
|
|
313
452
|
if (autoDetectedName) {
|
|
314
453
|
swaggerOpts.enumName = autoDetectedName;
|
|
315
454
|
}
|
|
316
|
-
} else {
|
|
317
|
-
// Property exists (even if undefined/null), use its value
|
|
318
|
-
swaggerOpts.enumName = opts.enum.enumName;
|
|
319
455
|
}
|
|
320
|
-
|
|
321
|
-
IsEnum(opts.enum.enum, opts.enum.options)(target, propertyKey);
|
|
322
456
|
}
|
|
323
457
|
|
|
324
|
-
// Array handling
|
|
458
|
+
// Array handling — must run BEFORE IsEnum so `each` is resolved
|
|
325
459
|
if (isArrayField) {
|
|
326
460
|
swaggerOpts.isArray = true;
|
|
327
461
|
IsArray(valOpts)(target, propertyKey);
|
|
@@ -331,16 +465,27 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
331
465
|
valOpts.each = false;
|
|
332
466
|
}
|
|
333
467
|
|
|
468
|
+
// Apply IsEnum after array handling so `each` from isArray is available.
|
|
469
|
+
// Merge isArray's `each` into the enum options automatically — the user
|
|
470
|
+
// no longer needs to pass `options: { each: true }` redundantly.
|
|
471
|
+
if (normalizedEnum && normalizedEnum.enum && !opts.isAny && !opts.validator) {
|
|
472
|
+
const enumValOpts: ValidationOptions = { ...normalizedEnum.options };
|
|
473
|
+
if (isArrayField && enumValOpts.each === undefined) {
|
|
474
|
+
enumValOpts.each = true;
|
|
475
|
+
}
|
|
476
|
+
IsEnum(normalizedEnum.enum, enumValOpts)(target, propertyKey);
|
|
477
|
+
}
|
|
478
|
+
|
|
334
479
|
// Type function for gql
|
|
335
480
|
// We need to keep the factory pattern (calling resolvedTypeFn inside the arrow function)
|
|
336
481
|
// to support circular references. But we also need to extract array item types to avoid double-nesting.
|
|
337
482
|
const gqlTypeFn = isArrayField
|
|
338
483
|
? () => {
|
|
339
|
-
const resolved =
|
|
484
|
+
const resolved = normalizedEnum?.enum || opts.gqlType || resolvedTypeFn();
|
|
340
485
|
// Extract item type if user provided [ItemType] syntax to avoid [[ItemType]]
|
|
341
486
|
return [Array.isArray(resolved) && resolved.length === 1 ? resolved[0] : resolved];
|
|
342
487
|
}
|
|
343
|
-
: () =>
|
|
488
|
+
: () => normalizedEnum?.enum || opts.gqlType || resolvedTypeFn();
|
|
344
489
|
|
|
345
490
|
// Gql decorator
|
|
346
491
|
Field(gqlTypeFn, gqlOpts)(target, propertyKey);
|
|
@@ -356,7 +501,12 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
356
501
|
// Completely skip validation if its any
|
|
357
502
|
if (opts.validator) {
|
|
358
503
|
opts.validator(valOpts).forEach((d) => d(target, propertyKey));
|
|
359
|
-
} else if (!opts.isAny) {
|
|
504
|
+
} else if (!opts.isAny && !normalizedEnum) {
|
|
505
|
+
// Skip the built-in validator when an enum is set: IsEnum has already been
|
|
506
|
+
// applied above and is the authoritative validator for enum values. Without this
|
|
507
|
+
// guard, fields declared as `foo?: MyEnum` emit `design:type = Object` (because
|
|
508
|
+
// `MyEnum | undefined` is not primitive), which maps to IsObject() here and
|
|
509
|
+
// rejects perfectly valid enum string values with "foo must be an object".
|
|
360
510
|
const validator = getBuiltInValidator(baseType, valOpts, isArrayField, target);
|
|
361
511
|
if (validator) {
|
|
362
512
|
validator(target, propertyKey);
|
|
@@ -370,7 +520,7 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
370
520
|
Type(() => Date)(target, propertyKey);
|
|
371
521
|
}
|
|
372
522
|
// Check if it's a primitive, if not apply transform
|
|
373
|
-
else if (!isPrimitive(baseType) && !
|
|
523
|
+
else if (!isPrimitive(baseType) && !normalizedEnum && !isGraphQLScalar(baseType)) {
|
|
374
524
|
Type(() => baseType)(target, propertyKey);
|
|
375
525
|
ValidateNested({ each: isArrayField })(target, propertyKey);
|
|
376
526
|
|
|
@@ -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:
|
|
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:
|
|
61
|
+
enum: ComparisonOperatorEnum,
|
|
62
62
|
roles: RoleEnum.S_EVERYONE,
|
|
63
63
|
})
|
|
64
64
|
operator: ComparisonOperatorEnum = undefined;
|