Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FRAMEWORK-API.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @lenne.tech/nest-server — Framework API Reference

> Auto-generated from source code on 2026-06-03 (v11.26.2)
> Auto-generated from source code on 2026-06-03 (v11.26.3)
> File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code

## CoreModule.forRoot()
Expand Down
186 changes: 186 additions & 0 deletions migration-guides/11.26.2-to-11.26.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Migration Guide: 11.26.2 → 11.26.3

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | None |
| **Bugfixes** | `@UnifiedField({ enum: … })` no longer emits a broken, unnamed `$ref` in the generated OpenAPI document under `@nestjs/swagger >= 11.4` — enum fields now produce proper named component schemas (or clean inline enums when `enumName: null` / auto-detection fails) instead of `allOf: [{ $ref: '#/components/schemas/' }]`, which crashed OpenAPI client generators like `@hey-api/openapi-ts` |
| **New Features** | None |
| **Migration Effort** | 0 minutes (automatic) — drop-in patch release |

---

## Quick Migration

No code changes required. The fix applies automatically as soon as the package is updated.

```bash
# Update package
pnpm add @lenne.tech/nest-server@11.26.3

# Verify build
pnpm run build

# Run tests
pnpm test

# (Optional) regenerate REST client SDK against the new OpenAPI document
pnpm --filter @your-app/api-sdk openapi-ts
```

---

## What's Fixed in 11.26.3

### OpenAPI: broken empty `$ref` on enum-typed `@UnifiedField` properties

**Affects:** Any project that

- consumes `@lenne.tech/nest-server` together with `@nestjs/swagger >= 11.4` (this repo pinned to `11.4.2`), AND
- exposes REST endpoints whose DTOs declare enum fields via `@UnifiedField({ enum: … })`, AND
- runs an OpenAPI client generator (e.g. `@hey-api/openapi-ts`, `openapi-typescript`, `openapi-generator-cli`) against the bundled OpenAPI document.

**Symptom (before 11.26.3):**

The decorator passed `type: () => String` to `@nestjs/swagger` ALONGSIDE `enum` + `enumName`. `@nestjs/swagger <= 11.2` silently tolerated the combination, but `@nestjs/swagger >= 11.4` emits a broken, **unnamed** enum reference and never registers the enum under `components.schemas`:

```jsonc
// Generated OpenAPI document — BROKEN
{
"components": {
"schemas": {
"SomeInput": {
"properties": {
"status": {
"allOf": [
{ "$ref": "#/components/schemas/" } // ← empty target!
]
}
}
}
// ← StatusEnum is missing entirely from components.schemas
}
}
}
```

Downstream tools crash:

```
@hey-api/openapi-ts: Missing $ref pointer "#/components/schemas/". Token "" does not exist.
```

**Fix (in 11.26.3):**

`@UnifiedField` no longer sets `swaggerOpts.type` when the field is an enum. `@nestjs/swagger` derives the schema from `enum` + `enumName` correctly:

```jsonc
// Generated OpenAPI document — CORRECT
{
"components": {
"schemas": {
"StatusEnum": { "type": "string", "enum": ["draft", "published", "review"] },
"SomeInput": {
"properties": {
"status": {
"allOf": [
{ "$ref": "#/components/schemas/StatusEnum" } // ← named, resolvable
]
}
}
}
}
}
}
```

**Behaviour matrix:**

| `@UnifiedField` form | OpenAPI output before 11.26.3 | OpenAPI output in 11.26.3 |
|---|---|---|
| `{ enum: MyEnum, enumName: 'MyEnum' }` | Empty `$ref`, `MyEnum` missing from `components.schemas` | Named `MyEnum` schema, property uses `$ref` |
| `{ enum: MyEnum }` + `registerEnum(MyEnum, { name: 'MyEnum' })` | Empty `$ref` | Named `MyEnum` schema, property uses `$ref` |
| `{ enum: MyEnum, enumName: null }` (opt out) | Empty `$ref` | Inline `enum: [...]`, no `$ref`, no named schema |
| `{ enum: MyEnum }` without registration | Empty `$ref` | Inline `enum: [...]`, no `$ref`, no named schema |
| Long-form `{ enum: { enum: MyEnum, enumName: 'MyEnum' } }` (deprecated) | Empty `$ref` | Named `MyEnum` schema, property uses `$ref` (deprecation warning unchanged) |
| Non-enum fields (`String`, `Number`, `Date`, custom classes, …) | Unchanged | Unchanged |

GraphQL schema, class-validator runtime validation (`IsEnum`), Mongoose `@Prop`, and field-level `@Restricted` / `@Roles` behaviour are all bit-for-bit identical to 11.26.2.

---

## Compatibility Notes

- **`@nestjs/swagger <= 11.2` consumers:** The previously emitted `type: () => String` was redundant; removing it produces the same enum schema as before. No observable change.
- **`@nestjs/swagger >= 11.4` consumers:** The OpenAPI document for enum fields changes from a **broken** empty `$ref` to a **correct** named schema (or clean inline enum). This is strictly a defect fix — any client generator that was previously crashing now succeeds.
- **OpenAPI client generators / SDK consumers:** After updating, regenerate the SDK once. Enum properties that were previously typed `string` (when the generator silently dropped the broken `$ref`) will now be typed as the proper enum union — review the generated SDK once and adjust call-sites if you relied on the loose `string` type.
- **GraphQL consumers:** No change. The `Field(...)` factory and enum resolution are untouched.
- **`@UnifiedField` public API:** Unchanged. All option shapes (`enum: MyEnum`, `enumName`, deprecated long-form `{ enum: { … } }`, `enumName: null`) keep their documented semantics.
- **Mongoose / class-validator:** Unchanged. `@Prop({ type: baseType })` still applied for enum fields; `IsEnum(...)` is still the authoritative validator.
- **Vendor-mode consumers:** Same fix lands in `src/core/common/decorators/unified-field.decorator.ts`. Sync via `/lt-dev:backend:update-nest-server-core`. No flatten-fix change required.
- **Hidden / excluded enum fields (`@UnifiedField({ exclude: true })`):** Unaffected — those still hide from the OpenAPI document via `ApiHideProperty()`.

