diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7387375..a581e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,17 +2,19 @@ name: CI on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: - build: + verify: + name: Node ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 @@ -25,5 +27,15 @@ jobs: run: npm ci - name: Type Check run: npm run type-check + - name: Lint + run: npm run lint + - name: Test + run: npm run test:coverage + - name: Build + run: npm run build + - name: Verify published entry points load + run: | + node --input-type=module -e "import * as m from './dist/esm/index.js'; if (typeof m.toInstance !== 'function') throw new Error('ESM entry point broken');" + node --input-type=commonjs -e "const m = require('./dist/cjs/index.js'); if (typeof m.toInstance !== 'function') throw new Error('CJS entry point broken');" - name: Run Demo run: npm run demo diff --git a/.gitignore b/.gitignore index 9fe6ced..81b0bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Node modules and dependency files +# NOTE: package-lock.json is intentionally committed — CI installs with `npm ci`, +# which requires a lockfile to be present in the repository. /node_modules/ -/package-lock.json # Build outputs /dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19f10e6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,118 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-08-03 + +The first release with a working test suite. Everything below the "Fixed" heading was +found by writing tests against the previous release; the suite has grown from 40 tests +that never executed to 136 that do. + +### Added + +**Field-name mapping.** A library whose headline feature is "JSON mapping" could not map +a name. It can now. + +- `@JsonProperty(name)` renames a property in both directions. +- `@JsonAlias(...names)` accepts extra names on input only, so a field can be renamed + without breaking older clients. +- Naming strategies — `snake_case`, `kebab-case`, `SCREAMING_SNAKE_CASE`, `PascalCase`, + `camelCase`, or your own function — applied to properties with no explicit name. + Acronyms split where a reader expects: `parseHTTPResponse` → `parse_http_response`. + +**Access control.** + +- `@JsonIgnore()` — excluded in both directions. +- `@JsonWriteOnly()` — accepted from input, never echoed back (passwords). +- `@JsonReadOnly()` — serialized, never settable by a client (server-owned ids). + +**Transform options**, per call or globally via `configure()`. + +- `validate: false` maps without validating, for lenient parsing. +- `unknownKeys: 'allow' | 'strip' | 'error'` decides what happens to undeclared keys. +- `namingStrategy` selects the JSON naming convention. + +**Error ergonomics.** Turning the nested `ValidationError` tree into an HTTP 400 body used +to be the caller's problem. + +- `flattenErrors(errors)` → `{ "items[0].qty": ["qty must be at least 1"] }` +- `formatErrors(errors)` → one human-readable line per failure +- `collectErrorMessages(errors)` → just the messages +- `validateOrReject(obj)` throws instead of returning an array you might forget to check + +**30 validation decorators.** `@Equals`, `@NotEquals`, `@IsEmpty`, `@IsEnum`, `@IsInstance`, +`@Length`, `@IsAlpha`, `@IsAlphanumeric`, `@IsNumberString`, `@IsLowercase`, `@IsUppercase`, +`@Contains`, `@NotContains`, `@StartsWith`, `@EndsWith`, `@IsUUID`, `@IsJSON`, +`@IsDateString`, `@IsSemVer`, `@IsHexColor`, `@IsIP`, `@IsDivisibleBy`, `@IsPort`, +`@IsLatitude`, `@IsLongitude`, `@IsBigInt`, `@MinDate`, `@MaxDate`, `@ArrayUnique`, +`@ArrayContains`, `@ArrayNotContains`. + +**Conditional validation.** `@ValidateIf(o => ...)` makes a property's rules depend on the +rest of the object; `@Allow()` declares a property that needs no rules of its own. + +**Correctly typed array entry points.** `toInstanceArray()` and `fromJsonArray()`. +`toInstance`/`fromJson` accept arrays at runtime but type the result as `T`, so callers had +to cast to reach the elements. + +**`@JsonPolymorphic` options.** `{ onUnknown: 'error' }` and `{ fallback: SomeClass }`. + +**`JsonMappingError`** — raised when a value cannot be mapped at all, as distinct from +mapping fine and failing validation. + +### Fixed + +- **Inheritance silently discarded base-class rules.** A subclass re-decorating an inherited + property registered its constraints against its own prototype, and the engine read only the + nearest set. Constraints now merge down the whole prototype chain, base first. The + library's own example was affected: `Media`'s `@IsString() title` had never been enforced + for `Book`. +- **A circular reference exhausted the heap.** `serialize()` recursed forever, taking 8 GB + and the process with it. It now raises a `JsonMappingError` naming the cause. Diamonds + still serialize; `validate()` skips back-edges. +- **`@Matches` with a `g` or `y` flag was stateful.** `RegExp.test` advances `lastIndex`, so + validating the same value twice gave different answers. Those flags are stripped. +- **An unmatched `@JsonPolymorphic` discriminator silently dropped the value.** The + single-object branch fell through without assigning; the property came back `undefined`. + The raw value is now preserved. +- **`@JsonSerialize` serializers ran on `null`/`undefined`**, crashing on any unset optional + property. They now only see real values. +- **`serialize()` crashed on null-prototype objects.** It read `obj.constructor.prototype`; + both engines now agree on `Object.getPrototypeOf`. +- **`__proto__`, `constructor` and `prototype` in untrusted JSON** were copied onto the + instance, detaching it from its own class. They are dropped. +- **Caller-supplied messages were mangled** by the `each element in ...` prefix, producing + sentences like "each element in tags must all be strings". +- **Two rules sharing a name overwrote each other**, so only one failure was ever reported. +- **`fromRequest` leaked a raw `SyntaxError`** for a non-JSON body; it now reports a + `JsonMappingError`. +- **`@ValidateNested({ each: true })`** was documented in the README but did not compile — + `ValidateNested()` accepted no arguments. It now does, and asserts the value is an array. + +### Changed + +- `toPlain`, `toJson`, `toInstance`, `fromJson`, `fromJsonArray`, `toInstanceArray` and + `fromRequest` accept an optional trailing options argument. All defaults preserve the + previous behaviour. +- `src/example.ts` is no longer published in `dist`. It called `runExample()` at import + time — an import side effect in a package declaring `"sideEffects": false`. +- Minimum supported Node is 20. + +### Infrastructure + +- **The test suite had never run.** Vitest 4 transpiles with oxc, which does not read + `experimentalDecorators` from a tsconfig that excludes the files it is transforming, so + every decorator-using suite failed to parse and was reported as "0 test" rather than as an + error. A `vitest.config.ts` enabling legacy decorators brought all 40 existing tests back + to life. +- Test files are now type-checked, which surfaced 17 strict-mode errors. +- CI runs lint, coverage tests, build and ESM/CJS entry-point smoke checks across Node + 20/22/24, and `npm ci` works because `package-lock.json` is committed. +- `npm run build:docs` regenerates the previously hand-maintained `docs/cereale.js`. + +## [0.0.1] + +Initial release: mapping and validation decorators, polymorphic types, custom +serializers/deserializers, and the `toJson` / `fromJson` / `toPlain` / `toInstance` API. diff --git a/README.md b/README.md index 8220fae..339d9a1 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ Cereale is a lightweight TypeScript library that provides Spring-like decorators ## Features -- **Spring-like Decorators:** Familiar `@JsonSerialize`, `@JsonDeserialize`, `@JsonType`, and `@JsonPolymorphic`. +- **Spring-like Decorators:** Familiar `@JsonProperty`, `@JsonSerialize`, `@JsonDeserialize`, `@JsonType`, and `@JsonPolymorphic`. +- **Field-name Mapping:** Map `first_name` to `firstName` per property or with a naming strategy. +- **Access Control:** Keep passwords out of responses and server-owned ids out of requests. - **Custom Serializers/Deserializers:** Easily handle complex types like Dates, BigInts, or custom objects. - **Polymorphism Support:** Native handling of polymorphic types via discriminators. -- **Integrated Validation:** Automatically validates objects during serialization and deserialization. +- **Integrated Validation:** 50+ validation decorators, applied during mapping or on demand. - **Type Safety:** Fully written in TypeScript for excellent developer experience. - **Zero Dependencies:** Extremely lightweight and fast. @@ -17,42 +19,40 @@ Cereale is a lightweight TypeScript library that provides Spring-like decorators npm install cereale ``` -Make sure to enable `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig.json`: +Enable `experimentalDecorators` in your `tsconfig.json`: ```json { "compilerOptions": { "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "target": "ES2025" + "target": "ES2022" } } ``` +Cereale stores its own metadata, so `reflect-metadata` is not required and +`emitDecoratorMetadata` is not read. Requires Node 20 or later. + ## Quick Start ### 1. Define your Models -Use decorators to define how your data should be transformed and validated. - ```typescript -import { - IsString, - IsInt, - Min, - IsDate, +import { + IsString, + IsDate, ValidateNested, - JsonSerialize, - JsonDeserialize, - JsonPolymorphic, - JsonSerializer, - JsonDeserializer + JsonSerialize, + JsonDeserialize, + JsonPolymorphic, + JsonSerializer, + JsonDeserializer } from 'cereale'; // Custom Date Serializer class DateSerializer implements JsonSerializer { serialize(value: Date): string { - return value.toISOString().split('T')[0]; + return value.toISOString().split('T')[0]!; } } @@ -72,7 +72,7 @@ abstract class Media { class Book extends Media { type = 'book'; - + @IsString() author: string; @@ -94,12 +94,13 @@ class Library { } ``` +Constraints accumulate down an inheritance chain: `Book` is checked against `Media`'s +`@IsString() title` as well as its own rules. + ### 2. Map JSON with Validation -Use standalone utility functions to handle the conversion process directly. - ```typescript -import { fromJson, toJson, JsonValidationError } from 'cereale'; +import { fromJson, toJson, JsonValidationError, flattenErrors } from 'cereale'; async function main() { const json = '{"name": "Central Library", "items": [{"type": "book", "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "publishedAt": "1925-04-10"}]}'; @@ -107,15 +108,15 @@ async function main() { try { // Deserialize JSON to Class Instance const library = await fromJson(Library, json); - console.log(library.name); // "Central Library" + console.log(library.name); // "Central Library" console.log(library.items[0] instanceof Book); // true // Serialize Class Instance back to JSON - const outputJson = await toJson(library); - console.log(outputJson); + console.log(await toJson(library)); } catch (error) { if (error instanceof JsonValidationError) { - console.error("Validation failed:", error.errors); + console.error(flattenErrors(error.errors)); + // { "items[0].title": ["title must be a string"] } } } } @@ -135,20 +136,102 @@ app.post('/books', async (c) => { }); ``` +## Field-name Mapping + +JSON rarely uses the same names as your classes. + +```typescript +import { JsonProperty, JsonAlias } from 'cereale'; + +class User { + @JsonProperty('first_name') + firstName: string; // <-> {"first_name": "Ada"} + + @JsonProperty('surname') + @JsonAlias('last_name') // also accepted on input, never emitted + lastName: string; +} +``` + +Or convert every property at once with a naming strategy: + +```typescript +import { configure, toPlain } from 'cereale'; + +// once, for the whole application +configure({ namingStrategy: 'snake_case' }); + +// or per call +await toPlain(user, { namingStrategy: 'snake_case' }); +``` + +Built-in strategies: `identity` (default), `camelCase`, `PascalCase`, `snake_case`, +`SCREAMING_SNAKE_CASE`, `kebab-case`. You can also pass your own +`(propertyKey: string) => string`. An explicit `@JsonProperty` always wins. + +Acronyms split where a reader expects them to: `parseHTTPResponse` becomes +`parse_http_response`, not `parse_h_t_t_p_response`. + +## Access Control + +```typescript +import { JsonIgnore, JsonReadOnly, JsonWriteOnly } from 'cereale'; + +class Account { + @JsonReadOnly() // sent to clients, never settable by them + id: number; + + @IsString() + email: string; + + @JsonWriteOnly() // accepted from clients, never echoed back + @IsString() + password: string; + + @JsonIgnore() // never crosses the boundary in either direction + internalNotes: string; +} +``` + +## Options + +Every mapping function takes an optional trailing options argument, and `configure()` sets +defaults for the whole application. Per-call options win. + +| Option | Values | Default | Meaning | +| --- | --- | --- | --- | +| `validate` | `boolean` | `true` | Validate the result; throw `JsonValidationError` on failure. | +| `namingStrategy` | strategy name or function | `identity` | JSON naming convention for properties without `@JsonProperty`. | +| `unknownKeys` | `allow` \| `strip` \| `error` | `allow` | What to do with incoming keys matching no declared property. | + +```typescript +// lenient parse: build the instance, inspect the damage yourself +const draft = await fromJson(Order, body, { validate: false }); +const problems = flattenErrors(await validate(draft)); + +// strict intake: reject anything you did not declare +const order = await fromJson(Order, body, { unknownKeys: 'error' }); +``` + ## API Reference -### Decorators +### Mapping Decorators -- `@JsonSerialize(serializer: ClassConstructor)`: Specifies a custom serializer for a property. -- `@JsonDeserialize(deserializer: ClassConstructor)`: Specifies a custom deserializer for a property. -- `@JsonType(typeFunction: () => ClassConstructor)`: Explicitly sets the type for nested transformations. -- `@JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor, name: string }[])`: Configures polymorphic transformation based on a discriminator field. +- `@JsonProperty(name: string)`: Renames the property in JSON, both directions. +- `@JsonAlias(...names: string[])`: Extra names accepted on input only. +- `@JsonIgnore()`: Excludes the property from mapping entirely. +- `@JsonReadOnly()`: Serialized, but never populated from incoming JSON. +- `@JsonWriteOnly()`: Populated from incoming JSON, but never serialized. +- `@JsonSerialize(serializer: ClassConstructor)`: Custom serializer for a property. Skipped when the value is `null`/`undefined`. +- `@JsonDeserialize(deserializer: ClassConstructor)`: Custom deserializer for a property. +- `@JsonType(typeFunction: () => ClassConstructor)`: Explicitly sets the type for nested transformations. Applies element-wise to arrays. +- `@JsonPolymorphic(discriminator, subTypes, options?)`: Polymorphic transformation based on a discriminator field. `options` accepts `{ onUnknown: 'keep' | 'error' }` (default `keep`, which preserves the raw value) and `{ fallback: ClassConstructor }`. -#### Validation Decorators +### Validation Decorators Most validation decorators accept an optional `ValidationOptions` object: - `each: boolean`: Apply validation to each element of an array. -- `message: string | ((args: ValidationArguments) => string)`: Custom error message. +- `message: string | ((args: ValidationArguments) => string)`: Custom error message, reported verbatim. | Decorator | Description | | --- | --- | @@ -156,46 +239,88 @@ Most validation decorators accept an optional `ValidationOptions` object: | `@IsNumber()` | Checks if value is a number (and not NaN). | | `@IsInt()` | Checks if value is an integer. | | `@IsBoolean()` | Checks if value is a boolean. | +| `@IsBigInt()` | Checks if value is a bigint. | | `@IsObject()` | Checks if value is an object (not null/array). | | `@IsDate()` | Checks if value is a valid Date object. | | `@IsDefined()` | Checks if value is not null or undefined. | | `@IsOptional()` | Skips other validations if value is null/undefined. | | `@IsNotEmpty()` | Checks if value is not null/undefined/empty string. | -| `@Min(value)` | Checks if number is >= value. | -| `@Max(value)` | Checks if number is <= value. | -| `@Positive()` | Checks if number is > 0. | -| `@Negative()` | Checks if number is < 0. | -| `@MinLength(len)` | Checks if string length is >= len. | -| `@MaxLength(len)` | Checks if string length is <= len. | +| `@IsEmpty()` | Checks if value is null/undefined/`''`/`[]`/`{}`. | +| `@Equals(value)` / `@NotEquals(value)` | Strict equality against a fixed value. | +| `@IsEnum(enumObject)` | Checks membership of a TypeScript enum. | +| `@IsInstance(Class)` | Checks `value instanceof Class`. | +| `@Min(value)` / `@Max(value)` | Numeric bounds. | +| `@Positive()` / `@Negative()` | Checks sign. | +| `@IsDivisibleBy(n)` | Checks `value % n === 0`. | +| `@IsPort()` | Integer in 0–65535, as number or numeric string. | +| `@IsLatitude()` / `@IsLongitude()` | Geographic bounds. | +| `@MinLength(len)` / `@MaxLength(len)` | String length bounds. | +| `@Length(min, max?)` | Both bounds in one rule. | +| `@IsAlpha()` / `@IsAlphanumeric()` | Character-class checks. | +| `@IsLowercase()` / `@IsUppercase()` | Case checks. | +| `@IsNumberString()` | String that parses as a finite number. | +| `@Contains(s)` / `@NotContains(s)` | Substring checks. | +| `@StartsWith(s)` / `@EndsWith(s)` | Affix checks. | | `@Email()` | Checks if string is a valid email. | | `@IsUrl()` | Checks if string is a valid URL. | -| `@Matches(regex)`| Checks if string matches a regular expression. | +| `@IsUUID(version?)` | Checks if string is a valid UUID. | +| `@IsIP(version?)` | Checks if string is a valid IPv4/IPv6 address. | +| `@IsJSON()` | Checks if string parses as JSON. | +| `@IsDateString()` | Checks if string is a parseable date. | +| `@IsSemVer()` | Checks if string is a semantic version. | +| `@IsHexColor()` | Checks `#rgb`, `#rrggbb`, `#rrggbbaa`. | +| `@Matches(regex)` | Checks if string matches a regular expression. | +| `@MinDate(d)` / `@MaxDate(d)` | Date bounds. Accepts `() => Date` for a moving bound. | | `@IsArray()` | Checks if value is an array. | -| `@ArrayNotEmpty()`| Checks if array is not empty. | -| `@ArrayMinSize(n)`| Checks if array has at least n elements. | -| `@ArrayMaxSize(n)`| Checks if array has at most n elements. | -| `@IsIn(values)` | Checks if value is in the allowed list. | -| `@IsNotIn(vals)` | Checks if value is NOT in the list. | -| `@ValidateNested()`| Recursively validates nested objects/arrays. | +| `@ArrayNotEmpty()` | Checks if array is not empty. | +| `@ArrayMinSize(n)` / `@ArrayMaxSize(n)` | Array size bounds. | +| `@ArrayUnique(keyFn?)` | Checks for duplicate elements. | +| `@ArrayContains(vals)` / `@ArrayNotContains(vals)` | Membership checks. | +| `@IsIn(values)` / `@IsNotIn(values)` | Allow/deny lists. | +| `@ValidateNested(options?)` | Recursively validates nested objects/arrays. | +| `@ValidateIf(o => boolean)` | Skips this property's rules when the condition is false. | +| `@Allow()` | Declares a property with no rules of its own. | +| `@Validate(validator, constraints?, options?)` | Applies a custom validator class or function. | + +Write your own with `registerDecorator({ name, target, propertyName, validator })`. ### Utilities -- `toJson(obj: any)`: Validates and serializes an instance to a JSON string (Returns `Promise`). -- `toPlain(obj: any)`: Validates and transforms an instance to a plain object (Returns `Promise`). -- `fromJson(clazz: ClassConstructor, json: string)`: Parses JSON and transforms it to a validated class instance (Returns `Promise`). -- `toInstance(clazz: ClassConstructor, plain: any)`: Transforms a plain object to a validated class instance (Returns `Promise`). -- `fromRequest(clazz: ClassConstructor, request: Request)`: Extracts JSON from a Fetch `Request` and transforms it to a validated instance (Returns `Promise`). -- `validate(obj: any)`: Performs full validation on an object/instance (Returns `Promise`). +- `toJson(obj, options?)`: Validates and serializes an instance to a JSON string (`Promise`). +- `toPlain(obj, options?)`: Validates and transforms an instance to a plain object (`Promise`). +- `fromJson(clazz, json, options?)`: Parses JSON to a validated class instance (`Promise`). +- `fromJsonArray(clazz, json, options?)`: Same, for a JSON array (`Promise`). +- `toInstance(clazz, plain, options?)`: Transforms a plain object to a validated class instance (`Promise`). +- `toInstanceArray(clazz, plain, options?)`: Same, for an array (`Promise`). +- `fromRequest(clazz, request, options?)`: Extracts JSON from a Fetch `Request` (`Promise`). +- `validate(obj)`: Full validation, returning `Promise`. +- `validateOrReject(obj)`: As above, but throws `JsonValidationError`. +- `configure(options)` / `getConfig()` / `resetConfig()`: Library-wide defaults. + +### Error Handling + +`JsonValidationError` carries a nested `ValidationError[]`. Three helpers turn it into +something you can return to a client: + +```typescript +import { flattenErrors, formatErrors, collectErrorMessages } from 'cereale'; + +flattenErrors(errors); // { "items[0].qty": ["qty must be at least 1"] } +formatErrors(errors); // "items[0].qty: qty must be at least 1" +collectErrorMessages(errors); // ["qty must be at least 1"] +``` + +`JsonMappingError` is raised when a value cannot be mapped at all — a body that is not +JSON, a circular reference, an unknown discriminator under `{ onUnknown: 'error' }` — as +distinct from mapping fine and failing validation. ## Framework Integrations -Cereale is designed to be compatible with all trending web frameworks. - ### Hono / Next.js / Cloudflare Workers Use `fromRequest` for seamless integration with the Fetch `Request` API. ### NestJS -You can use Cereale inside your controllers for explicit mapping and validation without needing `reflect-metadata`. +Use Cereale inside your controllers for explicit mapping and validation without needing `reflect-metadata`. ```typescript import { toInstance } from 'cereale'; @@ -208,25 +333,37 @@ async create(@Body() body: any) { ``` ### Express / Fastify -Easily integrate with traditional Node.js frameworks. ```typescript -import { toInstance, toPlain } from 'cereale'; +import { toInstance, toPlain, JsonValidationError, flattenErrors } from 'cereale'; app.post('/user', async (req, res) => { try { const user = await toInstance(User, req.body); res.json(await toPlain(user)); } catch (err) { - res.status(400).json(err); + if (err instanceof JsonValidationError) { + return res.status(400).json({ errors: flattenErrors(err.errors) }); + } + throw err; } }); ``` +## Notes and Limitations + +- **Circular references** are rejected during serialization with a `JsonMappingError`. Break + the cycle with `@JsonIgnore()` on the back-reference. +- **`validate()` on a plain object** returns no errors: rules live on the class, so validate + the instance you get back from `toInstance`, not the raw payload. +- **Renaming is not backwards-compatible by itself.** Once a property carries + `@JsonProperty`, its original name is no longer accepted on input — add `@JsonAlias` to + keep older clients working. + ## Contributing Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project. ## License -Cereale is licensed under the [MIT License](LICENSE). \ No newline at end of file +Cereale is licensed under the [MIT License](LICENSE). diff --git a/docs/cereale.js b/docs/cereale.js index e16a915..13e6de6 100644 --- a/docs/cereale.js +++ b/docs/cereale.js @@ -1 +1,2 @@ -"use strict";var Cereale=(()=>{var A=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var $=(n,t)=>{for(var e in t)A(n,e,{get:t[e],enumerable:!0})},P=(n,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of T(t))!b.call(n,i)&&i!==e&&A(n,i,{get:()=>t[i],enumerable:!(a=O(t,i))||a.enumerable});return n};var S=n=>P(A({},"__esModule",{value:!0}),n);var it={};$(it,{ArrayMaxSize:()=>K,ArrayMinSize:()=>j,ArrayNotEmpty:()=>tt,Email:()=>_,IsArray:()=>X,IsBoolean:()=>R,IsDate:()=>at,IsDefined:()=>Y,IsIn:()=>et,IsInt:()=>k,IsNotEmpty:()=>Z,IsNotIn:()=>nt,IsNumber:()=>D,IsObject:()=>J,IsOptional:()=>z,IsString:()=>L,IsUrl:()=>G,JsonDeserialize:()=>w,JsonMapper:()=>x,JsonPolymorphic:()=>N,JsonSerialize:()=>V,JsonType:()=>C,JsonValidationError:()=>h,METADATA_KEYS:()=>u,Matches:()=>Q,Max:()=>H,MaxLength:()=>F,Min:()=>U,MinLength:()=>B,Negative:()=>q,Positive:()=>W,ValidateNested:()=>st});var M=class n{constructor(){this.properties=new WeakMap;this.propertyMetadata=new WeakMap;this.classMetadata=new WeakMap}static getInstance(){return n.instance||(n.instance=new n),n.instance}defineMetadata(t,e,a,i){if(i){let s=this.propertyMetadata.get(a);s||(s=new Map,this.propertyMetadata.set(a,s));let r=s.get(i);r||(r=new Map,s.set(i,r)),r.set(t,e)}else{let s=this.classMetadata.get(a);s||(s=new Map,this.classMetadata.set(a,s)),s.set(t,e)}}getMetadata(t,e,a){let i=e;for(;i;){let s=this.getOwnMetadata(t,i,a);if(s!==void 0)return s;i=Object.getPrototypeOf(i)}}getOwnMetadata(t,e,a){return a?this.propertyMetadata.get(e)?.get(a)?.get(t):this.classMetadata.get(e)?.get(t)}registerProperty(t,e){let a=this.properties.get(t);a||(a=[],this.properties.set(t,a)),a.includes(e)||a.push(e)}getProperties(t){let e=new Set,a=t;for(;a;){let i=this.properties.get(a);i&&i.forEach(s=>e.add(s)),a=Object.getPrototypeOf(a)}return Array.from(e)}},l=M.getInstance();var u={PROPERTIES:"cereale:properties",TYPE:"cereale:type",VALIDATION:"cereale:validation",SERIALIZER:"cereale:serializer",DESERIALIZER:"cereale:deserializer",POLYMORPHIC:"cereale:polymorphic",IS_OPTIONAL:"cereale:optional"};function f(n,t){l.registerProperty(n,t)}function o(n,t,e,a){f(n,t),a?.message&&(e.message=a.message);let i=l.getOwnMetadata(u.VALIDATION,n,t)||[];i.push(e),l.defineMetadata(u.VALIDATION,i,n,t)}function V(n){return(t,e)=>{f(t,e),l.defineMetadata(u.SERIALIZER,n,t,e)}}function w(n){return(t,e)=>{f(t,e),l.defineMetadata(u.DESERIALIZER,n,t,e)}}function C(n){return(t,e)=>{f(t,e),l.defineMetadata(u.TYPE,n,t,e)}}function N(n,t){return(e,a)=>{f(e,a),l.defineMetadata(u.POLYMORPHIC,{discriminator:n,subTypes:t},e,a)}}function z(){return(n,t)=>{f(n,t),l.defineMetadata(u.IS_OPTIONAL,!0,n,t)}}function L(){return(n,t)=>{o(n,t,{name:"isString",validate:e=>typeof e=="string",message:`${t} must be a string`})}}function R(){return(n,t)=>{o(n,t,{name:"isBoolean",validate:e=>typeof e=="boolean",message:`${t} must be a boolean`})}}function D(){return(n,t)=>{o(n,t,{name:"isNumber",validate:e=>typeof e=="number"&&!isNaN(e),message:`${t} must be a number`})}}function k(){return(n,t)=>{o(n,t,{name:"isInt",validate:e=>Number.isInteger(e),message:`${t} must be an integer`})}}function J(){return(n,t)=>{o(n,t,{name:"isObject",validate:e=>typeof e=="object"&&e!==null&&!Array.isArray(e),message:`${t} must be an object`})}}function Y(){return(n,t)=>{o(n,t,{name:"isDefined",validate:e=>e!=null,message:`${t} should not be null or undefined`})}}function Z(){return(n,t)=>{o(n,t,{name:"isNotEmpty",validate:e=>e!=null&&e!=="",message:`${t} should not be empty`})}}function U(n){return(t,e)=>{o(t,e,{name:"min",validate:a=>typeof a=="number"&&a>=n,message:`${e} must be at least ${n}`})}}function H(n){return(t,e)=>{o(t,e,{name:"max",validate:a=>typeof a=="number"&&a<=n,message:`${e} must be at most ${n}`})}}function W(){return(n,t)=>{o(n,t,{name:"positive",validate:e=>typeof e=="number"&&e>0,message:`${t} must be positive`})}}function q(){return(n,t)=>{o(n,t,{name:"negative",validate:e=>typeof e=="number"&&e<0,message:`${t} must be negative`})}}function B(n){return(t,e)=>{o(t,e,{name:"minLength",validate:a=>typeof a=="string"&&a.length>=n,message:`${e} must be longer than or equal to ${n} characters`})}}function F(n){return(t,e)=>{o(t,e,{name:"maxLength",validate:a=>typeof a=="string"&&a.length<=n,message:`${e} must be shorter than or equal to ${n} characters`})}}function _(){let n=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return(t,e)=>{o(t,e,{name:"isEmail",validate:a=>typeof a=="string"&&n.test(a),message:`${e} must be a valid email`})}}function G(){return(n,t)=>{o(n,t,{name:"isUrl",validate:e=>{try{return new URL(e),!0}catch{return!1}},message:`${t} must be a valid URL`})}}function Q(n){return(t,e)=>{o(t,e,{name:"matches",validate:a=>typeof a=="string"&&n.test(a),message:`${e} must match ${n} regular expression`})}}function X(){return(n,t)=>{o(n,t,{name:"isArray",validate:e=>Array.isArray(e),message:`${t} must be an array`})}}function j(n){return(t,e)=>{o(t,e,{name:"arrayMinSize",validate:a=>Array.isArray(a)&&a.length>=n,message:`${e} must contain at least ${n} elements`})}}function K(n){return(t,e)=>{o(t,e,{name:"arrayMaxSize",validate:a=>Array.isArray(a)&&a.length<=n,message:`${e} must contain at most ${n} elements`})}}function tt(){return(n,t)=>{o(n,t,{name:"arrayNotEmpty",validate:e=>Array.isArray(e)&&e.length>0,message:`${t} should not be empty`})}}function et(n){return(t,e)=>{o(t,e,{name:"isIn",validate:a=>n.includes(a),message:`${e} must be one of the following values: ${n.join(", ")}`})}}function nt(n){return(t,e)=>{o(t,e,{name:"isNotIn",validate:a=>!n.includes(a),message:`${e} must not be one of the following values: ${n.join(", ")}`})}}function at(){return(n,t)=>{o(n,t,{name:"isDate",validate:e=>e instanceof Date&&!isNaN(e.getTime()),message:`${t} must be a valid Date object`})}}function st(){return(n,t)=>{f(n,t),l.defineMetadata("cereale:nested",!0,n,t)}}var h=class extends Error{constructor(e,a){super(e);this.errors=a;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}},x=class{static async toPlain(t){if(t==null)return t;let e=await this.validate(t);if(e.length>0)throw new h("Validation failed during serialization",e);return this.serialize(t)}static async toJson(t){let e=await this.toPlain(t);return JSON.stringify(e)}static async toInstance(t,e){let a=this.deserialize(t,e),i=await this.validate(a);if(i.length>0)throw new h("Validation failed during deserialization",i);return a}static async fromJson(t,e){let a=JSON.parse(e);return this.toInstance(t,a)}static serialize(t){if(t==null||typeof t!="object")return t;if(Array.isArray(t))return t.map(s=>this.serialize(s));if(t instanceof Date)return t.toISOString();let e=t.constructor.prototype,a={},i=Object.keys(t);for(let s of i){let r=t[s],g=l.getMetadata(u.SERIALIZER,e,s);if(g){let p=new g;a[s]=p.serialize(r)}else a[s]=this.serialize(r)}return a}static deserialize(t,e){if(e==null)return e;if(Array.isArray(e))return e.map(s=>this.deserialize(t,s));let a=new t,i=t.prototype;for(let s of Object.keys(e)){let r=e[s],g=l.getMetadata(u.DESERIALIZER,i,s);if(g){let d=new g;a[s]=d.deserialize(r);continue}let p=l.getMetadata(u.POLYMORPHIC,i,s);if(p&&r!==null&&r!==void 0){let{discriminator:d,subTypes:y}=p;if(Array.isArray(r))a[s]=r.map(m=>{let c=y.find(v=>m[d]===v.name);return c?this.deserialize(c.value,m):m});else{let m=y.find(c=>r[d]===c.name);if(m){a[s]=this.deserialize(m.value,r);continue}}continue}let I=l.getMetadata(u.TYPE,i,s);if(I&&r!==null&&r!==void 0){let d=I();a[s]=this.deserialize(d,r);continue}a[s]=r}return a}static async validate(t){let e=[];if(t==null||typeof t!="object")return e;if(Array.isArray(t)){for(let s=0;s0&&e.push({property:`[${s}]`,value:t[s],constraints:{},children:r})}return e}let a=Object.getPrototypeOf(t),i=l.getProperties(a);for(let s of i){let r=t[s],g={property:s,value:r,constraints:{}},p=l.getMetadata(u.IS_OPTIONAL,a,s),I=r==null;if(p&&I)continue;let d=l.getMetadata(u.VALIDATION,a,s)||[],y={value:r,object:t,property:s,constraints:[]};for(let c of d)if(y.constraints=c.constraints||[],!await c.validate(r,y)){let E=typeof c.message=="function"?c.message(y):c.message;g.constraints[c.name]=E}if(l.getMetadata("cereale:nested",a,s)&&r!==null&&r!==void 0){let c=await this.validate(r);c.length>0&&(g.children=c)}(Object.keys(g.constraints).length>0||g.children)&&e.push(g)}return e}};return S(it);})(); +"use strict";var Cereale=(()=>{var k=Object.defineProperty;var an=Object.getOwnPropertyDescriptor;var sn=Object.getOwnPropertyNames;var rn=Object.prototype.hasOwnProperty;var on=(n,t)=>{for(var a in t)k(n,a,{get:t[a],enumerable:!0})},ln=(n,t,a,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of sn(t))!rn.call(n,i)&&i!==a&&k(n,i,{get:()=>t[i],enumerable:!(e=an(t,i))||e.enumerable});return n};var un=n=>ln(k({},"__esModule",{value:!0}),n);var Lt={};on(Lt,{Allow:()=>xt,ArrayContains:()=>At,ArrayMaxSize:()=>_n,ArrayMinSize:()=>zn,ArrayNotContains:()=>It,ArrayNotEmpty:()=>qn,ArrayUnique:()=>wt,Contains:()=>at,Email:()=>Jn,EndsWith:()=>rt,Equals:()=>Bn,IsAlpha:()=>Qn,IsAlphanumeric:()=>Kn,IsArray:()=>Un,IsBigInt:()=>bt,IsBoolean:()=>In,IsDate:()=>jn,IsDateString:()=>ft,IsDefined:()=>Nn,IsDivisibleBy:()=>pt,IsEmpty:()=>Gn,IsEnum:()=>Yn,IsHexColor:()=>dt,IsIP:()=>mt,IsIn:()=>Fn,IsInstance:()=>Hn,IsInt:()=>xn,IsJSON:()=>ct,IsLatitude:()=>Ot,IsLongitude:()=>ht,IsLowercase:()=>tt,IsNotEmpty:()=>En,IsNotIn:()=>Zn,IsNumber:()=>Cn,IsNumberString:()=>nt,IsObject:()=>Vn,IsOptional:()=>wn,IsPort:()=>yt,IsSemVer:()=>gt,IsString:()=>An,IsUUID:()=>ut,IsUppercase:()=>et,IsUrl:()=>Rn,JsonAlias:()=>mn,JsonDeserialize:()=>bn,JsonIgnore:()=>pn,JsonMapper:()=>z,JsonMappingError:()=>h,JsonPolymorphic:()=>Sn,JsonProperty:()=>dn,JsonReadOnly:()=>yn,JsonSerialize:()=>hn,JsonType:()=>$n,JsonValidationError:()=>I,JsonWriteOnly:()=>On,Length:()=>Xn,METADATA_KEYS:()=>f,Matches:()=>Ln,Max:()=>Tn,MaxDate:()=>St,MaxLength:()=>Dn,Min:()=>Mn,MinDate:()=>$t,MinLength:()=>kn,Negative:()=>vn,NotContains:()=>it,NotEquals:()=>Wn,Positive:()=>Pn,StartsWith:()=>st,Validate:()=>Nt,ValidateIf:()=>Ct,ValidateNested:()=>Vt,collectErrorMessages:()=>Tt,configure:()=>cn,flattenErrors:()=>R,formatErrors:()=>Mt,fromJson:()=>K,fromJsonArray:()=>nn,fromRequest:()=>en,getConfig:()=>fn,registerDecorator:()=>Et,resetConfig:()=>gn,resolveNamingStrategy:()=>E,resolveOptions:()=>A,toInstance:()=>N,toInstanceArray:()=>q,toJson:()=>Q,toPlain:()=>_,validate:()=>V,validateOrReject:()=>X});function C(n){return n.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g,"$1 $2").replace(/[_\-\s]+/g," ").trim().split(" ").filter(Boolean).map(t=>t.toLowerCase())}var Z=n=>n&&n.charAt(0).toUpperCase()+n.slice(1),D={identity:n=>n,camelCase:n=>{let t=C(n);return t.length===0?n:t[0]+t.slice(1).map(Z).join("")},PascalCase:n=>C(n).map(Z).join("")||n,snake_case:n=>C(n).join("_")||n,SCREAMING_SNAKE_CASE:n=>C(n).join("_").toUpperCase()||n,"kebab-case":n=>C(n).join("-")||n};function E(n){if(!n)return D.identity;if(typeof n=="function")return n;let t=D[n];if(!t)throw new Error(`Unknown naming strategy ${JSON.stringify(n)}. Use one of: ${Object.keys(D).join(", ")}, or pass your own function.`);return t}var j={namingStrategy:"identity",unknownKeys:"allow",validate:!0},S={...j};function cn(n){S={...S,...n}}function fn(){return{...S}}function gn(){S={...j}}function A(n){return n?{namingStrategy:n.namingStrategy??S.namingStrategy,unknownKeys:n.unknownKeys??S.unknownKeys,validate:n.validate??S.validate}:S}var J=class n{static instance;properties=new WeakMap;propertyMetadata=new WeakMap;classMetadata=new WeakMap;constructor(){}static getInstance(){return n.instance||(n.instance=new n),n.instance}defineMetadata(t,a,e,i){if(i){let s=this.propertyMetadata.get(e);s||(s=new Map,this.propertyMetadata.set(e,s));let r=s.get(i);r||(r=new Map,s.set(i,r)),r.set(t,a)}else{let s=this.classMetadata.get(e);s||(s=new Map,this.classMetadata.set(e,s)),s.set(t,a)}}getMetadata(t,a,e){let i=a;for(;i;){let s=this.getOwnMetadata(t,i,e);if(s!==void 0)return s;i=Object.getPrototypeOf(i)}}getMetadataChain(t,a,e){let i=[],s=a;for(;s;){let r=this.getOwnMetadata(t,s,e);r!==void 0&&i.unshift(r),s=Object.getPrototypeOf(s)}return i}getOwnMetadata(t,a,e){return e?this.propertyMetadata.get(a)?.get(e)?.get(t):this.classMetadata.get(a)?.get(t)}registerProperty(t,a){let e=this.properties.get(t);e||(e=[],this.properties.set(t,e)),e.includes(a)||e.push(a)}getProperties(t){let a=new Set,e=t;for(;e;){let i=this.properties.get(e);i&&i.forEach(s=>a.add(s)),e=Object.getPrototypeOf(e)}return Array.from(a)}},c=J.getInstance();var f={PROPERTIES:"cereale:properties",TYPE:"cereale:type",VALIDATION:"cereale:validation",SERIALIZER:"cereale:serializer",DESERIALIZER:"cereale:deserializer",POLYMORPHIC:"cereale:polymorphic",IS_OPTIONAL:"cereale:optional",NESTED:"cereale:nested",NAME:"cereale:name",ALIASES:"cereale:aliases",ACCESS:"cereale:access",CONDITION:"cereale:condition"};function p(n,t){c.registerProperty(n,t)}function o(n,t,a,e){p(n,t),e&&(e.each&&(a.each=!0),e.message&&(a.message=e.message,a.hasCustomMessage=!0));let i=c.getOwnMetadata(f.VALIDATION,n,t)||[];i.push(a),c.defineMetadata(f.VALIDATION,i,n,t)}function dn(n){return(t,a)=>{p(t,a),c.defineMetadata(f.NAME,n,t,a)}}function mn(...n){return(t,a)=>{p(t,a);let e=c.getOwnMetadata(f.ALIASES,t,a)||[];c.defineMetadata(f.ALIASES,[...e,...n],t,a)}}function pn(){return(n,t)=>{p(n,t),c.defineMetadata(f.ACCESS,"none",n,t)}}function yn(){return(n,t)=>{p(n,t),c.defineMetadata(f.ACCESS,"readonly",n,t)}}function On(){return(n,t)=>{p(n,t),c.defineMetadata(f.ACCESS,"writeonly",n,t)}}function hn(n){return(t,a)=>{p(t,a),c.defineMetadata(f.SERIALIZER,n,t,a)}}function bn(n){return(t,a)=>{p(t,a),c.defineMetadata(f.DESERIALIZER,n,t,a)}}function $n(n){return(t,a)=>{p(t,a),c.defineMetadata(f.TYPE,n,t,a)}}function Sn(n,t,a){return(e,i)=>{p(e,i),c.defineMetadata(f.POLYMORPHIC,{discriminator:n,subTypes:t,onUnknown:a?.onUnknown??"keep",fallback:a?.fallback},e,i)}}function wn(){return(n,t)=>{p(n,t),c.defineMetadata(f.IS_OPTIONAL,!0,n,t)}}function An(n){return(t,a)=>{o(t,a,{name:"isString",validate:e=>typeof e=="string",message:`${a} must be a string`},n)}}function In(n){return(t,a)=>{o(t,a,{name:"isBoolean",validate:e=>typeof e=="boolean",message:`${a} must be a boolean`},n)}}function Cn(n){return(t,a)=>{o(t,a,{name:"isNumber",validate:e=>typeof e=="number"&&!isNaN(e),message:`${a} must be a number`},n)}}function xn(n){return(t,a)=>{o(t,a,{name:"isInt",validate:e=>Number.isInteger(e),message:`${a} must be an integer`},n)}}function Vn(n){return(t,a)=>{o(t,a,{name:"isObject",validate:e=>typeof e=="object"&&e!==null&&!Array.isArray(e),message:`${a} must be an object`},n)}}function Nn(n){return(t,a)=>{o(t,a,{name:"isDefined",validate:e=>e!=null,message:`${a} should not be null or undefined`},n)}}function En(n){return(t,a)=>{o(t,a,{name:"isNotEmpty",validate:e=>e!=null&&e!=="",message:`${a} should not be empty`},n)}}function Mn(n,t){return(a,e)=>{o(a,e,{name:"min",validate:i=>typeof i=="number"&&i>=n,message:`${e} must be at least ${n}`,constraints:[n]},t)}}function Tn(n,t){return(a,e)=>{o(a,e,{name:"max",validate:i=>typeof i=="number"&&i<=n,message:`${e} must be at most ${n}`,constraints:[n]},t)}}function Pn(n){return(t,a)=>{o(t,a,{name:"positive",validate:e=>typeof e=="number"&&e>0,message:`${a} must be positive`},n)}}function vn(n){return(t,a)=>{o(t,a,{name:"negative",validate:e=>typeof e=="number"&&e<0,message:`${a} must be negative`},n)}}function kn(n,t){return(a,e)=>{o(a,e,{name:"minLength",validate:i=>typeof i=="string"&&i.length>=n,message:`${e} must be longer than or equal to ${n} characters`,constraints:[n]},t)}}function Dn(n,t){return(a,e)=>{o(a,e,{name:"maxLength",validate:i=>typeof i=="string"&&i.length<=n,message:`${e} must be shorter than or equal to ${n} characters`,constraints:[n]},t)}}function Jn(n){let t=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return(a,e)=>{o(a,e,{name:"isEmail",validate:i=>typeof i=="string"&&t.test(i),message:`${e} must be a valid email`},n)}}function Rn(n){return(t,a)=>{o(t,a,{name:"isUrl",validate:e=>{try{return new URL(e),!0}catch{return!1}},message:`${a} must be a valid URL`},n)}}function Ln(n,t){let a=n.flags.includes("g")||n.flags.includes("y")?new RegExp(n.source,n.flags.replace(/[gy]/g,"")):n;return(e,i)=>{o(e,i,{name:"matches",validate:s=>typeof s=="string"&&a.test(s),message:`${i} must match ${n} regular expression`,constraints:[n]},t)}}function Un(n){return(t,a)=>{o(t,a,{name:"isArray",validate:e=>Array.isArray(e),message:`${a} must be an array`},n)}}function zn(n,t){return(a,e)=>{o(a,e,{name:"arrayMinSize",validate:i=>Array.isArray(i)&&i.length>=n,message:`${e} must contain at least ${n} elements`,constraints:[n]},t)}}function _n(n,t){return(a,e)=>{o(a,e,{name:"arrayMaxSize",validate:i=>Array.isArray(i)&&i.length<=n,message:`${e} must contain at most ${n} elements`,constraints:[n]},t)}}function qn(n){return(t,a)=>{o(t,a,{name:"arrayNotEmpty",validate:e=>Array.isArray(e)&&e.length>0,message:`${a} should not be empty`},n)}}function Fn(n,t){return(a,e)=>{o(a,e,{name:"isIn",validate:i=>n.includes(i),message:`${e} must be one of the following values: ${n.join(", ")}`,constraints:[n]},t)}}function Zn(n,t){return(a,e)=>{o(a,e,{name:"isNotIn",validate:i=>!n.includes(i),message:`${e} must not be one of the following values: ${n.join(", ")}`,constraints:[n]},t)}}function jn(n){return(t,a)=>{o(t,a,{name:"isDate",validate:e=>e instanceof Date&&!isNaN(e.getTime()),message:`${a} must be a valid Date object`},n)}}function Bn(n,t){return(a,e)=>{o(a,e,{name:"equals",validate:i=>i===n,message:`${e} must be equal to ${JSON.stringify(n)}`,constraints:[n]},t)}}function Wn(n,t){return(a,e)=>{o(a,e,{name:"notEquals",validate:i=>i!==n,message:`${e} must not be equal to ${JSON.stringify(n)}`,constraints:[n]},t)}}function Gn(n){return(t,a)=>{o(t,a,{name:"isEmpty",validate:e=>e==null||e===""?!0:Array.isArray(e)?e.length===0:typeof e=="object"?Object.keys(e).length===0:!1,message:`${a} must be empty`},n)}}function Yn(n,t){let a=Object.keys(n).filter(e=>typeof n[n[e]]!="number").map(e=>n[e]);return(e,i)=>{o(e,i,{name:"isEnum",validate:s=>a.includes(s),message:`${i} must be one of the following values: ${a.join(", ")}`,constraints:[a]},t)}}function Hn(n,t){return(a,e)=>{o(a,e,{name:"isInstance",validate:i=>i instanceof n,message:`${e} must be an instance of ${n.name}`,constraints:[n]},t)}}function Xn(n,t,a){return(e,i)=>{o(e,i,{name:"length",validate:s=>typeof s=="string"&&s.length>=n&&(t===void 0||s.length<=t),message:t===void 0?`${i} must be at least ${n} characters`:`${i} must be between ${n} and ${t} characters`,constraints:t===void 0?[n]:[n,t]},a)}}function T(n,t,a){return e=>(i,s)=>{o(i,s,{name:n,validate:r=>typeof r=="string"&&t.test(r),message:a(s)},e)}}var Qn=T("isAlpha",/^[A-Za-z]+$/,n=>`${n} must contain only letters`),Kn=T("isAlphanumeric",/^[A-Za-z0-9]+$/,n=>`${n} must contain only letters and numbers`);function nt(n){return(t,a)=>{o(t,a,{name:"isNumberString",validate:e=>typeof e=="string"&&e.trim()!==""&&Number.isFinite(Number(e)),message:`${a} must be a number string`},n)}}function tt(n){return(t,a)=>{o(t,a,{name:"isLowercase",validate:e=>typeof e=="string"&&e===e.toLowerCase(),message:`${a} must be lowercase`},n)}}function et(n){return(t,a)=>{o(t,a,{name:"isUppercase",validate:e=>typeof e=="string"&&e===e.toUpperCase(),message:`${a} must be uppercase`},n)}}function at(n,t){return(a,e)=>{o(a,e,{name:"contains",validate:i=>typeof i=="string"&&i.includes(n),message:`${e} must contain ${JSON.stringify(n)}`,constraints:[n]},t)}}function it(n,t){return(a,e)=>{o(a,e,{name:"notContains",validate:i=>typeof i=="string"&&!i.includes(n),message:`${e} must not contain ${JSON.stringify(n)}`,constraints:[n]},t)}}function st(n,t){return(a,e)=>{o(a,e,{name:"startsWith",validate:i=>typeof i=="string"&&i.startsWith(n),message:`${e} must start with ${JSON.stringify(n)}`,constraints:[n]},t)}}function rt(n,t){return(a,e)=>{o(a,e,{name:"endsWith",validate:i=>typeof i=="string"&&i.endsWith(n),message:`${e} must end with ${JSON.stringify(n)}`,constraints:[n]},t)}}var ot="00000000-0000-0000-0000-000000000000",lt="ffffffff-ffff-ffff-ffff-ffffffffffff";function ut(n,t){let a=n?new RegExp(`^[0-9a-f]{8}-[0-9a-f]{4}-${n}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`,"i"):/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return(e,i)=>{o(e,i,{name:"isUuid",validate:s=>typeof s!="string"?!1:!n&&(s.toLowerCase()===ot||s.toLowerCase()===lt)?!0:a.test(s),message:`${i} must be a valid UUID${n?` (version ${n})`:""}`,...n?{constraints:[n]}:{}},t)}}function ct(n){return(t,a)=>{o(t,a,{name:"isJson",validate:e=>{if(typeof e!="string")return!1;try{return JSON.parse(e),!0}catch{return!1}},message:`${a} must be a JSON string`},n)}}function ft(n){return(t,a)=>{o(t,a,{name:"isDateString",validate:e=>typeof e=="string"&&!isNaN(Date.parse(e)),message:`${a} must be a valid ISO 8601 date string`},n)}}var gt=T("isSemVer",/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,n=>`${n} must be a valid semantic version`),dt=T("isHexColor",/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i,n=>`${n} must be a hex color`),B=/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;function mt(n,t){let a=e=>{try{return new URL(`http://[${e}]`).hostname===`[${e.toLowerCase()}]`||/^[0-9a-f:.]+$/i.test(e)&&e.includes(":")}catch{return!1}};return(e,i)=>{o(e,i,{name:"isIp",validate:s=>typeof s!="string"?!1:n===4?B.test(s):n===6?a(s):B.test(s)||a(s),message:`${i} must be a valid IP${n?`v${n}`:""} address`,...n?{constraints:[n]}:{}},t)}}function pt(n,t){return(a,e)=>{o(a,e,{name:"isDivisibleBy",validate:i=>typeof i=="number"&&Number.isFinite(i)&&n!==0&&i%n===0,message:`${e} must be divisible by ${n}`,constraints:[n]},t)}}function yt(n){return(t,a)=>{o(t,a,{name:"isPort",validate:e=>{let i=typeof e=="string"&&e.trim()!==""?Number(e):e;return typeof i=="number"&&Number.isInteger(i)&&i>=0&&i<=65535},message:`${a} must be a valid port number`},n)}}function Ot(n){return(t,a)=>{o(t,a,{name:"isLatitude",validate:e=>typeof e=="number"&&Number.isFinite(e)&&e>=-90&&e<=90,message:`${a} must be a latitude between -90 and 90`},n)}}function ht(n){return(t,a)=>{o(t,a,{name:"isLongitude",validate:e=>typeof e=="number"&&Number.isFinite(e)&&e>=-180&&e<=180,message:`${a} must be a longitude between -180 and 180`},n)}}function bt(n){return(t,a)=>{o(t,a,{name:"isBigInt",validate:e=>typeof e=="bigint",message:`${a} must be a bigint`},n)}}var M=n=>typeof n=="function"?n():n;function $t(n,t){return(a,e)=>{o(a,e,{name:"minDate",validate:i=>i instanceof Date&&!isNaN(i.getTime())&&i.getTime()>=M(n).getTime(),message:i=>`${i.property} must not be earlier than ${M(n).toISOString()}`,constraints:[n]},t)}}function St(n,t){return(a,e)=>{o(a,e,{name:"maxDate",validate:i=>i instanceof Date&&!isNaN(i.getTime())&&i.getTime()<=M(n).getTime(),message:i=>`${i.property} must not be later than ${M(n).toISOString()}`,constraints:[n]},t)}}function wt(n,t){return(a,e)=>{o(a,e,{name:"arrayUnique",validate:i=>{if(!Array.isArray(i))return!1;let s=n?i.map(n):i;return new Set(s).size===s.length},message:`${e} must not contain duplicate values`},t)}}function At(n,t){return(a,e)=>{o(a,e,{name:"arrayContains",validate:i=>Array.isArray(i)&&n.every(s=>i.includes(s)),message:`${e} must contain the following values: ${n.join(", ")}`,constraints:[n]},t)}}function It(n,t){return(a,e)=>{o(a,e,{name:"arrayNotContains",validate:i=>Array.isArray(i)&&n.every(s=>!i.includes(s)),message:`${e} must not contain any of the following values: ${n.join(", ")}`,constraints:[n]},t)}}function Ct(n){return(t,a)=>{p(t,a),c.defineMetadata(f.CONDITION,n,t,a)}}function xt(){return(n,t)=>{p(n,t)}}function Vt(n){return(t,a)=>{p(t,a),c.defineMetadata(f.NESTED,!0,t,a),n?.each&&o(t,a,{name:"nestedEach",validate:e=>Array.isArray(e),message:`${a} must be an array`})}}function Nt(n,t,a){return(e,i)=>{let s=[],r;if(Array.isArray(t)?(s=t,r=a):typeof t=="object"&&(r=t),typeof n=="function"&&!n.prototype?.validate)o(e,i,{name:"custom",validate:n,message:l=>`${l.property} is invalid`,constraints:s},r);else{let l=new n;o(e,i,{name:n.name,validate:(u,g)=>l.validate(u,g),message:u=>l.defaultMessage?l.defaultMessage(u):`${u.property} is invalid`,constraints:s},r)}}}function Et(n){let{name:t,target:a,propertyName:e,options:i,constraints:s,validator:r}=n,l;if(typeof r=="function"&&!r.prototype?.validate)l={name:t,validate:r,message:u=>`${u.property} is invalid`,...s?{constraints:s}:{}};else{let u=typeof r=="function"?new r:r;l={name:t,validate:(g,d)=>u.validate(g,d),message:g=>u.defaultMessage?u.defaultMessage(g):`${g.property} is invalid`,...s?{constraints:s}:{}}}o(a.prototype,e,l,i)}function R(n){let t={},a=(e,i)=>{for(let s of e){let r=s.property.startsWith("[")?`${i}${s.property}`:i?`${i}.${s.property}`:s.property,l=Object.values(s.constraints);l.length>0&&(t[r]??=[]).push(...l),s.children?.length&&a(s.children,r)}};return a(n,""),t}function Mt(n){let t=R(n);return Object.entries(t).flatMap(([a,e])=>e.map(i=>`${a}: ${i}`)).join(` +`)}function Tt(n){return Object.values(R(n)).flat()}var I=class extends Error{constructor(a,e){super(a);this.errors=e;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}},h=class extends Error{constructor(t){super(t),this.name="JsonMappingError"}},Pt=new Set(["__proto__","constructor","prototype"]);function G(n){return Object.getPrototypeOf(n)??void 0}function Y(n,t){return(n?c.getMetadata(f.ACCESS,n,t):void 0)??"readwrite"}function H(n,t,a){return(n?c.getMetadata(f.NAME,n,t):void 0)??a(t)}var W=new WeakMap;function vt(n,t){let a=W.get(n);a||(a=new Map,W.set(n,a));let e=a.get(t.namingKey);if(e)return e;let i=new Map,s=new Set,r=(u,g)=>{let d=i.get(u);if(d&&d!==g)throw new h(`Properties "${d}" and "${g}" both map to the JSON name ${JSON.stringify(u)}. Give one of them a distinct @JsonProperty name.`);i.set(u,g)};for(let u of c.getProperties(n)){let g=[H(n,u,t.naming),...c.getMetadata(f.ALIASES,n,u)||[]],d=Y(n,u);if(d==="none"||d==="readonly"){for(let $ of g)s.add($);continue}for(let $ of g)r($,u)}let l={accept:i,blocked:s};return a.set(t.namingKey,l),l}async function L(n,t,a){if(n==null||typeof n!="object")return n;if(n instanceof Date)return n.toISOString();if(t.has(n))throw new h("Circular reference detected during serialization. Break the cycle with @JsonIgnore() on the back-reference, or supply a @JsonSerialize() serializer for that property.");t.add(n);try{if(Array.isArray(n)){let s=[];for(let r of n)s.push(await L(r,t,a));return s}let e=G(n),i={};for(let s of Object.keys(n)){let r=Y(e,s);if(r==="none"||r==="writeonly")continue;let l=n[s],u=H(e,s,a.naming),g=e?c.getMetadata(f.SERIALIZER,e,s):void 0;if(g&&l!==null&&l!==void 0){let d=new g;i[u]=await d.serialize(l)}else i[u]=await L(l,t,a)}return i}finally{t.delete(n)}}async function x(n,t,a){if(t==null)return t;if(Array.isArray(t))return await Promise.all(t.map(l=>x(n,l,a)));if(typeof t!="object")return t;let e=new n,i=n.prototype,s=vt(i,a);for(let r of Object.keys(t)){if(Pt.has(r)||s.blocked.has(r))continue;let l=s.accept.get(r);if(l===void 0){if(a.unknownKeys==="strip")continue;if(a.unknownKeys==="error")throw new h(`Unknown property ${JSON.stringify(r)} for ${n.name}. Allowed: ${[...s.accept.keys()].map(y=>JSON.stringify(y)).join(", ")||"(none declared)"}.`);e[r]=t[r];continue}let u=t[r],g=c.getMetadata(f.DESERIALIZER,i,l);if(g){let y=new g;e[l]=await y.deserialize(u);continue}let d=c.getMetadata(f.POLYMORPHIC,i,l);if(d&&u!==null&&u!==void 0){let{discriminator:y,subTypes:P,onUnknown:m,fallback:w}=d,b=async O=>{if(O==null||typeof O!="object")return O;let F=P.find(v=>O[y]===v.name);if(F)return x(F.value,O,a);if(w)return x(w,O,a);if(m==="error")throw new h(`Unknown discriminator value ${JSON.stringify(O[y])} for property "${l}". Known values: ${P.map(v=>JSON.stringify(v.name)).join(", ")}.`);return O};e[l]=Array.isArray(u)?await Promise.all(u.map(b)):await b(u);continue}let $=c.getMetadata(f.TYPE,i,l);if($&&u!==null&&u!==void 0){let y=$();e[l]=await x(y,u,a);continue}e[l]=u}return e}function kt(n,t){let a=c.getMetadataChain(f.VALIDATION,n,t),e=[],i=new Set;for(let s of a)for(let r of s){if(typeof r.message=="string"){let l=`${r.name}|${String(r.constraints)}|${r.message}`;if(i.has(l))continue;i.add(l)}e.push(r)}return e}function Dt(n,t,a){if(!(t in n)){n[t]=a;return}let e=2;for(;`${t}_${e}`in n;)e++;n[`${t}_${e}`]=a}async function U(n,t){let a=[];if(n==null||typeof n!="object"||t.has(n))return a;t.add(n);try{if(Array.isArray(n)){for(let s=0;s0&&a.push({property:`[${s}]`,value:n[s],constraints:{},children:r})}return a}let e=G(n);if(!e)return a;let i=c.getProperties(e);for(let s of i){let r=n[s],l={property:s,value:r,constraints:{}},u=c.getMetadata(f.CONDITION,e,s);if(u&&!u(n))continue;let g=c.getMetadata(f.IS_OPTIONAL,e,s),d=r==null;if(g&&d)continue;let $=kt(e,s),y={value:r,object:n,property:s,constraints:[]};for(let m of $){y.constraints=m.constraints||[];let w=!0;if(m.each&&Array.isArray(r))for(let b of r){let O={...y,value:b};if(!await m.validate(b,O)){w=!1;break}}else w=await m.validate(r,y);if(!w){let b=typeof m.message=="function"?m.message(y):m.message;m.each&&!m.hasCustomMessage&&(b=`each element in ${b}`),Dt(l.constraints,m.name,b)}}if(c.getMetadata(f.NESTED,e,s)&&r!==null&&r!==void 0){let m=await U(r,t);m.length>0&&(l.children=m)}(Object.keys(l.constraints).length>0||l.children)&&a.push(l)}return a}finally{t.delete(n)}}function Jt(n){let t=A(n);return{naming:E(t.namingStrategy)}}function Rt(n){let t=A(n);return{naming:E(t.namingStrategy),namingKey:t.namingStrategy,unknownKeys:t.unknownKeys}}async function V(n){return U(n,new Set)}async function X(n){let t=await V(n);if(t.length>0)throw new I("Validation failed",t)}async function _(n,t){if(n==null)return n;if(A(t).validate){let a=await V(n);if(a.length>0)throw new I("Validation failed during serialization",a)}return L(n,new Set,Jt(t))}async function Q(n,t){let a=await _(n,t);return JSON.stringify(a)}async function N(n,t,a){let e=await x(n,t,Rt(a));if(A(a).validate){let i=await V(e);if(i.length>0)throw new I("Validation failed during deserialization",i)}return e}async function q(n,t,a){if(!Array.isArray(t))throw new h(`Expected an array to map to ${n.name}[], received ${typeof t}.`);return await N(n,t,a)}async function K(n,t,a){return N(n,tn(t),a)}async function nn(n,t,a){return q(n,tn(t),a)}function tn(n){try{return JSON.parse(n)}catch(t){throw new h(`Input is not valid JSON: ${t instanceof Error?t.message:String(t)}`)}}async function en(n,t,a){let e;try{e=await t.json()}catch(i){throw new h(`Request body is not valid JSON: ${i instanceof Error?i.message:String(i)}`)}return N(n,e,a)}var z=class{static toPlain=_;static toJson=Q;static toInstance=N;static toInstanceArray=q;static fromJson=K;static fromJsonArray=nn;static fromRequest=en;static validate=V;static validateOrReject=X};return un(Lt);})(); diff --git a/docs/index.html b/docs/index.html index 2f83b6e..c1a7955 100644 --- a/docs/index.html +++ b/docs/index.html @@ -136,12 +136,23 @@

Mapping

    +
  • @JsonProperty('first_name')
  • +
  • @JsonAlias(...names)
  • @JsonSerialize(cls)
  • @JsonDeserialize(cls)
  • @JsonType(() => cls)
  • @JsonPolymorphic(field, types)
+
+

Access Control

+
    +
  • @JsonIgnore()
  • +
  • @JsonReadOnly()
  • +
  • @JsonWriteOnly()
  • +
  • Naming strategies: snake_case, kebab-case, …
  • +
+

Basic Validation

    @@ -158,7 +169,9 @@
  • @Min(n), @Max(n)
  • @MinLength(n), @MaxLength(n)
  • @Email(), @IsUrl()
  • -
  • @ValidateNested()
  • +
  • @IsUUID(v?), @IsEnum(e)
  • +
  • @MinDate(d), @ArrayUnique()
  • +
  • @ValidateNested(), @ValidateIf(fn)
@@ -175,9 +188,10 @@