# Migration Guide: 11.15.x → 11.16.0

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | `IServerOptions.security.mapAndValidatePipe` type changed from `boolean` to `boolean \| { nonWhitelistedFields?: 'strip' \| 'error' \| false }` (TypeScript only — runtime is backward compatible). Input properties without `@UnifiedField` are now **silently stripped** by default. |
| **New Features** | `@UnifiedField({ exclude })` option, automatic input property whitelisting, new error code `LTNS_0303` |
| **Bugfixes** | None |
| **Migration Effort** | Low (~5-15 minutes) — update package, migrate any `@Field`-decorated input properties to `@UnifiedField`, verify build |

---

## Quick Migration

```bash
# Update package
npm install @lenne.tech/nest-server@11.16.1

# Verify build
npm run build

# Run tests
npm test
```

> **Important:** If your input classes mix `@Field`/`@IsOptional` with `@UnifiedField` (from parent classes), those `@Field`-only properties will now be silently stripped from REST requests. See [Migration Steps](#detailed-migration-steps) below.

---

## What's New in 11.16.0

### 1. Automatic Input Property Whitelisting

The `MapAndValidatePipe` now automatically handles input properties that are **not** decorated with `@UnifiedField`. This closes a security gap where properties without `@UnifiedField` (e.g. `type`, `id`, `roles`) were silently accepted in REST request bodies.

**How it works:**

1. When a request arrives, the pipe checks which properties are decorated with `@UnifiedField` on the input class (including inherited fields).
2. Any property **not** in the whitelist is handled according to the configured mode.
3. The check applies recursively to nested objects and arrays.

**Three modes available:**

| Mode | Behavior | Use Case |
|------|----------|----------|
| `'strip'` (default) | Silently removes non-whitelisted properties | Production — tolerant to extra fields |
| `'error'` | Throws `BadRequestException` (`LTNS_0303`) | Development — strict validation, helps find issues |
| `false` | Disabled — all properties pass through | Legacy behavior, backward compatible |

**Configuration:**

```typescript
// config.env.ts

// Default behavior (strip) — no config change needed
security: {
  mapAndValidatePipe: true,
}

// Explicit strip (same as default)
security: {
  mapAndValidatePipe: {
    nonWhitelistedFields: 'strip',
  },
}

// Error mode — throws BadRequestException for non-whitelisted properties
security: {
  mapAndValidatePipe: {
    nonWhitelistedFields: 'error',
  },
}

// Disable whitelist check entirely (legacy behavior)
security: {
  mapAndValidatePipe: {
    nonWhitelistedFields: false,
  },
}
```

**Scope of the check:**
- Only applies to classes with **at least one** `@UnifiedField` decorator (directly or inherited)
- Classes without any `@UnifiedField` are skipped entirely (e.g. pure `class-validator` classes)
- Recursion into nested objects uses `nestedTypeRegistry` (populated by `@UnifiedField({ type: () => NestedType })`)
- Nested objects without a registry entry are not recursed into (only top-level keys are checked)

**What is excluded from the check:**
- **Custom decorator parameters** (`@CurrentUser()`, `@RESTServiceOptions()`, `@GraphQLServiceOptions()`, etc.) are always passed through unchanged — they inject framework values, not user input, and must never be mutated by the pipe
- **Basic types** (`String`, `Number`, `Boolean`, `Array`, `Object`, `Buffer`, `ArrayBuffer`) cause an early return — no validation and no whitelist check (e.g. `@Param('id') id: string`)

### 2. `@UnifiedField({ exclude })` Option

New option to explicitly control whether a property is included in the input whitelist. This enables inheritance-safe field exclusion.

```typescript
// Exclude a property — rejected/stripped from input, hidden from GraphQL & Swagger
@UnifiedField({ exclude: true })
override type?: TypeEnum;

// Explicitly re-enable a property excluded by a parent class
@UnifiedField({ exclude: false, description: 'Type override', isOptional: true })
override type?: TypeEnum;
```

**When `exclude: true` is set:**
- Property is registered in `EXCLUDED_FIELD_KEYS` metadata
- `@HideField()` (GraphQL) and `@ApiHideProperty()` (Swagger) are applied automatically
- No other decorators (`@Field`, `@ApiProperty`, validators) are applied
- Property is removed from the whitelist → stripped/rejected at runtime

**When `exclude: false` is set:**
- Property is registered in `FORCE_INCLUDED_FIELD_KEYS` metadata
- Can override a parent's `exclude: true` (explicit re-enable)
- Normal `@UnifiedField` decorators are applied (`@Field`, `@ApiProperty`, validators)

### 3. Inheritance Priority Model

The whitelist system uses a 3-level priority model when resolving fields across class hierarchies:

| Priority | Setting | Behavior |
|----------|---------|----------|
| 1 (highest) | `exclude: true` / `exclude: false` | Explicit — resolved by nearest class (child wins) |
| 2 | `@UnifiedField()` (no `exclude`) | Implicit inclusion — **cannot** override explicit `exclude: true` |
| 3 (lowest) | No decorator | Not in whitelist |

**Key rule:** An implicit `@UnifiedField()` in a child class **cannot** override a parent's `exclude: true`. Only `@UnifiedField({ exclude: false })` can re-enable an excluded field.

#### Full Inheritance Table

| Parent Decorator | Child Decorator | Field in Parent Input | Field in Child Input |
|-----------------|-----------------|----------------------|---------------------|
| `@UnifiedField()` | _(inherited, no override)_ | ACCEPTED | ACCEPTED |
| `@UnifiedField()` | `@UnifiedField()` | ACCEPTED | ACCEPTED |
| `@UnifiedField()` | `@UnifiedField({ exclude: true })` | ACCEPTED | **REJECTED** |
| `@UnifiedField()` | `@UnifiedField({ exclude: false })` | ACCEPTED | ACCEPTED |
| `@UnifiedField({ exclude: true })` | _(inherited, no override)_ | REJECTED | REJECTED |
| `@UnifiedField({ exclude: true })` | `@UnifiedField()` _(implicit)_ | REJECTED | **REJECTED** |
| `@UnifiedField({ exclude: true })` | `@UnifiedField({ exclude: true })` | REJECTED | REJECTED |
| `@UnifiedField({ exclude: true })` | `@UnifiedField({ exclude: false })` | REJECTED | **ACCEPTED** |
| `@UnifiedField({ exclude: false })` | _(inherited, no override)_ | ACCEPTED | ACCEPTED |
| `@UnifiedField({ exclude: false })` | `@UnifiedField({ exclude: true })` | ACCEPTED | **REJECTED** |
| _(no decorator)_ | `@UnifiedField()` | — | ACCEPTED |
| _(no decorator)_ | `@UnifiedField({ exclude: true })` | — | REJECTED |
| _(no decorator)_ | `@UnifiedField({ exclude: false })` | — | ACCEPTED |

> **Reading the table:** "Parent Input" = behavior when the parent class is used as input type. "Child Input" = behavior when the child class is used as input type.

#### Practical Example

```typescript
// CoreUserInput (from nest-server)
class CoreUserInput extends CoreInput {
  @UnifiedField({ description: 'Email', isOptional: true })
  email?: string;          // ← ACCEPTED everywhere

  @UnifiedField({ description: 'First name', isOptional: true })
  firstName?: string;      // ← ACCEPTED everywhere
}

// UserInput (your project) — exclude 'type' from REST input
class UserInput extends CoreUserInput {
  @UnifiedField({ exclude: true })
  override type?: TypeEnum; // ← REJECTED: stripped/error in REST

  @UnifiedField({ description: 'Job Title', isOptional: true })
  jobTitle?: string;        // ← ACCEPTED: new field in child
}

// AdminUserInput (special override) — re-enable 'type' for admins
class AdminUserInput extends UserInput {
  @UnifiedField({ exclude: false, description: 'Type', isOptional: true })
  override type?: TypeEnum; // ← ACCEPTED: explicit re-enable
}
```

### 4. New Error Code: LTNS_0303

Available when using `nonWhitelistedFields: 'error'` mode:

| Code | Message | Translation (DE) | Translation (EN) |
|------|---------|-----------------|-----------------|
| `LTNS_0303` | Non-whitelisted properties found | Die folgenden Eigenschaften sind nicht erlaubt: {{properties}}. Nur mit @UnifiedField dekorierte Eigenschaften werden akzeptiert. | The following properties are not allowed: {{properties}}. Only properties decorated with @UnifiedField are accepted. |

Error response format:
```json
{
  "statusCode": 400,
  "message": "#LTNS_0303: Non-whitelisted properties found [evilProp, nested.badField]"
}
```

### 5. Nested Object & Array Support

The whitelist check recurses into nested objects and arrays:

```typescript
class AddressInput {
  @UnifiedField({ description: 'Street', isOptional: true })
  street?: string;

  @UnifiedField({ description: 'City', isOptional: true })
  city?: string;
}

class UserInput extends CoreUserInput {
  @UnifiedField({ description: 'Address', isOptional: true, type: () => AddressInput })
  address?: AddressInput;
}

// Request body:
{
  "firstName": "John",
  "address": {
    "street": "Main St",
    "city": "Berlin",
    "malicious": "injected"   // ← stripped (or error: "address.malicious")
  }
}
```

For arrays, each element is checked individually:
```json
{
  "items": [
    { "name": "ok" },
    { "name": "ok", "evil": "hack" }  // ← items[1].evil stripped/rejected
  ]
}
```

---

## Breaking Changes

### Properties Without `@UnifiedField` Are Now Stripped

**This is the most important change.** If your input classes have properties decorated with `@Field` + `@IsOptional` (the old pattern) but inherit from a parent that uses `@UnifiedField`, those properties will be silently stripped from REST requests.

**Before (11.15.x):**
```typescript
// This worked — jobTitle was accepted via REST
@InputType()
export class UserInput extends CoreUserInput {
  @Field(() => String, { nullable: true })
  @IsOptional()
  jobTitle?: string;
}
```

**After (11.16.0):**
```typescript
// jobTitle is now stripped because CoreUserInput uses @UnifiedField
// and jobTitle is not decorated with @UnifiedField

// Fix: migrate to @UnifiedField
@InputType()
export class UserInput extends CoreUserInput {
  @UnifiedField({ description: 'Job Title', isOptional: true })
  jobTitle?: string;
}
```

### TypeScript Interface Change: `mapAndValidatePipe`

The `mapAndValidatePipe` property type in `IServerOptions.security` changed from:

```typescript
// Before (11.15.x)
mapAndValidatePipe?: boolean;
```

To:

```typescript
// After (11.16.0)
mapAndValidatePipe?:
  | boolean
  | {
      nonWhitelistedFields?: 'strip' | 'error' | false;
    };
```

**Impact:** This is a **TypeScript-only** change. All existing `boolean` configurations remain valid. Code that directly accesses `mapAndValidatePipe` as a boolean may need type narrowing.

---

## Detailed Migration Steps

### Step 1: Update Package

```bash
npm install @lenne.tech/nest-server@11.16.1
```

### Step 2: Find Input Properties Using Old `@Field` Pattern

Search for input classes that extend Core* classes but use `@Field` instead of `@UnifiedField`:

```bash
# Find input files using @Field directly
grep -rn "@Field(" src/server/modules/*/inputs/ --include="*.ts"
```

### Step 3: Migrate `@Field` Properties to `@UnifiedField`

For each property found:

**Before:**
```typescript
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
import { Restricted } from '@lenne.tech/nest-server';

@Field(() => String, { description: 'Job Title', nullable: true })
@IsOptional()
@Restricted(RoleEnum.ADMIN)
jobTitle?: string;
```

**After:**
```typescript
import { UnifiedField } from '@lenne.tech/nest-server';

@UnifiedField({
  description: 'Job Title',
  isOptional: true,
  roles: RoleEnum.ADMIN,
})
jobTitle?: string;
```

**Mapping table:**

| Old Pattern | `@UnifiedField` Equivalent |
|------------|---------------------------|
| `@Field(() => String, { nullable: true })` | `isOptional: true` |
| `@Field(() => [String])` | `isArray: true, type: () => String` |
| `@IsOptional()` | `isOptional: true` |
| `@IsEnum(MyEnum)` | `enum: { enum: MyEnum }` |
| `@Restricted(RoleEnum.ADMIN)` | `roles: RoleEnum.ADMIN` |
| `@ValidateNested()` + `@Type(() => X)` | `type: () => X` (automatic) |

### Step 4: Exclude Properties That Should Not Be Set via Input

If you have properties that should never be set via REST (e.g. `type`, `roles`, `id`):

```typescript
@UnifiedField({ exclude: true })
override type?: TypeEnum;
```

### Step 5: Verify Build and Tests

```bash
npm run build
npm test
```

### Step 6: Choose Whitelist Mode (Optional)

For development/staging, consider using `'error'` mode to catch issues early:

```typescript
// config.env.ts (development only)
security: {
  mapAndValidatePipe: {
    nonWhitelistedFields: 'error',
  },
}
```

---

## Compatibility Notes

### Existing Projects Using `mapAndValidatePipe: true`

No configuration change needed. The default `'strip'` mode activates automatically.

### Existing Projects Using `mapAndValidatePipe: false`

The pipe is not registered at all, so no whitelist check runs. No change needed.

### Projects Without `@UnifiedField`

If your input classes do not use `@UnifiedField` at all (neither directly nor inherited), the whitelist check is skipped entirely (`whitelistedKeys.size === 0` → no check). This ensures backward compatibility for pure `class-validator` setups.

### GraphQL vs REST

The whitelist check runs in `MapAndValidatePipe`, which processes both REST and GraphQL inputs. However, GraphQL already enforces its schema strictly — unknown fields cause GraphQL validation errors before the pipe runs. The primary benefit is for **REST endpoints** where JSON bodies accept arbitrary properties.

### `@HideField` and `@ApiHideProperty` with `exclude: true`

When using `@UnifiedField({ exclude: true })`, the decorator automatically applies `@HideField()` (GraphQL) and `@ApiHideProperty()` (Swagger). This means the field is hidden from both the GraphQL schema and the Swagger documentation, in addition to being stripped/rejected at runtime.

---

## Troubleshooting

### Property Suddenly Returning `null` After Update

**Cause:** The property uses `@Field` instead of `@UnifiedField`, and its parent class uses `@UnifiedField`. The property is now being stripped.

**Solution:** Migrate the property to `@UnifiedField`:
```typescript
// Before
@Field(() => String, { nullable: true })
@IsOptional()
jobTitle?: string;

// After
@UnifiedField({ description: 'Job Title', isOptional: true })
jobTitle?: string;
```

### `BadRequestException` with `LTNS_0303` in Production

**Cause:** `nonWhitelistedFields` is set to `'error'` and the client sends extra properties.

**Solution:** Either:
1. Fix the client to only send whitelisted properties
2. Switch to `'strip'` mode (default): `nonWhitelistedFields: 'strip'`

### Child Class Cannot Override Parent's `exclude: true`

**Cause:** Using implicit `@UnifiedField()` (without `exclude: false`) cannot override a parent's `exclude: true`.

**Solution:** Use explicit `@UnifiedField({ exclude: false })`:
```typescript
// This does NOT work — implicit cannot override
@UnifiedField({ description: 'Type', isOptional: true })
override type?: TypeEnum;  // Still rejected!

// This works — explicit re-enable
@UnifiedField({ exclude: false, description: 'Type', isOptional: true })
override type?: TypeEnum;  // Accepted
```

### Nested Object Properties Not Being Checked

**Cause:** The nested type is not registered in `nestedTypeRegistry`. This happens when using `Record<string, any>` or `object` type without specifying `type: () => NestedClass`.

**Solution:** Use explicit type in `@UnifiedField`:
```typescript
// No recursive check (metadata is Record<string, any>)
@UnifiedField({ description: 'Metadata', isOptional: true })
metadata?: Record<string, any>;

// Recursive check enabled
@UnifiedField({ description: 'Address', isOptional: true, type: () => AddressInput })
address?: AddressInput;
```

---

## Module Documentation

### MapAndValidatePipe (Updated)

- **Key File:** [src/core/common/pipes/map-and-validate.pipe.ts](../src/core/common/pipes/map-and-validate.pipe.ts)
- **Changes:**
  - `processNonWhitelistedFields()` — recursive whitelist enforcement
  - `nonWhitelistedFieldsMode` — configurable via `ConfigService`
  - New import of `ErrorCode` for `LTNS_0303` error messages

### UnifiedField Decorator (Updated)

- **Key File:** [src/core/common/decorators/unified-field.decorator.ts](../src/core/common/decorators/unified-field.decorator.ts)
- **Changes:**
  - New symbols: `UNIFIED_FIELD_KEYS`, `EXCLUDED_FIELD_KEYS`, `FORCE_INCLUDED_FIELD_KEYS`
  - New option: `exclude?: boolean`
  - New function: `getUnifiedFieldKeys(metatype)` — resolves whitelist with inheritance
  - `exclude: true` applies `@HideField()` + `@ApiHideProperty()` automatically

### Error Codes (Updated)

- **Key File:** [src/core/modules/error-code/error-codes.ts](../src/core/modules/error-code/error-codes.ts)
- **Documentation:** [docs/error-codes.md](../docs/error-codes.md)
- **Changes:** New error code `LTNS_0303: NON_WHITELISTED_PROPERTIES`

### Unit Tests

- **Key File:** [tests/unit/unified-field-whitelist.spec.ts](../tests/unit/unified-field-whitelist.spec.ts)
- **Coverage:** 49 tests covering all modes, inheritance, nesting, config variants, custom decorator parameters, edge cases

---

## References

- [IServerOptions Interface](../src/core/common/interfaces/server-options.interface.ts) — Updated `mapAndValidatePipe` type
- [UnifiedField Decorator](../src/core/common/decorators/unified-field.decorator.ts) — `exclude` option and metadata tracking
- [MapAndValidatePipe](../src/core/common/pipes/map-and-validate.pipe.ts) — Whitelist enforcement implementation
- [Error Codes](../src/core/modules/error-code/error-codes.ts) — `LTNS_0303` definition
- [Unit Tests](../tests/unit/unified-field-whitelist.spec.ts) — Comprehensive test coverage
- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) — Reference implementation