---

## Verifying the Fix

If you previously hit the empty-`$ref` defect, confirm the regenerated OpenAPI document:

```bash
# 1. Boot the API and dump the OpenAPI document
pnpm start &
curl -s http://localhost:3000/api-json > /tmp/openapi.json

# 2. There must be no empty/unnamed component refs
grep -F '"$ref": "#/components/schemas/"' /tmp/openapi.json && echo 'BROKEN' || echo 'OK'

# 3. Every enum used in a DTO must appear in components.schemas
jq '.components.schemas | keys' /tmp/openapi.json
```

A complete regression test ships in `tests/unified-field-enum-swagger.e2e-spec.ts` and inspects the real document built by `SwaggerModule.createDocument()` for:

- no empty `$ref` anywhere in the document,
- a named component schema per enum (string / numeric / array / auto-detected / deprecated long-form),
- correct enum values and property references,
- inline-enum fallback for `enumName: null` and for unregistered enums (no empty `$ref`).

---

## Troubleshooting

### After updating, my generated SDK still has an empty `$ref` error

Make sure the SDK is regenerated against a **freshly rebuilt** API. Stale `openapi.json` artefacts checked into the consumer repo continue to be broken. Rebuild the API (`pnpm run build`) and re-export the document (`/api-json` or `SwaggerModule.createDocument` snapshot) before running your codegen.

### My enum properties used to be typed `string` in the generated client and now they're a strict union

That is the corrected behaviour — the previous client was generated against a broken document and silently widened the type. Update call-sites to use the enum union (or import the enum from your shared package). If you need the loose `string` type during the rollout, your codegen typically offers an `--enum-style` flag (e.g. `@hey-api/openapi-ts` → `enums: 'javascript'`) to keep the old shape.

### I rely on the deprecated long-form `enum: { enum: MyEnum, enumName: 'MyEnum' }`

It still works and now produces the same named schema as the shortcut form. The deprecation warning emitted at decoration time is unchanged. Plan to migrate to the shortcut form (`enum: MyEnum, enumName: 'MyEnum'`) before a future MINOR removes the long form.

---

## Module Documentation

### Core Common — `@UnifiedField`

- **Decorator:** `src/core/common/decorators/unified-field.decorator.ts`
- **Architecture notes:** [.claude/rules/architecture.md](../.claude/rules/architecture.md) (Input Validation section)
- **Reference tests:**
- `tests/unified-field-enum-swagger.e2e-spec.ts` — OpenAPI schema regression guard (the contract this release restores)
- `tests/unified-field-enum.e2e-spec.ts` — metadata-level enum behaviour
- `tests/unified-field-enum-api.e2e-spec.ts` — runtime REST/GraphQL enum behaviour

---

## References

- [Migration Guide 11.26.1 → 11.26.2](./11.26.1-to-11.26.2.md) — Previous release (`COOKIE_PREFIX` env, cross-layer cookie-prefix lockstep)
- [Architecture rules — Input Validation](../.claude/rules/architecture.md)
- [@nestjs/swagger 11.4 release notes](https://github.com/nestjs/swagger/releases) — context for the schema-emission change that surfaced the latent defect
- [@hey-api/openapi-ts](https://heyapi.dev/) — one of the OpenAPI client generators that was crashing on the broken document
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lenne.tech/nest-server",
"version": "11.26.2",
"version": "11.26.3",
"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).",
"keywords": [
"node",
Expand Down
2 changes: 1 addition & 1 deletion spectaql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ servers:
info:
title: lT Nest Server
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).
version: 11.26.2
version: 11.26.3
contact:
name: lenne.Tech GmbH
url: https://lenne.tech
Expand Down
20 changes: 13 additions & 7 deletions src/core/common/decorators/unified-field.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,19 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
swaggerOpts.required = true;
}

// Set type for swagger
if (baseType) {
if (normalizedEnum) {
swaggerOpts.type = () => String;
} else {
swaggerOpts.type = baseType;
}
// Set type for swagger.
//
// For enum fields we deliberately do NOT set `type`: @nestjs/swagger derives
// the schema from `enum` + `enumName` (set further below). Passing
// `type: () => String` ALONGSIDE `enum`/`enumName` makes @nestjs/swagger
// >= 11.4 emit a broken, UNNAMED enum reference
// (`allOf: [{ $ref: '#/components/schemas/' }]`) and never adds the enum to
// `components.schemas`. That crashes OpenAPI client generators — e.g.
// @hey-api/openapi-ts fails with «Missing $ref pointer "#/components/schemas/"».
// (On @nestjs/swagger <= 11.2 the extra `type` was tolerated, which is why
// this only surfaced after a swagger bump.)
if (baseType && !normalizedEnum) {
swaggerOpts.type = baseType;
}

// Set description
Expand Down
Loading
Loading