From 66e19f690f7bfe0c112b49c7946ce6c55e546677 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:14:16 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=94=A7=20fix:=20repair=20test,=20buil?= =?UTF-8?q?d=20and=20CI=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vitest suite silently ran zero tests: vitest 4 transpiles with oxc, which does not pick up `experimentalDecorators` from a tsconfig that excludes the files being transformed, so every decorator-using suite failed to parse and was reported as "0 test". A vitest.config.ts enabling legacy decorators brings all 40 existing tests back to life. - Add vitest.config.ts (oxc legacy decorators + v8 coverage config) - Type-check test files: move the test/demo exclusions from the base tsconfig onto the two build configs, and fix the strict-mode errors this surfaced - Stop shipping src/example.ts in dist (it invokes runExample() at import time, a side effect in a package declaring "sideEffects": false) - CI: run lint, tests with coverage, build and entry-point smoke checks; drop EOL Node 18, add Node 24; commit package-lock.json so `npm ci` works - Add `verify`, `test:watch` and `build:docs` scripts, and an engines field; `build:docs` regenerates the previously hand-maintained docs/cereale.js Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK --- .github/workflows/ci.yml | 16 +- .gitignore | 3 +- docs/cereale.js | 2 +- package-lock.json | 3258 ++++++++++++++++++++++++++++++++++++++ package.json | 9 +- src/decorators.test.ts | 14 +- src/index.test.ts | 18 +- src/utils.test.ts | 2 +- tsconfig.cjs.json | 3 +- tsconfig.esm.json | 3 +- tsconfig.json | 4 +- vitest.config.ts | 20 + 12 files changed, 3327 insertions(+), 25 deletions(-) create mode 100644 package-lock.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7387375..f504770 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,14 @@ on: branches: [ main ] 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/docs/cereale.js b/docs/cereale.js index e16a915..c576e02 100644 --- a/docs/cereale.js +++ b/docs/cereale.js @@ -1 +1 @@ -"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 v=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var L=(t,n)=>{for(var e in n)v(t,e,{get:n[e],enumerable:!0})},D=(t,n,e,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of N(n))!R.call(t,i)&&i!==e&&v(t,i,{get:()=>n[i],enumerable:!(a=S(n,i))||a.enumerable});return t};var z=t=>D(v({},"__esModule",{value:!0}),t);var pt={};L(pt,{ArrayMaxSize:()=>ot,ArrayMinSize:()=>rt,ArrayNotEmpty:()=>lt,Email:()=>et,IsArray:()=>st,IsBoolean:()=>H,IsDate:()=>dt,IsDefined:()=>_,IsIn:()=>ut,IsInt:()=>B,IsNotEmpty:()=>G,IsNotIn:()=>ct,IsNumber:()=>W,IsObject:()=>F,IsOptional:()=>q,IsString:()=>U,IsUrl:()=>at,JsonDeserialize:()=>J,JsonMapper:()=>C,JsonPolymorphic:()=>Z,JsonSerialize:()=>k,JsonType:()=>Y,JsonValidationError:()=>O,METADATA_KEYS:()=>f,Matches:()=>it,Max:()=>X,MaxLength:()=>nt,Min:()=>Q,MinLength:()=>tt,Negative:()=>K,Positive:()=>j,Validate:()=>ft,ValidateNested:()=>gt,fromJson:()=>w,fromRequest:()=>T,registerDecorator:()=>mt,toInstance:()=>M,toJson:()=>P,toPlain:()=>E,validate:()=>I});var x=class t{static instance;properties=new WeakMap;propertyMetadata=new WeakMap;classMetadata=new WeakMap;constructor(){}static getInstance(){return t.instance||(t.instance=new t),t.instance}defineMetadata(n,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(n,e)}else{let s=this.classMetadata.get(a);s||(s=new Map,this.classMetadata.set(a,s)),s.set(n,e)}}getMetadata(n,e,a){let i=e;for(;i;){let s=this.getOwnMetadata(n,i,a);if(s!==void 0)return s;i=Object.getPrototypeOf(i)}}getOwnMetadata(n,e,a){return a?this.propertyMetadata.get(e)?.get(a)?.get(n):this.classMetadata.get(e)?.get(n)}registerProperty(n,e){let a=this.properties.get(n);a||(a=[],this.properties.set(n,a)),a.includes(e)||a.push(e)}getProperties(n){let e=new Set,a=n;for(;a;){let i=this.properties.get(a);i&&i.forEach(s=>e.add(s)),a=Object.getPrototypeOf(a)}return Array.from(e)}},l=x.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"};function y(t,n){l.registerProperty(t,n)}function o(t,n,e,a){y(t,n),a&&(a.each&&(e.each=!0),a.message&&(e.message=a.message));let i=l.getOwnMetadata(f.VALIDATION,t,n)||[];i.push(e),l.defineMetadata(f.VALIDATION,i,t,n)}function k(t){return(n,e)=>{y(n,e),l.defineMetadata(f.SERIALIZER,t,n,e)}}function J(t){return(n,e)=>{y(n,e),l.defineMetadata(f.DESERIALIZER,t,n,e)}}function Y(t){return(n,e)=>{y(n,e),l.defineMetadata(f.TYPE,t,n,e)}}function Z(t,n){return(e,a)=>{y(e,a),l.defineMetadata(f.POLYMORPHIC,{discriminator:t,subTypes:n},e,a)}}function q(){return(t,n)=>{y(t,n),l.defineMetadata(f.IS_OPTIONAL,!0,t,n)}}function U(t){return(n,e)=>{o(n,e,{name:"isString",validate:a=>typeof a=="string",message:`${e} must be a string`},t)}}function H(t){return(n,e)=>{o(n,e,{name:"isBoolean",validate:a=>typeof a=="boolean",message:`${e} must be a boolean`},t)}}function W(t){return(n,e)=>{o(n,e,{name:"isNumber",validate:a=>typeof a=="number"&&!isNaN(a),message:`${e} must be a number`},t)}}function B(t){return(n,e)=>{o(n,e,{name:"isInt",validate:a=>Number.isInteger(a),message:`${e} must be an integer`},t)}}function F(t){return(n,e)=>{o(n,e,{name:"isObject",validate:a=>typeof a=="object"&&a!==null&&!Array.isArray(a),message:`${e} must be an object`},t)}}function _(t){return(n,e)=>{o(n,e,{name:"isDefined",validate:a=>a!=null,message:`${e} should not be null or undefined`},t)}}function G(t){return(n,e)=>{o(n,e,{name:"isNotEmpty",validate:a=>a!=null&&a!=="",message:`${e} should not be empty`},t)}}function Q(t,n){return(e,a)=>{o(e,a,{name:"min",validate:i=>typeof i=="number"&&i>=t,message:`${a} must be at least ${t}`,constraints:[t]},n)}}function X(t,n){return(e,a)=>{o(e,a,{name:"max",validate:i=>typeof i=="number"&&i<=t,message:`${a} must be at most ${t}`,constraints:[t]},n)}}function j(t){return(n,e)=>{o(n,e,{name:"positive",validate:a=>typeof a=="number"&&a>0,message:`${e} must be positive`},t)}}function K(t){return(n,e)=>{o(n,e,{name:"negative",validate:a=>typeof a=="number"&&a<0,message:`${e} must be negative`},t)}}function tt(t,n){return(e,a)=>{o(e,a,{name:"minLength",validate:i=>typeof i=="string"&&i.length>=t,message:`${a} must be longer than or equal to ${t} characters`,constraints:[t]},n)}}function nt(t,n){return(e,a)=>{o(e,a,{name:"maxLength",validate:i=>typeof i=="string"&&i.length<=t,message:`${a} must be shorter than or equal to ${t} characters`,constraints:[t]},n)}}function et(t){let n=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return(e,a)=>{o(e,a,{name:"isEmail",validate:i=>typeof i=="string"&&n.test(i),message:`${a} must be a valid email`},t)}}function at(t){return(n,e)=>{o(n,e,{name:"isUrl",validate:a=>{try{return new URL(a),!0}catch{return!1}},message:`${e} must be a valid URL`},t)}}function it(t,n){return(e,a)=>{o(e,a,{name:"matches",validate:i=>typeof i=="string"&&t.test(i),message:`${a} must match ${t} regular expression`,constraints:[t]},n)}}function st(t){return(n,e)=>{o(n,e,{name:"isArray",validate:a=>Array.isArray(a),message:`${e} must be an array`},t)}}function rt(t,n){return(e,a)=>{o(e,a,{name:"arrayMinSize",validate:i=>Array.isArray(i)&&i.length>=t,message:`${a} must contain at least ${t} elements`,constraints:[t]},n)}}function ot(t,n){return(e,a)=>{o(e,a,{name:"arrayMaxSize",validate:i=>Array.isArray(i)&&i.length<=t,message:`${a} must contain at most ${t} elements`,constraints:[t]},n)}}function lt(t){return(n,e)=>{o(n,e,{name:"arrayNotEmpty",validate:a=>Array.isArray(a)&&a.length>0,message:`${e} should not be empty`},t)}}function ut(t,n){return(e,a)=>{o(e,a,{name:"isIn",validate:i=>t.includes(i),message:`${a} must be one of the following values: ${t.join(", ")}`,constraints:[t]},n)}}function ct(t,n){return(e,a)=>{o(e,a,{name:"isNotIn",validate:i=>!t.includes(i),message:`${a} must not be one of the following values: ${t.join(", ")}`,constraints:[t]},n)}}function dt(t){return(n,e)=>{o(n,e,{name:"isDate",validate:a=>a instanceof Date&&!isNaN(a.getTime()),message:`${e} must be a valid Date object`},t)}}function gt(){return(t,n)=>{y(t,n),l.defineMetadata("cereale:nested",!0,t,n)}}function ft(t,n,e){return(a,i)=>{let s=[],r;if(Array.isArray(n)?(s=n,r=e):typeof n=="object"&&(r=n),typeof t=="function"&&!t.prototype?.validate)o(a,i,{name:"custom",validate:t,message:c=>`${c.property} is invalid`,constraints:s},r);else{let c=new t;o(a,i,{name:t.name,validate:(d,g)=>c.validate(d,g),message:d=>c.defaultMessage?c.defaultMessage(d):`${d.property} is invalid`,constraints:s},r)}}}function mt(t){let{name:n,target:e,propertyName:a,options:i,constraints:s,validator:r}=t,c;if(typeof r=="function"&&!r.prototype?.validate)c={name:n,validate:r,message:d=>`${d.property} is invalid`,...s?{constraints:s}:{}};else{let d=typeof r=="function"?new r:r;c={name:n,validate:(g,m)=>d.validate(g,m),message:g=>d.defaultMessage?d.defaultMessage(g):`${g.property} is invalid`,...s?{constraints:s}:{}}}o(e.prototype,a,c,i)}var O=class extends Error{constructor(e,a){super(e);this.errors=a;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}};async function b(t){if(t==null||typeof t!="object")return t;if(Array.isArray(t))return Promise.all(t.map(i=>b(i)));if(t instanceof Date)return t.toISOString();let n=t.constructor.prototype,e={},a=Object.keys(t);for(let i of a){let s=t[i],r=l.getMetadata(f.SERIALIZER,n,i);if(r){let c=new r;e[i]=await c.serialize(s)}else e[i]=await b(s)}return e}async function A(t,n){if(n==null)return n;if(Array.isArray(n))return await Promise.all(n.map(s=>A(t,s)));let e=new t,a=t.prototype;for(let i of Object.keys(n)){let s=n[i],r=l.getMetadata(f.DESERIALIZER,a,i);if(r){let g=new r;e[i]=await g.deserialize(s);continue}let c=l.getMetadata(f.POLYMORPHIC,a,i);if(c&&s!==null&&s!==void 0){let{discriminator:g,subTypes:m}=c;if(Array.isArray(s))e[i]=await Promise.all(s.map(async p=>{let u=m.find(h=>p[g]===h.name);return u?A(u.value,p):p}));else{let p=m.find(u=>s[g]===u.name);if(p){e[i]=await A(p.value,s);continue}}continue}let d=l.getMetadata(f.TYPE,a,i);if(d&&s!==null&&s!==void 0){let g=d();e[i]=await A(g,s);continue}e[i]=s}return e}async function I(t){let n=[];if(t==null||typeof t!="object")return n;if(Array.isArray(t)){for(let i=0;i0&&n.push({property:`[${i}]`,value:t[i],constraints:{},children:s})}return n}let e=Object.getPrototypeOf(t),a=l.getProperties(e);for(let i of a){let s=t[i],r={property:i,value:s,constraints:{}},c=l.getMetadata(f.IS_OPTIONAL,e,i),d=s==null;if(c&&d)continue;let g=l.getMetadata(f.VALIDATION,e,i)||[],m={value:s,object:t,property:i,constraints:[]};for(let u of g){m.constraints=u.constraints||[];let h=!0;if(u.each&&Array.isArray(s))for(let V of s){let $={...m,value:V};if(!await u.validate(V,$)){h=!1;break}}else h=await u.validate(s,m);if(!h){let V=typeof u.message=="function"?u.message(m):u.message;u.each&&(V=`each element in ${V}`),r.constraints[u.name]=V}}if(l.getMetadata("cereale:nested",e,i)&&s!==null&&s!==void 0){let u=await I(s);u.length>0&&(r.children=u)}(Object.keys(r.constraints).length>0||r.children)&&n.push(r)}return n}async function E(t){if(t==null)return t;let n=await I(t);if(n.length>0)throw new O("Validation failed during serialization",n);return b(t)}async function P(t){let n=await E(t);return JSON.stringify(n)}async function M(t,n){let e=await A(t,n),a=await I(e);if(a.length>0)throw new O("Validation failed during deserialization",a);return e}async function w(t,n){let e=JSON.parse(n);return M(t,e)}async function T(t,n){let e=await n.json();return M(t,e)}var C=class{static toPlain=E;static toJson=P;static toInstance=M;static fromJson=w;static fromRequest=T;static validate=I};return z(pt);})(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68287f1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3258 @@ +{ + "name": "cereale", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cereale", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^25.6.0", + "@vitest/coverage-v8": "^4.1.4", + "esbuild": "^0.25.0", + "eslint": "^10.2.1", + "globals": "^17.5.0", + "ts-node": "^10.9.2", + "typescript": "^6.0.2", + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.4" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rolldown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", + "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 6c8f7ec..76b930b 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,19 @@ ], "scripts": { "build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json", + "build:docs": "esbuild src/index.ts --bundle --format=iife --global-name=Cereale --minify --tsconfig=tsconfig.json --outfile=docs/cereale.js", "demo": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/example.ts", "type-check": "tsc --noEmit", "test": "vitest run", + "test:watch": "vitest", "test:coverage": "vitest run --coverage", "lint": "eslint .", "lint:fix": "eslint . --fix", - "prepublishOnly": "npm run build" + "verify": "npm run type-check && npm run lint && npm run test && npm run build", + "prepublishOnly": "npm run verify" + }, + "engines": { + "node": ">=20.0.0" }, "repository": { "type": "git", @@ -49,6 +55,7 @@ "@eslint/js": "^10.0.1", "@types/node": "^25.6.0", "@vitest/coverage-v8": "^4.1.4", + "esbuild": "^0.25.0", "eslint": "^10.2.1", "globals": "^17.5.0", "ts-node": "^10.9.2", diff --git a/src/decorators.test.ts b/src/decorators.test.ts index 7d22f3d..414abd6 100644 --- a/src/decorators.test.ts +++ b/src/decorators.test.ts @@ -238,7 +238,7 @@ describe('Additional Decorators', () => { t.val = 'wrong'; const errors = await JsonMapper.validate(t); expect(errors).toHaveLength(1); - expect(errors[0].constraints['CustomValidator']).toBe('val must be correct'); + expect(errors[0]!.constraints['CustomValidator']).toBe('val must be correct'); }); }); @@ -304,7 +304,7 @@ describe('Additional Decorators', () => { t.val = 5; const errors = await JsonMapper.validate(t); expect(errors).toHaveLength(1); - expect(errors[0].constraints['custom']).toBe('must be ten'); + expect(errors[0]!.constraints['custom']).toBe('must be ten'); }); it('should handle options as second argument', async () => { @@ -318,7 +318,7 @@ describe('Additional Decorators', () => { t.val = 2; const errors = await JsonMapper.validate(t); expect(errors).toHaveLength(1); - expect(errors[0].constraints['custom']).toBe('must be one'); + expect(errors[0]!.constraints['custom']).toBe('must be one'); }); }); @@ -339,10 +339,10 @@ describe('Additional Decorators', () => { name: string; } const json = '[{"name": "a"}, {"name": "b"}]'; - const items = await JsonMapper.fromJson(Item, json); + const items = (await JsonMapper.fromJson(Item, json)) as unknown as Item[]; expect(Array.isArray(items)).toBe(true); expect(items[0]).toBeInstanceOf(Item); - expect(items[0].name).toBe('a'); + expect(items[0]!.name).toBe('a'); }); it('should handle single polymorphic object', async () => { @@ -350,7 +350,7 @@ describe('Additional Decorators', () => { @IsString() type: string; } class Dog extends Animal { - type = 'dog'; + override type = 'dog'; @IsString() breed: string; } class Test { @@ -376,7 +376,7 @@ describe('Additional Decorators', () => { t.tags = ['a', 1 as any]; const errors = await JsonMapper.validate(t); expect(errors).toHaveLength(1); - expect(errors[0].constraints['isString']).toContain('each element'); + expect(errors[0]!.constraints['isString']).toContain('each element'); }); }); }); diff --git a/src/index.test.ts b/src/index.test.ts index 642b5f0..e33616d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -202,8 +202,8 @@ describe('JsonMapper', () => { const errors = await JsonMapper.validate(user); expect(errors).toHaveLength(1); - expect(errors[0].property).toBe('username'); - expect(errors[0].constraints).toHaveProperty('minLength'); + expect(errors[0]!.property).toBe('username'); + expect(errors[0]!.constraints).toHaveProperty('minLength'); }); it('should fail on invalid email', async () => { @@ -215,8 +215,8 @@ describe('JsonMapper', () => { const errors = await JsonMapper.validate(user); expect(errors).toHaveLength(1); - expect(errors[0].property).toBe('email'); - expect(errors[0].constraints).toHaveProperty('isEmail'); + expect(errors[0]!.property).toBe('email'); + expect(errors[0]!.constraints).toHaveProperty('isEmail'); }); it('should fail on invalid role (IsIn)', async () => { @@ -228,8 +228,8 @@ describe('JsonMapper', () => { const errors = await JsonMapper.validate(user); expect(errors).toHaveLength(1); - expect(errors[0].property).toBe('roles'); - expect(errors[0].constraints).toHaveProperty('isIn'); + expect(errors[0]!.property).toBe('roles'); + expect(errors[0]!.constraints).toHaveProperty('isIn'); }); it('should skip validation for null optional field', async () => { @@ -238,7 +238,7 @@ describe('JsonMapper', () => { user.email = 'john@example.com'; user.active = true; user.roles = ['user']; - user.age = undefined; // optional + delete user.age; // optional const errors = await JsonMapper.validate(user); expect(errors).toHaveLength(0); @@ -254,8 +254,8 @@ describe('JsonMapper', () => { const errors = await JsonMapper.validate(user); expect(errors).toHaveLength(1); - expect(errors[0].property).toBe('age'); - expect(errors[0].constraints).toHaveProperty('min'); + expect(errors[0]!.property).toBe('age'); + expect(errors[0]!.constraints).toHaveProperty('min'); }); }); }); diff --git a/src/utils.test.ts b/src/utils.test.ts index 4bf36d2..5544ce1 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -30,7 +30,7 @@ describe('Standalone Utility Functions', () => { user.age = '30' as any; const errors2 = await validate(user); expect(errors2).toHaveLength(1); - expect(errors2[0].property).toBe('age'); + expect(errors2[0]!.property).toBe('age'); }); it('should transform to plain object directly', async () => { diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 429b0e9..b0f4510 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -5,5 +5,6 @@ "moduleResolution": "Bundler", "outDir": "dist/cjs", "declaration": true - } + }, + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"] } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 2f46ede..dc4097c 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -4,5 +4,6 @@ "module": "NodeNext", "outDir": "dist/esm", "declaration": true - } + }, + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 61fcc79..ac24741 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,8 @@ "experimentalDecorators": true }, + // NOTE: test files are deliberately included here so that `npm run type-check` + // covers them. The two build configs exclude them (and the demo) from `dist`. "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts"] + "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..767a332 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + // Vitest 4 transpiles with oxc, which does not read `experimentalDecorators` + // out of tsconfig.json for files the tsconfig does not `include`. Without this + // the decorator syntax in the test files fails to parse and every suite is + // silently reported as "0 test". + oxc: { + decorator: { legacy: true }, + }, + test: { + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/example.ts', 'src/index.ts'], + }, + }, +}); From 6d04b439644479f33f3759a3b658b07d427336c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:41:10 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20fix:=20repair=20eight=20corr?= =?UTF-8?q?ectness=20defects=20in=20the=20mapping=20and=20validation=20eng?= =?UTF-8?q?ines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each fix is pinned by a regression test in src/regressions.test.ts describing the old behaviour. - Inheritance dropped base-class rules. A subclass re-decorating an inherited property registered its constraints against its own prototype, and validate() read only the nearest set, so everything the base declared was silently lost. Constraints are now merged down the whole prototype chain, base first, with genuinely identical rules collapsed so restating @IsString() on an override does not double-report. The library's own example.ts was affected: Media's @IsString() title had never been enforced for Book. - Circular references exhausted the heap. serialize() recursed forever, taking 8 GB and the process with it; it now tracks ancestors and raises a JsonMappingError naming the cause. Diamonds still serialize. validate() skips back-edges instead of recursing. - @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 raw value is now preserved, with { onUnknown: 'error' } and { fallback } to choose otherwise. - Custom @JsonSerialize serializers ran on null/undefined, crashing on any unset optional property. They now only see real values. - serialize() read obj.constructor.prototype, which throws for null-prototype objects; both engines now agree on Object.getPrototypeOf. - __proto__, constructor and prototype arriving in untrusted JSON were copied onto the instance, detaching it from its own class. They are dropped. - The "each element in ..." prefix was glued onto caller-supplied messages, and two rules sharing a name overwrote each other so only one failure surfaced. Also adds toInstanceArray/fromJsonArray, since toInstance and fromJson accept arrays at runtime but type the result as T, and reports a non-JSON request body in fromRequest as a JsonMappingError rather than a raw SyntaxError. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK --- src/decorators.ts | 63 +++++- src/metadata-storage.ts | 23 +++ src/regressions.test.ts | 447 ++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 416 +++++++++++++++++++++++++------------ 4 files changed, 811 insertions(+), 138 deletions(-) create mode 100644 src/regressions.test.ts diff --git a/src/decorators.ts b/src/decorators.ts index d8edc93..e3db72d 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -9,6 +9,7 @@ export const METADATA_KEYS = { DESERIALIZER: 'cereale:deserializer', POLYMORPHIC: 'cereale:polymorphic', IS_OPTIONAL: 'cereale:optional', + NESTED: 'cereale:nested', }; export interface ValidationArguments { @@ -29,6 +30,12 @@ export type ValidationConstraint = { message: string | ((args: ValidationArguments) => string); constraints?: any[]; each?: boolean; + /** + * True when the message came from the user via `ValidationOptions.message`. + * The engine only decorates default messages with the "each element in ..." prefix; + * a message the user wrote is reported exactly as written. + */ + hasCustomMessage?: boolean; }; export interface ValidatorConstraintInterface { @@ -55,9 +62,10 @@ function addValidation(target: any, propertyKey: string, constraint: ValidationC } if (options.message) { constraint.message = options.message; + constraint.hasCustomMessage = true; } } - + const constraints: ValidationConstraint[] = metadataStorage.getOwnMetadata(METADATA_KEYS.VALIDATION, target, propertyKey) || []; constraints.push(constraint); metadataStorage.defineMetadata(METADATA_KEYS.VALIDATION, constraints, target, propertyKey); @@ -98,14 +106,34 @@ export function JsonType(typeFunction: () => ClassConstructor) { }; } +export interface PolymorphicOptions { + /** + * What to do when the discriminator value matches no registered subtype. + * - `keep` (default): pass the raw value through untouched. + * - `error`: throw a {@link JsonMappingError} naming the unknown discriminator value. + */ + onUnknown?: 'keep' | 'error'; + /** Subtype to use when the discriminator matches nothing. Takes precedence over `onUnknown`. */ + fallback?: ClassConstructor; +} + /** - * @JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor, name: string }[]) + * @JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor, name: string }[], options?: PolymorphicOptions) * Defines polymorphic behavior for a property. */ -export function JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor, name: string }[]) { +export function JsonPolymorphic( + discriminator: string, + subTypes: { value: ClassConstructor, name: string }[], + options?: PolymorphicOptions +) { return (target: any, propertyKey: string) => { registerProperty(target, propertyKey); - metadataStorage.defineMetadata(METADATA_KEYS.POLYMORPHIC, { discriminator, subTypes }, target, propertyKey); + metadataStorage.defineMetadata( + METADATA_KEYS.POLYMORPHIC, + { discriminator, subTypes, onUnknown: options?.onUnknown ?? 'keep', fallback: options?.fallback }, + target, + propertyKey + ); }; } @@ -333,10 +361,16 @@ export function IsUrl(options?: ValidationOptions) { * @Matches(pattern: RegExp) */ export function Matches(pattern: RegExp, options?: ValidationOptions) { + // A `g` or `y` flag makes RegExp.prototype.test stateful: it advances lastIndex on a + // match and resumes from there on the next call, so validating the same value twice + // yields different answers. Validation must be a pure predicate, so drop those flags. + const stateless = pattern.flags.includes('g') || pattern.flags.includes('y') + ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, '')) + : pattern; return (target: any, propertyKey: string) => { addValidation(target, propertyKey, { name: 'matches', - validate: (v) => typeof v === 'string' && pattern.test(v), + validate: (v) => typeof v === 'string' && stateless.test(v), message: `${propertyKey} must match ${pattern} regular expression`, constraints: [pattern] }, options); @@ -439,13 +473,26 @@ export function IsDate(options?: ValidationOptions) { } /** - * @ValidateNested() + * @ValidateNested(options?: ValidationOptions) + * Recursively validates the value of this property. + * + * `{ each: true }` documents that the property holds a collection; nested validation + * already recurses into arrays, but passing `each` additionally asserts that the value + * really is an array. */ -export function ValidateNested() { +export function ValidateNested(options?: ValidationOptions) { return (target: any, propertyKey: string) => { registerProperty(target, propertyKey); // This is a marker for recursive validation - metadataStorage.defineMetadata('cereale:nested', true, target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.NESTED, true, target, propertyKey); + + if (options?.each) { + addValidation(target, propertyKey, { + name: 'nestedEach', + validate: (v) => Array.isArray(v), + message: `${propertyKey} must be an array` + }); + } }; } diff --git a/src/metadata-storage.ts b/src/metadata-storage.ts index bb413db..8c82a7f 100644 --- a/src/metadata-storage.ts +++ b/src/metadata-storage.ts @@ -63,6 +63,29 @@ export class MetadataStorage { return undefined; } + /** + * Collects a metadata value from every level of the prototype chain that defines one. + * + * Unlike {@link getMetadata}, which stops at the first (most derived) match, this returns + * every value found, ordered from the BASE class down to the most derived one. It exists + * for metadata that must accumulate across an inheritance chain rather than be overridden — + * validation constraints in particular, where a subclass re-decorating an inherited property + * must add to the base class's rules instead of silently replacing them. + */ + getMetadataChain(key: string, target: any, propertyKey?: string): any[] { + const chain: any[] = []; + let current = target; + while (current) { + const value = this.getOwnMetadata(key, current, propertyKey); + if (value !== undefined) { + // Walking derived -> base, so prepend to end up base-first. + chain.unshift(value); + } + current = Object.getPrototypeOf(current); + } + return chain; + } + /** * Gets metadata defined directly on the target. */ diff --git a/src/regressions.test.ts b/src/regressions.test.ts new file mode 100644 index 0000000..4f4ba5a --- /dev/null +++ b/src/regressions.test.ts @@ -0,0 +1,447 @@ +import { describe, it, expect } from 'vitest'; +import { + IsString, IsInt, Min, MinLength, Matches, IsIn, ValidateNested, JsonType, + JsonPolymorphic, JsonSerialize, JsonSerializer, JsonMappingError, + toInstance, toInstanceArray, toPlain, toJson, fromJson, fromJsonArray, fromRequest, validate, +} from './index.js'; + +/** + * Each block here pins down a defect that the engine used to have. The comment above the + * block describes the old, wrong behaviour. + */ +describe('regressions', () => { + describe('inheritance', () => { + // Was: a subclass re-decorating an inherited property registered its constraints on its + // own prototype, and the engine read only the nearest set — so every rule the base class + // declared was silently dropped. + it('merges validation constraints across the prototype chain', async () => { + class Base { + @MinLength(5) + name: string; + } + class Sub extends Base { + @IsString() + declare name: string; + } + + const s = new Sub(); + s.name = 'ab'; // satisfies Sub's @IsString, violates Base's @MinLength(5) + + const errors = await validate(s); + expect(errors).toHaveLength(1); + expect(errors[0]!.constraints).toHaveProperty('minLength'); + }); + + it('enforces base constraints that the subclass never restates', async () => { + abstract class Media { + @IsString() + title: string; + } + class Book extends Media { + @IsString() + author: string; + } + + const b = new Book(); + b.title = 42 as any; + b.author = 'Fitzgerald'; + + const errors = await validate(b); + expect(errors.map(e => e.property)).toContain('title'); + }); + + it('does not report an identical inherited rule twice', async () => { + class Base { + @IsString() + type: string; + } + class Sub extends Base { + @IsString() + declare type: string; + } + + const s = new Sub(); + s.type = 1 as any; + + const errors = await validate(s); + expect(errors).toHaveLength(1); + expect(Object.keys(errors[0]!.constraints)).toEqual(['isString']); + }); + }); + + describe('cycles', () => { + // Was: serialize() recursed forever on a cycle, exhausting an 8 GB heap and killing the + // process. A clear error beats an OOM. + it('reports a circular reference instead of exhausting the heap', async () => { + class Node { + @IsString() + name: string; + next?: any; + } + const a = new Node(); + a.name = 'a'; + a.next = a; + + await expect(toPlain(a)).rejects.toThrow(JsonMappingError); + await expect(toPlain(a)).rejects.toThrow(/Circular reference/); + }); + + it('still serializes a diamond, where one object is referenced twice', async () => { + class Leaf { + @IsString() + id: string; + } + class Holder { + left: Leaf; + right: Leaf; + } + + const shared = new Leaf(); + shared.id = 'shared'; + const h = new Holder(); + h.left = shared; + h.right = shared; + + const plain = await toPlain(h); + expect(plain).toEqual({ left: { id: 'shared' }, right: { id: 'shared' } }); + }); + + it('terminates when validating a cyclic @ValidateNested graph', async () => { + class Person { + @IsString() + name: string; + + @ValidateNested() + friend?: Person; + } + + const a = new Person(); + a.name = 'a'; + const b = new Person(); + b.name = 'b'; + a.friend = b; + b.friend = a; + + await expect(validate(a)).resolves.toEqual([]); + }); + }); + + describe('messages', () => { + // Was: the "each element in ..." prefix was glued onto every message, including ones the + // caller wrote, producing "each element in tags must all be strings". + it('reports a caller-supplied message verbatim under each:true', async () => { + class T { + @IsString({ each: true, message: 'tags must all be strings' }) + tags: any[]; + } + const t = new T(); + t.tags = [1]; + + const errors = await validate(t); + expect(errors[0]!.constraints['isString']).toBe('tags must all be strings'); + }); + + it('still prefixes the library default message under each:true', async () => { + class T { + @IsString({ each: true }) + tags: any[]; + } + const t = new T(); + t.tags = [1]; + + const errors = await validate(t); + expect(errors[0]!.constraints['isString']).toContain('each element in'); + }); + + // Was: two constraints sharing a name overwrote each other in the error record, so only + // the last failure was ever reported. + it('keeps every failure when two rules share a name', async () => { + class T { + @Min(10) + @Min(5) + n: number; + } + const t = new T(); + t.n = 1; + + const errors = await validate(t); + const messages = Object.values(errors[0]!.constraints); + expect(messages).toHaveLength(2); + expect(messages).toEqual(expect.arrayContaining([ + 'n must be at least 5', + 'n must be at least 10', + ])); + }); + }); + + describe('@Matches', () => { + // Was: a /g regex kept its lastIndex between calls, so validating the same value twice + // gave different answers — the second call spuriously failed. + it('is stateless when the pattern carries a g flag', async () => { + class T { + @Matches(/^[a-z]+$/g) + v: string; + } + const t = new T(); + t.v = 'abc'; + + expect(await validate(t)).toHaveLength(0); + expect(await validate(t)).toHaveLength(0); + expect(await validate(t)).toHaveLength(0); + }); + + it('is stateless when the pattern carries a y flag', async () => { + class T { + @Matches(/^[a-z]+$/y) + v: string; + } + const t = new T(); + t.v = 'abc'; + + expect(await validate(t)).toHaveLength(0); + expect(await validate(t)).toHaveLength(0); + }); + }); + + describe('@JsonPolymorphic', () => { + abstract class Animal { + @IsString() + type: string; + } + class Dog extends Animal { + @IsString() + breed: string; + } + + // Was: when the discriminator matched no subtype, the single-object branch fell through + // without assigning anything, so the property came back `undefined` and the caller's data + // vanished without a word. + it('keeps the raw value when the discriminator matches nothing', async () => { + class Holder { + @JsonPolymorphic('type', [{ value: Dog, name: 'dog' }]) + pet: Animal; + } + + const h = await toInstance(Holder, { pet: { type: 'cat', sound: 'meow' } }); + expect(h.pet).toBeDefined(); + expect(h.pet).toEqual({ type: 'cat', sound: 'meow' }); + }); + + it('can be told to reject an unknown discriminator instead', async () => { + class Holder { + @JsonPolymorphic('type', [{ value: Dog, name: 'dog' }], { onUnknown: 'error' }) + pet: Animal; + } + + await expect(toInstance(Holder, { pet: { type: 'cat' } })).rejects.toThrow(JsonMappingError); + await expect(toInstance(Holder, { pet: { type: 'cat' } })).rejects.toThrow(/Unknown discriminator/); + }); + + it('can fall back to a default subtype', async () => { + class Unknown extends Animal { + @IsString() + override type = 'unknown'; + } + class Holder { + @JsonPolymorphic('type', [{ value: Dog, name: 'dog' }], { fallback: Unknown }) + pet: Animal; + } + + const h = await toInstance(Holder, { pet: { type: 'cat' } }); + expect(h.pet).toBeInstanceOf(Unknown); + }); + + it('keeps unmatched entries inside an array', async () => { + class Holder { + @JsonPolymorphic('type', [{ value: Dog, name: 'dog' }]) + pets: Animal[]; + } + + const h = await toInstance(Holder, { + pets: [{ type: 'dog', breed: 'Lab' }, { type: 'cat', sound: 'meow' }], + }); + expect(h.pets[0]).toBeInstanceOf(Dog); + expect(h.pets[1]).toEqual({ type: 'cat', sound: 'meow' }); + }); + }); + + describe('prototype handling', () => { + // Was: serialize() read `obj.constructor.prototype`, which throws for an object created + // with a null prototype because it has no `constructor`. + it('serializes a null-prototype object', async () => { + const o = Object.create(null); + o.a = 1; + o.b = { c: 2 }; + + await expect(toPlain(o)).resolves.toEqual({ a: 1, b: { c: 2 } }); + }); + + // Was: `__proto__` arriving in a JSON body was copied straight onto the instance, which + // swaps the instance's prototype and detaches it from its own class. + it('drops __proto__ from untrusted input', async () => { + class Dto { + @IsString() + name: string; + } + + const malicious = JSON.parse('{"name":"x","__proto__":{"polluted":"yes"}}'); + const dto = await toInstance(Dto, malicious); + + expect(dto).toBeInstanceOf(Dto); + expect(Object.getPrototypeOf(dto)).toBe(Dto.prototype); + expect(({} as any).polluted).toBeUndefined(); + }); + + it('drops constructor and prototype keys from untrusted input', async () => { + class Dto { + @IsString() + name: string; + } + + const dto = await toInstance(Dto, JSON.parse('{"name":"x","constructor":1,"prototype":2}')); + expect(dto.constructor).toBe(Dto); + expect((dto as any).prototype).toBeUndefined(); + }); + }); + + describe('custom serializers', () => { + // Was: a @JsonSerialize serializer was invoked even when the property was null or + // undefined, so any serializer that touched the value crashed on an unset optional field. + it('is skipped for an unset optional property', async () => { + class IsoDate implements JsonSerializer { + serialize(value: Date): string { + return value.toISOString(); + } + } + class T { + @JsonSerialize(IsoDate) + when?: Date | undefined; + + @IsString() + other: string; + } + + const t = new T(); + t.other = 'x'; + t.when = undefined; + + await expect(toPlain(t)).resolves.toEqual({ when: undefined, other: 'x' }); + }); + + it('still runs for a property that has a value', async () => { + class IsoDate implements JsonSerializer { + serialize(value: Date): string { + return value.toISOString().slice(0, 10); + } + } + class T { + @JsonSerialize(IsoDate) + when: Date; + } + + const t = new T(); + t.when = new Date('1925-04-10T00:00:00Z'); + + await expect(toJson(t)).resolves.toBe('{"when":"1925-04-10"}'); + }); + }); + + describe('array entry points', () => { + class Item { + @IsString() + name: string; + } + + // Was: `toInstance`/`fromJson` accepted arrays at runtime but typed the result as `T`, + // so consumers had to cast to reach the elements. + it('toInstanceArray returns a correctly typed array', async () => { + const items = await toInstanceArray(Item, [{ name: 'a' }, { name: 'b' }]); + expect(items).toHaveLength(2); + expect(items[0]).toBeInstanceOf(Item); + expect(items[0]!.name).toBe('a'); + }); + + it('fromJsonArray parses and validates a JSON array', async () => { + const items = await fromJsonArray(Item, '[{"name":"a"}]'); + expect(items[0]!.name).toBe('a'); + }); + + it('toInstanceArray rejects a non-array payload', async () => { + await expect(toInstanceArray(Item, {} as any)).rejects.toThrow(JsonMappingError); + }); + + it('fromJson still accepts an array for backwards compatibility', async () => { + const items = (await fromJson(Item, '[{"name":"a"}]')) as unknown as Item[]; + expect(Array.isArray(items)).toBe(true); + }); + }); + + describe('fromRequest', () => { + it('reports a non-JSON body as a mapping error', async () => { + class Dto { + @IsString() + name: string; + } + const request = new Request('https://example.com', { method: 'POST', body: 'not json' }); + + await expect(fromRequest(Dto, request)).rejects.toThrow(JsonMappingError); + await expect( + fromRequest(Dto, new Request('https://example.com', { method: 'POST', body: '' })) + ).rejects.toThrow(/not valid JSON/); + }); + }); + + describe('@ValidateNested', () => { + it('accepts the documented { each: true } option', async () => { + class Item { + @IsInt() + @Min(1) + qty: number; + } + class Order { + @ValidateNested({ each: true }) + @JsonType(() => Item) + items: Item[]; + } + + const bad = new Item(); + bad.qty = -5; + const o = new Order(); + o.items = [bad]; + + const errors = await validate(o); + expect(errors).toHaveLength(1); + expect(errors[0]!.children?.[0]?.children?.[0]?.property).toBe('qty'); + }); + + it('{ each: true } asserts the value really is an array', async () => { + class Item { + @IsInt() + qty: number; + } + class Order { + @ValidateNested({ each: true }) + items: Item[]; + } + + const o = new Order(); + o.items = 'nope' as any; + + const errors = await validate(o); + expect(errors[0]!.constraints).toHaveProperty('nestedEach'); + }); + }); + + describe('@IsIn with each:true', () => { + it('rejects a non-array value rather than passing it through', async () => { + class T { + @IsIn(['a', 'b'], { each: true }) + tags: any; + } + const t = new T(); + t.tags = 'not-allowed'; + + expect(await validate(t)).toHaveLength(1); + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index e79dd29..d72fe54 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,55 +20,106 @@ export class JsonValidationError extends Error { } } +/** + * Thrown when a value cannot be mapped at all — as opposed to mapping fine but failing + * validation, which raises {@link JsonValidationError}. + */ +export class JsonMappingError extends Error { + constructor(message: string) { + super(message); + this.name = 'JsonMappingError'; + } +} + +/** + * Keys that must never be copied from untrusted input onto an instance. Assigning + * `__proto__` swaps an object's prototype, and `constructor` / `prototype` are the usual + * next steps in a pollution chain. This library exists to parse request bodies, so the + * transform layer drops them rather than trusting callers to sanitise first. + */ +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + // --- Internal Engine --- -async function serialize(obj: any): Promise { +/** + * Resolves the metadata lookup target for a value. + * + * `Object.getPrototypeOf` rather than `obj.constructor.prototype`: the latter throws on + * null-prototype objects (which have no `constructor`) and lies for instances whose + * `constructor` property has been overwritten. + */ +function prototypeOf(obj: any): any { + return Object.getPrototypeOf(obj) ?? undefined; +} + +async function serialize(obj: any, ancestors: Set): Promise { if (obj === null || obj === undefined || typeof obj !== 'object') { return obj; } - if (Array.isArray(obj)) { - return Promise.all(obj.map(item => serialize(item))); - } - if (obj instanceof Date) { return obj.toISOString(); } - const target = obj.constructor.prototype; - - const result: any = {}; - const allKeys = Object.keys(obj); - - for (const key of allKeys) { - const value = obj[key]; - - // Check for custom serializer - const serializerCls = metadataStorage.getMetadata(METADATA_KEYS.SERIALIZER, target, key); - if (serializerCls) { - const serializer = new serializerCls(); - result[key] = await serializer.serialize(value); - } else { - result[key] = await serialize(value); - } + if (ancestors.has(obj)) { + throw new JsonMappingError( + 'Circular reference detected during serialization. Break the cycle with @JsonIgnore() ' + + 'on the back-reference, or supply a @JsonSerialize() serializer for that property.' + ); } - return result; + ancestors.add(obj); + try { + if (Array.isArray(obj)) { + const out: any[] = []; + for (const item of obj) { + out.push(await serialize(item, ancestors)); + } + return out; + } + + const target = prototypeOf(obj); + + const result: any = {}; + for (const key of Object.keys(obj)) { + const value = obj[key]; + + // Custom serializers only see real values. Handing a serializer `undefined` for a + // property that was simply never set turns an optional field into a crash. + const serializerCls = target ? metadataStorage.getMetadata(METADATA_KEYS.SERIALIZER, target, key) : undefined; + if (serializerCls && value !== null && value !== undefined) { + const serializer = new serializerCls(); + result[key] = await serializer.serialize(value); + } else { + result[key] = await serialize(value, ancestors); + } + } + + return result; + } finally { + // Only direct ancestors count as a cycle; the same object appearing twice in + // sibling positions (a diamond) is perfectly serializable. + ancestors.delete(obj); + } } async function deserialize(clazz: ClassConstructor, plain: any): Promise { if (plain === null || plain === undefined) return plain; - + if (Array.isArray(plain)) { const results = await Promise.all(plain.map(item => deserialize(clazz, item))); return results as any; } + if (typeof plain !== 'object') return plain; + const instance = new clazz(); const target = clazz.prototype; // Copy all properties from plain to instance for (const key of Object.keys(plain)) { + if (FORBIDDEN_KEYS.has(key)) continue; + const value = plain[key]; // Custom Deserializer @@ -82,19 +133,26 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise { - const subTypeInfo = subTypes.find((s: any) => item[discriminator] === s.name); - return subTypeInfo ? deserialize(subTypeInfo.value, item) : item; - })) as any; - } else { - const subTypeInfo = subTypes.find((s: any) => value[discriminator] === s.name); - if (subTypeInfo) { - instance[key as keyof T] = await deserialize(subTypeInfo.value, value); - continue; + const { discriminator, subTypes, onUnknown, fallback } = poly; + + const resolve = async (item: any): Promise => { + if (item === null || item === undefined || typeof item !== 'object') return item; + const subTypeInfo = subTypes.find((s: any) => item[discriminator] === s.name); + if (subTypeInfo) return deserialize(subTypeInfo.value, item); + if (fallback) return deserialize(fallback, item); + if (onUnknown === 'error') { + throw new JsonMappingError( + `Unknown discriminator value ${JSON.stringify(item[discriminator])} for property ` + + `"${key}". Known values: ${subTypes.map((s: any) => JSON.stringify(s.name)).join(', ')}.` + ); } - } + // Preserve the raw value. Dropping it silently loses data the caller sent. + return item; + }; + + instance[key as keyof T] = Array.isArray(value) + ? (await Promise.all(value.map(resolve))) as any + : await resolve(value); continue; } @@ -112,6 +170,157 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise(); + + for (const level of levels) { + for (const constraint of level) { + if (typeof constraint.message === 'string') { + const identity = `${constraint.name}|${String(constraint.constraints)}|${constraint.message}`; + if (seen.has(identity)) continue; + seen.add(identity); + } + merged.push(constraint); + } + } + + return merged; +} + +/** + * Records a failure without letting a later constraint overwrite an earlier one that happens + * to share a name (two `@Min` rules, or a rule inherited and re-declared). + */ +function recordFailure(constraints: { [key: string]: string }, name: string, message: string) { + if (!(name in constraints)) { + constraints[name] = message; + return; + } + let suffix = 2; + while (`${name}_${suffix}` in constraints) suffix++; + constraints[`${name}_${suffix}`] = message; +} + +async function validateInternal(obj: any, ancestors: Set): Promise { + const errors: ValidationError[] = []; + if (obj === null || obj === undefined || typeof obj !== 'object') return errors; + + // A cycle has already been validated further up the stack; re-entering it would never + // terminate. Diamonds are still validated on each distinct path. + if (ancestors.has(obj)) return errors; + ancestors.add(obj); + + try { + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + const childErrors = await validateInternal(obj[i], ancestors); + if (childErrors.length > 0) { + errors.push({ + property: `[${i}]`, + value: obj[i], + constraints: {}, + children: childErrors + }); + } + } + return errors; + } + + const target = prototypeOf(obj); + if (!target) return errors; + + const properties: string[] = metadataStorage.getProperties(target); + + for (const key of properties) { + const value = obj[key]; + const propertyErrors: ValidationError = { + property: key, + value: value, + constraints: {} + }; + + // Handle IsOptional + const isOptional = metadataStorage.getMetadata(METADATA_KEYS.IS_OPTIONAL, target, key); + const isNullOrUndefined = value === null || value === undefined; + + if (isOptional && isNullOrUndefined) { + continue; + } + + // Check validation constraints + const constraints = collectConstraints(target, key); + const validationArgs: ValidationArguments = { + value: value, + object: obj, + property: key, + constraints: [] + }; + + for (const constraint of constraints) { + validationArgs.constraints = constraint.constraints || []; + + let isValid = true; + if (constraint.each && Array.isArray(value)) { + for (const item of value) { + const itemArgs = { ...validationArgs, value: item }; + if (!(await constraint.validate(item, itemArgs))) { + isValid = false; + break; + } + } + } else { + isValid = await constraint.validate(value, validationArgs); + } + + if (!isValid) { + let message = typeof constraint.message === 'function' + ? constraint.message(validationArgs) + : constraint.message; + + // Only decorate the library's own default wording. A message the caller wrote + // is reported verbatim — prefixing it produced sentences like + // "each element in tags must all be strings". + if (constraint.each && !constraint.hasCustomMessage) { + message = `each element in ${message}`; + } + + recordFailure(propertyErrors.constraints, constraint.name, message); + } + } + + // Recursive validation + const isNested = metadataStorage.getMetadata(METADATA_KEYS.NESTED, target, key); + if (isNested && value !== null && value !== undefined) { + const nestedErrors = await validateInternal(value, ancestors); + if (nestedErrors.length > 0) { + propertyErrors.children = nestedErrors; + } + } + + if (Object.keys(propertyErrors.constraints).length > 0 || propertyErrors.children) { + errors.push(propertyErrors); + } + } + + return errors; + } finally { + ancestors.delete(obj); + } +} + // --- Public API Functions --- /** @@ -120,96 +329,7 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise { - const errors: ValidationError[] = []; - if (obj === null || obj === undefined || typeof obj !== 'object') return errors; - - if (Array.isArray(obj)) { - for (let i = 0; i < obj.length; i++) { - const childErrors = await validate(obj[i]); - if (childErrors.length > 0) { - errors.push({ - property: `[${i}]`, - value: obj[i], - constraints: {}, - children: childErrors - }); - } - } - return errors; - } - - const target = Object.getPrototypeOf(obj); - const properties: string[] = metadataStorage.getProperties(target); - - for (const key of properties) { - const value = obj[key]; - const propertyErrors: ValidationError = { - property: key, - value: value, - constraints: {} - }; - - // Handle IsOptional - const isOptional = metadataStorage.getMetadata(METADATA_KEYS.IS_OPTIONAL, target, key); - const isNullOrUndefined = value === null || value === undefined; - - if (isOptional && isNullOrUndefined) { - continue; - } - - // Check validation constraints - const constraints: ValidationConstraint[] = metadataStorage.getMetadata(METADATA_KEYS.VALIDATION, target, key) || []; - const validationArgs: ValidationArguments = { - value: value, - object: obj, - property: key, - constraints: [] - }; - - for (const constraint of constraints) { - validationArgs.constraints = constraint.constraints || []; - - let isValid = true; - if (constraint.each && Array.isArray(value)) { - for (const item of value) { - const itemArgs = { ...validationArgs, value: item }; - if (!(await constraint.validate(item, itemArgs))) { - isValid = false; - break; - } - } - } else { - isValid = await constraint.validate(value, validationArgs); - } - - if (!isValid) { - let message = typeof constraint.message === 'function' - ? constraint.message(validationArgs) - : constraint.message; - - if (constraint.each) { - message = `each element in ${message}`; - } - - propertyErrors.constraints[constraint.name] = message; - } - } - - // Recursive validation - const isNested = metadataStorage.getMetadata('cereale:nested', target, key); - if (isNested && value !== null && value !== undefined) { - const nestedErrors = await validate(value); - if (nestedErrors.length > 0) { - propertyErrors.children = nestedErrors; - } - } - - if (Object.keys(propertyErrors.constraints).length > 0 || propertyErrors.children) { - errors.push(propertyErrors); - } - } - - return errors; + return validateInternal(obj, new Set()); } /** @@ -219,13 +339,13 @@ export async function validate(obj: any): Promise { */ export async function toPlain(obj: T): Promise { if (obj === null || obj === undefined) return obj; - + const errors = await validate(obj); if (errors.length > 0) { throw new JsonValidationError('Validation failed during serialization', errors); } - return serialize(obj); + return serialize(obj, new Set()); } /** @@ -246,15 +366,32 @@ export async function toJson(obj: T): Promise { */ export async function toInstance(clazz: ClassConstructor, plain: any): Promise { const instance = await deserialize(clazz, plain); - + const errors = await validate(instance); if (errors.length > 0) { throw new JsonValidationError('Validation failed during deserialization', errors); } - + return instance; } +/** + * Converts an array of plain objects to an array of class instances with validation. + * + * `toInstance` also accepts arrays at runtime, but its return type says `T`. Use this when + * the payload is a collection so the static type matches what you actually get back. + * + * @param clazz The class constructor + * @param plain The array of plain objects to transform + * @returns Validated array of class instances + */ +export async function toInstanceArray(clazz: ClassConstructor, plain: any[]): Promise { + if (!Array.isArray(plain)) { + throw new JsonMappingError(`Expected an array to map to ${clazz.name}[], received ${typeof plain}.`); + } + return (await toInstance(clazz, plain)) as unknown as T[]; +} + /** * Parses a JSON string to a class instance with validation. * @param clazz The class constructor @@ -266,6 +403,16 @@ export async function fromJson(clazz: ClassConstructor, json: string): Pro return toInstance(clazz, plain); } +/** + * Parses a JSON string containing an array into validated class instances. + * @param clazz The class constructor + * @param json JSON string holding an array + * @returns Validated array of class instances + */ +export async function fromJsonArray(clazz: ClassConstructor, json: string): Promise { + return toInstanceArray(clazz, JSON.parse(json)); +} + /** * Helper for Fetch-based frameworks (Next.js, Hono, etc.) * Extracts JSON from a Request and transforms it to a validated instance. @@ -274,7 +421,14 @@ export async function fromJson(clazz: ClassConstructor, json: string): Pro * @returns Validated class instance */ export async function fromRequest(clazz: ClassConstructor, request: Request): Promise { - const plain = await request.json(); + let plain: any; + try { + plain = await request.json(); + } catch (error) { + throw new JsonMappingError( + `Request body is not valid JSON: ${error instanceof Error ? error.message : String(error)}` + ); + } return toInstance(clazz, plain); } @@ -285,7 +439,9 @@ export class JsonMapper { static toPlain = toPlain; static toJson = toJson; static toInstance = toInstance; + static toInstanceArray = toInstanceArray; static fromJson = fromJson; + static fromJsonArray = fromJsonArray; static fromRequest = fromRequest; static validate = validate; } From 666762a146d6b174419a00917dfea3a5377abb00 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:46:18 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20field-name=20mapp?= =?UTF-8?q?ing,=20access=20control=20and=20transform=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A library whose headline feature is "JSON mapping" could not map a name: there was no way to read {"first_name": ...} into firstName, no way to keep a password out of the response, and no way to parse a payload without validating it. Name mapping - @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) for properties with no explicit name. Acronyms split where a reader expects: parseHTTPResponse -> parse_http_response Access control - @JsonIgnore() excluded both ways - @JsonWriteOnly() accepted from input, never echoed back (passwords) - @JsonReadOnly() serialized, never settable by a client (server-owned ids) Blocked names are dropped explicitly rather than falling through to the unknown key path, which would otherwise have copied a rejected id straight back on under the default policy. Transform options, per call or globally via configure() - validate: false to map without validating, for lenient parsing - unknownKeys: 'allow' | 'strip' | 'error' - namingStrategy Error ergonomics — the nested ValidationError tree was hard to turn into an HTTP 400 body. flattenErrors() yields {"items[0].qty": ["qty must be at least 1"]}, plus formatErrors() and collectErrorMessages(). Adds validateOrReject(). All defaults preserve existing behaviour; the 68 prior tests pass unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK --- src/config.ts | 75 ++++++++ src/decorators.ts | 93 ++++++++++ src/errors.ts | 57 ++++++ src/index.ts | 3 + src/mapping.test.ts | 444 ++++++++++++++++++++++++++++++++++++++++++++ src/naming.ts | 74 ++++++++ src/utils.ts | 281 +++++++++++++++++++++++----- 7 files changed, 977 insertions(+), 50 deletions(-) create mode 100644 src/config.ts create mode 100644 src/errors.ts create mode 100644 src/mapping.test.ts create mode 100644 src/naming.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e6d5df4 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,75 @@ +import { NamingStrategy } from './naming.js'; + +/** + * How an incoming key that maps to no known property should be treated. + * + * - `allow` (default): copy it onto the instance untouched, preserving the previous behaviour. + * - `strip`: drop it, so instances only ever carry declared properties. + * - `error`: reject the payload with a {@link JsonMappingError}. + */ +export type UnknownKeyPolicy = 'allow' | 'strip' | 'error'; + +export interface TransformOptions { + /** + * Validate the result and throw {@link JsonValidationError} on failure. + * + * Defaults to `true`, matching the behaviour of every previous release. Set it to `false` + * to map without validating — useful when you want to inspect a partially-valid payload, + * or when validation happens elsewhere in your stack. + */ + validate?: boolean; + + /** + * Naming convention used on the JSON side for properties without an explicit + * `@JsonProperty`. Defaults to `identity` (property names are used as-is). + */ + namingStrategy?: NamingStrategy; + + /** What to do with incoming keys that match no declared property. Deserialization only. */ + unknownKeys?: UnknownKeyPolicy; +} + +/** Options that can be set once for the whole application via {@link configure}. */ +export type GlobalOptions = Pick; + +const DEFAULTS: Required = { + namingStrategy: 'identity', + unknownKeys: 'allow', + validate: true, +}; + +let globalOptions: Required = { ...DEFAULTS }; + +/** + * Sets library-wide defaults, so an application that consistently speaks `snake_case` does + * not have to repeat itself at every call site. + * + * ```ts + * configure({ namingStrategy: 'snake_case', unknownKeys: 'strip' }); + * ``` + * + * Per-call options always take precedence over these. + */ +export function configure(options: GlobalOptions): void { + globalOptions = { ...globalOptions, ...options }; +} + +/** Returns the current library-wide defaults. */ +export function getConfig(): Required { + return { ...globalOptions }; +} + +/** Restores the library-wide defaults to their original values. */ +export function resetConfig(): void { + globalOptions = { ...DEFAULTS }; +} + +/** Merges per-call options over the library-wide defaults. */ +export function resolveOptions(options?: TransformOptions): Required { + if (!options) return globalOptions; + return { + namingStrategy: options.namingStrategy ?? globalOptions.namingStrategy, + unknownKeys: options.unknownKeys ?? globalOptions.unknownKeys, + validate: options.validate ?? globalOptions.validate, + }; +} diff --git a/src/decorators.ts b/src/decorators.ts index e3db72d..2215226 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -10,8 +10,21 @@ export const METADATA_KEYS = { POLYMORPHIC: 'cereale:polymorphic', IS_OPTIONAL: 'cereale:optional', NESTED: 'cereale:nested', + NAME: 'cereale:name', + ALIASES: 'cereale:aliases', + ACCESS: 'cereale:access', }; +/** + * Which directions a property participates in. + * + * - `readwrite` (default): mapped both ways. + * - `readonly`: written to JSON, never populated from incoming JSON (server-assigned ids). + * - `writeonly`: populated from incoming JSON, never written back out (passwords). + * - `none`: ignored entirely. + */ +export type PropertyAccess = 'readwrite' | 'readonly' | 'writeonly' | 'none'; + export interface ValidationArguments { value: any; object: any; @@ -73,6 +86,86 @@ function addValidation(target: any, propertyKey: string, constraint: ValidationC // --- Mapping Decorators --- +/** + * @JsonProperty(name: string) + * Maps this property to a different name in JSON, in both directions. + * + * ```ts + * class User { + * @JsonProperty('first_name') + * firstName: string; // <-> {"first_name": "Ada"} + * } + * ``` + * + * An explicit name always wins over the active naming strategy. + */ +export function JsonProperty(name: string) { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.NAME, name, target, propertyKey); + }; +} + +/** + * @JsonAlias(...names: string[]) + * Additional names accepted for this property when reading JSON. + * + * Aliases are input-only — output always uses the canonical name — which makes them the + * tool for accepting a renamed field from older clients without emitting it. + * + * ```ts + * class User { + * @JsonProperty('surname') + * @JsonAlias('last_name', 'lastName') + * surname: string; + * } + * ``` + */ +export function JsonAlias(...names: string[]) { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + const existing: string[] = metadataStorage.getOwnMetadata(METADATA_KEYS.ALIASES, target, propertyKey) || []; + metadataStorage.defineMetadata(METADATA_KEYS.ALIASES, [...existing, ...names], target, propertyKey); + }; +} + +/** + * @JsonIgnore() + * Excludes this property from mapping in both directions. + */ +export function JsonIgnore() { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.ACCESS, 'none', target, propertyKey); + }; +} + +/** + * @JsonReadOnly() + * Serialized to JSON, but never populated from incoming JSON. + * + * For server-owned fields — ids, timestamps — that a client must not be able to set. + */ +export function JsonReadOnly() { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.ACCESS, 'readonly', target, propertyKey); + }; +} + +/** + * @JsonWriteOnly() + * Populated from incoming JSON, but never serialized back out. + * + * For secrets — passwords, tokens — that you accept but must never echo. + */ +export function JsonWriteOnly() { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.ACCESS, 'writeonly', target, propertyKey); + }; +} + /** * @JsonSerialize(serializer: ClassConstructor) * Custom serializer decorator. diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..a17aa9d --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,57 @@ +import type { ValidationError } from './utils.js'; + +/** + * Flattens the nested {@link ValidationError} tree into a flat map of dotted paths to + * messages — the shape you actually want when turning a failure into an HTTP 400 body. + * + * ```ts + * flattenErrors(errors); + * // { + * // "name": ["name must be a string"], + * // "items[0].qty": ["qty must be at least 1"] + * // } + * ``` + */ +export function flattenErrors(errors: ValidationError[]): Record { + const flat: Record = {}; + + const walk = (nodes: ValidationError[], prefix: string) => { + for (const node of nodes) { + // Array indices read as `items[0]`, named properties as `order.total`. + const path = node.property.startsWith('[') + ? `${prefix}${node.property}` + : prefix ? `${prefix}.${node.property}` : node.property; + + const messages = Object.values(node.constraints); + if (messages.length > 0) { + (flat[path] ??= []).push(...messages); + } + + if (node.children?.length) { + walk(node.children, path); + } + } + }; + + walk(errors, ''); + return flat; +} + +/** + * Renders the error tree as human-readable lines, one per failed rule. + * + * Intended for logs and CLI output; use {@link flattenErrors} when the destination is JSON. + */ +export function formatErrors(errors: ValidationError[]): string { + const flat = flattenErrors(errors); + return Object.entries(flat) + .flatMap(([path, messages]) => messages.map(message => `${path}: ${message}`)) + .join('\n'); +} + +/** + * Collects every message in the tree, discarding paths. + */ +export function collectErrorMessages(errors: ValidationError[]): string[] { + return Object.values(flattenErrors(errors)).flat(); +} diff --git a/src/index.ts b/src/index.ts index edf10ec..d8c295f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ export * from './interfaces.js'; +export * from './naming.js'; +export * from './config.js'; export * from './decorators.js'; +export * from './errors.js'; export * from './utils.js'; diff --git a/src/mapping.test.ts b/src/mapping.test.ts new file mode 100644 index 0000000..3a4025d --- /dev/null +++ b/src/mapping.test.ts @@ -0,0 +1,444 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { + IsString, IsInt, IsOptional, ValidateNested, Min, + JsonProperty, JsonAlias, JsonIgnore, JsonReadOnly, JsonWriteOnly, JsonType, + JsonMappingError, JsonValidationError, + toPlain, toJson, toInstance, fromJson, validate, validateOrReject, + configure, resetConfig, getConfig, + flattenErrors, formatErrors, collectErrorMessages, + resolveNamingStrategy, +} from './index.js'; + +afterEach(() => resetConfig()); + +describe('@JsonProperty', () => { + class User { + @JsonProperty('first_name') + @IsString() + firstName: string; + + @JsonProperty('last_name') + @IsString() + lastName: string; + } + + it('renames on the way out', async () => { + const u = new User(); + u.firstName = 'Ada'; + u.lastName = 'Lovelace'; + + await expect(toPlain(u)).resolves.toEqual({ first_name: 'Ada', last_name: 'Lovelace' }); + }); + + it('renames on the way in', async () => { + const u = await fromJson(User, '{"first_name":"Ada","last_name":"Lovelace"}'); + expect(u.firstName).toBe('Ada'); + expect(u.lastName).toBe('Lovelace'); + }); + + it('round-trips', async () => { + const json = '{"first_name":"Ada","last_name":"Lovelace"}'; + expect(await toJson(await fromJson(User, json))).toBe(json); + }); + + it('no longer accepts the raw property name once renamed', async () => { + const u = await toInstance( + User, + { firstName: 'Ada', last_name: 'L' }, + { unknownKeys: 'strip', validate: false } + ); + expect(u.firstName).toBeUndefined(); + expect(u.lastName).toBe('L'); + }); + + it('rejects two properties claiming the same JSON name', async () => { + class Clash { + @JsonProperty('name') + a: string; + + @JsonProperty('name') + b: string; + } + + await expect(toInstance(Clash, { name: 'x' })).rejects.toThrow(JsonMappingError); + await expect(toInstance(Clash, { name: 'x' })).rejects.toThrow(/both map to the JSON name/); + }); +}); + +describe('@JsonAlias', () => { + class Person { + @JsonProperty('surname') + @JsonAlias('last_name', 'lastName') + @IsString() + surname: string; + } + + it('accepts every alias on input', async () => { + for (const key of ['surname', 'last_name', 'lastName']) { + const p = await toInstance(Person, { [key]: 'Hopper' }); + expect(p.surname).toBe('Hopper'); + } + }); + + it('never emits an alias on output', async () => { + const p = new Person(); + p.surname = 'Hopper'; + await expect(toPlain(p)).resolves.toEqual({ surname: 'Hopper' }); + }); +}); + +describe('access control decorators', () => { + it('@JsonIgnore drops the property in both directions', async () => { + class Secretive { + @IsString() + name: string; + + @JsonIgnore() + internalNote: string; + } + + const s = new Secretive(); + s.name = 'x'; + s.internalNote = 'do not leak'; + await expect(toPlain(s)).resolves.toEqual({ name: 'x' }); + + const parsed = await toInstance(Secretive, { name: 'x', internalNote: 'injected' }); + expect(parsed.internalNote).toBeUndefined(); + }); + + it('@JsonWriteOnly accepts input but never echoes it back', async () => { + class Credentials { + @IsString() + email: string; + + @JsonWriteOnly() + @IsString() + password: string; + } + + const c = await toInstance(Credentials, { email: 'a@b.com', password: 'hunter2' }); + expect(c.password).toBe('hunter2'); + await expect(toPlain(c)).resolves.toEqual({ email: 'a@b.com' }); + }); + + it('@JsonReadOnly is emitted but cannot be set by a client', async () => { + class Record { + @JsonReadOnly() + id: number; + + @IsString() + title: string; + } + + const r = await toInstance(Record, { id: 999, title: 'hello' }); + expect(r.id).toBeUndefined(); + + r.id = 1; + await expect(toPlain(r)).resolves.toEqual({ id: 1, title: 'hello' }); + }); + + it('@JsonReadOnly is not resurrected by the default unknownKeys policy', async () => { + class Record { + @JsonProperty('identifier') + @JsonReadOnly() + id: number; + + @IsString() + title: string; + } + + const r = await toInstance(Record, { identifier: 999, title: 't' }, { unknownKeys: 'allow' }); + expect(r.id).toBeUndefined(); + expect((r as any).identifier).toBeUndefined(); + }); +}); + +describe('naming strategies', () => { + class Account { + @IsString() + accountHolderName: string; + + @IsInt() + balanceInCents: number; + } + + it('snake_case both ways', async () => { + const a = new Account(); + a.accountHolderName = 'Ada'; + a.balanceInCents = 100; + + const plain = await toPlain(a, { namingStrategy: 'snake_case' }); + expect(plain).toEqual({ account_holder_name: 'Ada', balance_in_cents: 100 }); + + const back = await toInstance(Account, plain, { namingStrategy: 'snake_case' }); + expect(back.accountHolderName).toBe('Ada'); + expect(back.balanceInCents).toBe(100); + }); + + it('kebab-case, SCREAMING_SNAKE_CASE and PascalCase', async () => { + const a = new Account(); + a.accountHolderName = 'Ada'; + a.balanceInCents = 1; + + await expect(toPlain(a, { namingStrategy: 'kebab-case' })) + .resolves.toEqual({ 'account-holder-name': 'Ada', 'balance-in-cents': 1 }); + await expect(toPlain(a, { namingStrategy: 'SCREAMING_SNAKE_CASE' })) + .resolves.toEqual({ ACCOUNT_HOLDER_NAME: 'Ada', BALANCE_IN_CENTS: 1 }); + await expect(toPlain(a, { namingStrategy: 'PascalCase' })) + .resolves.toEqual({ AccountHolderName: 'Ada', BalanceInCents: 1 }); + }); + + it('accepts a custom function', async () => { + const a = new Account(); + a.accountHolderName = 'Ada'; + a.balanceInCents = 1; + + await expect(toPlain(a, { namingStrategy: (k) => `x_${k}` })) + .resolves.toEqual({ x_accountHolderName: 'Ada', x_balanceInCents: 1 }); + }); + + it('@JsonProperty wins over the naming strategy', async () => { + class Mixed { + @JsonProperty('EXPLICIT') + someField: string; + + otherField: string; + } + const m = new Mixed(); + m.someField = 'a'; + m.otherField = 'b'; + + await expect(toPlain(m, { namingStrategy: 'snake_case' })) + .resolves.toEqual({ EXPLICIT: 'a', other_field: 'b' }); + }); + + it('splits acronyms the way a reader expects', () => { + const snake = resolveNamingStrategy('snake_case'); + expect(snake('parseHTTPResponse')).toBe('parse_http_response'); + expect(snake('firstName')).toBe('first_name'); + expect(snake('id')).toBe('id'); + expect(snake('already_snake')).toBe('already_snake'); + + const camel = resolveNamingStrategy('camelCase'); + expect(camel('first_name')).toBe('firstName'); + }); + + it('rejects an unknown strategy name', () => { + expect(() => resolveNamingStrategy('shouty' as any)).toThrow(/Unknown naming strategy/); + }); + + it('applies to nested objects too', async () => { + class Inner { + @IsString() + innerValue: string; + } + class Outer { + @ValidateNested() + @JsonType(() => Inner) + outerChild: Inner; + } + + const parsed = await toInstance( + Outer, + { outer_child: { inner_value: 'v' } }, + { namingStrategy: 'snake_case' } + ); + expect(parsed.outerChild).toBeInstanceOf(Inner); + expect(parsed.outerChild.innerValue).toBe('v'); + }); +}); + +describe('configure()', () => { + class Account { + @IsString() + accountHolderName: string; + } + + it('sets a library-wide default', async () => { + configure({ namingStrategy: 'snake_case' }); + + const a = new Account(); + a.accountHolderName = 'Ada'; + await expect(toPlain(a)).resolves.toEqual({ account_holder_name: 'Ada' }); + expect(getConfig().namingStrategy).toBe('snake_case'); + }); + + it('is overridden by per-call options', async () => { + configure({ namingStrategy: 'snake_case' }); + + const a = new Account(); + a.accountHolderName = 'Ada'; + await expect(toPlain(a, { namingStrategy: 'kebab-case' })) + .resolves.toEqual({ 'account-holder-name': 'Ada' }); + }); + + it('resetConfig() restores the defaults', async () => { + configure({ namingStrategy: 'snake_case', unknownKeys: 'error', validate: false }); + resetConfig(); + expect(getConfig()).toEqual({ namingStrategy: 'identity', unknownKeys: 'allow', validate: true }); + }); +}); + +describe('unknownKeys policy', () => { + class Dto { + @IsString() + known: string; + } + + it('allow (default) copies unknown keys through', async () => { + const d = await toInstance(Dto, { known: 'a', extra: 'b' }); + expect((d as any).extra).toBe('b'); + }); + + it('strip drops them', async () => { + const d = await toInstance(Dto, { known: 'a', extra: 'b' }, { unknownKeys: 'strip' }); + expect((d as any).extra).toBeUndefined(); + expect(d.known).toBe('a'); + }); + + it('error rejects the payload and names the offending key', async () => { + await expect(toInstance(Dto, { known: 'a', extra: 'b' }, { unknownKeys: 'error' })) + .rejects.toThrow(/Unknown property "extra"/); + }); + + it('never lets __proto__ through, whatever the policy', async () => { + for (const unknownKeys of ['allow', 'strip', 'error'] as const) { + const d = await toInstance(Dto, JSON.parse('{"known":"a","__proto__":{"x":1}}'), { unknownKeys }); + expect(Object.getPrototypeOf(d)).toBe(Dto.prototype); + expect(({} as any).x).toBeUndefined(); + } + }); +}); + +describe('validate option', () => { + class Strict { + @IsString() + name: string; + + @IsOptional() + @IsInt() + @Min(0) + age?: number; + } + + it('throws by default', async () => { + await expect(toInstance(Strict, { name: 123 })).rejects.toThrow(JsonValidationError); + }); + + it('maps without validating when told to', async () => { + const s = await toInstance(Strict, { name: 123 }, { validate: false }); + expect(s).toBeInstanceOf(Strict); + expect(s.name).toBe(123 as any); + }); + + it('skips validation on the way out too', async () => { + const s = new Strict(); + s.name = 123 as any; + await expect(toPlain(s, { validate: false })).resolves.toEqual({ name: 123 }); + }); + + it('validateOrReject throws, validate returns', async () => { + const s = new Strict(); + s.name = 123 as any; + + await expect(validateOrReject(s)).rejects.toThrow(JsonValidationError); + await expect(validate(s)).resolves.toHaveLength(1); + }); +}); + +describe('error helpers', () => { + class Item { + @IsInt() + @Min(1) + qty: number; + } + class Order { + @IsString() + reference: string; + + @ValidateNested() + @JsonType(() => Item) + items: Item[]; + } + + const buildFailing = () => { + const bad = new Item(); + bad.qty = -5; + const o = new Order(); + o.reference = 42 as any; + o.items = [bad]; + return o; + }; + + it('flattenErrors produces dotted paths with array indices', async () => { + const errors = await validate(buildFailing()); + const flat = flattenErrors(errors); + + expect(flat['reference']).toEqual(['reference must be a string']); + expect(flat['items[0].qty']).toEqual(['qty must be at least 1']); + }); + + it('formatErrors renders one line per failure', async () => { + const errors = await validate(buildFailing()); + const text = formatErrors(errors); + + expect(text).toContain('reference: reference must be a string'); + expect(text).toContain('items[0].qty: qty must be at least 1'); + }); + + it('collectErrorMessages returns just the messages', async () => { + const messages = collectErrorMessages(await validate(buildFailing())); + expect(messages).toHaveLength(2); + expect(messages).toContain('qty must be at least 1'); + }); + + it('returns an empty result for a valid object', async () => { + const o = new Order(); + o.reference = 'ok'; + o.items = []; + expect(flattenErrors(await validate(o))).toEqual({}); + expect(formatErrors(await validate(o))).toBe(''); + }); +}); + +describe('a realistic API payload', () => { + it('maps a snake_case request and answers without the secret', async () => { + class SignUp { + @JsonReadOnly() + id: number; + + @JsonProperty('email_address') + @IsString() + email: string; + + @JsonWriteOnly() + @IsString() + password: string; + + @IsString() + displayName: string; + } + + const body = JSON.stringify({ + id: 999, // client must not be able to set this + email_address: 'ada@example.com', + password: 'hunter2', + display_name: 'Ada', + }); + + const signUp = await fromJson(SignUp, body, { namingStrategy: 'snake_case' }); + expect(signUp.id).toBeUndefined(); + expect(signUp.email).toBe('ada@example.com'); + expect(signUp.password).toBe('hunter2'); + expect(signUp.displayName).toBe('Ada'); + + signUp.id = 1; + const response = await toJson(signUp, { namingStrategy: 'snake_case' }); + expect(JSON.parse(response)).toEqual({ + id: 1, + email_address: 'ada@example.com', + display_name: 'Ada', + }); + expect(response).not.toContain('hunter2'); + }); +}); diff --git a/src/naming.ts b/src/naming.ts new file mode 100644 index 0000000..2ba4caa --- /dev/null +++ b/src/naming.ts @@ -0,0 +1,74 @@ +/** + * Translates a class property name into the name used in JSON. + * + * Applied only to properties that do not carry an explicit `@JsonProperty`, which always wins. + */ +export type NamingStrategyFn = (propertyKey: string) => string; + +/** + * A built-in strategy name, or your own function. + * + * The built-ins assume property names are written in the TypeScript convention (camelCase) + * and convert away from it. + */ +export type NamingStrategy = + | 'identity' + | 'camelCase' + | 'PascalCase' + | 'snake_case' + | 'SCREAMING_SNAKE_CASE' + | 'kebab-case' + | NamingStrategyFn; + +/** + * Splits an identifier into lowercase words. + * + * Handles the two boundaries that matter in practice: a lowercase-or-digit followed by an + * uppercase (`firstName`), and an acronym running into a new word (`parseHTTPResponse`, + * where the split belongs before `Response`, not inside `HTTP`). Existing separators are + * treated as boundaries too, so an already-converted name survives a second pass unchanged. + */ +function words(propertyKey: string): string[] { + return propertyKey + .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(word => word.toLowerCase()); +} + +const capitalize = (word: string): string => (word ? word.charAt(0).toUpperCase() + word.slice(1) : word); + +const BUILT_INS: Record, NamingStrategyFn> = { + identity: (key) => key, + camelCase: (key) => { + const parts = words(key); + if (parts.length === 0) return key; + return parts[0] + parts.slice(1).map(capitalize).join(''); + }, + PascalCase: (key) => words(key).map(capitalize).join('') || key, + snake_case: (key) => words(key).join('_') || key, + SCREAMING_SNAKE_CASE: (key) => words(key).join('_').toUpperCase() || key, + 'kebab-case': (key) => words(key).join('-') || key, +}; + +/** + * Resolves a {@link NamingStrategy} to the function that implements it. + * + * @throws Error when given a name that is not one of the built-in strategies. + */ +export function resolveNamingStrategy(strategy: NamingStrategy | undefined): NamingStrategyFn { + if (!strategy) return BUILT_INS.identity; + if (typeof strategy === 'function') return strategy; + + const builtIn = BUILT_INS[strategy]; + if (!builtIn) { + throw new Error( + `Unknown naming strategy ${JSON.stringify(strategy)}. ` + + `Use one of: ${Object.keys(BUILT_INS).join(', ')}, or pass your own function.` + ); + } + return builtIn; +} diff --git a/src/utils.ts b/src/utils.ts index d72fe54..911feac 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,8 @@ import { ClassConstructor } from './interfaces.js'; -import { METADATA_KEYS, ValidationConstraint, ValidationArguments } from './decorators.js'; +import { METADATA_KEYS, PropertyAccess, ValidationConstraint, ValidationArguments } from './decorators.js'; import { metadataStorage } from './metadata-storage.js'; +import { NamingStrategyFn, resolveNamingStrategy } from './naming.js'; +import { TransformOptions, UnknownKeyPolicy, resolveOptions } from './config.js'; export interface ValidationError { property: string; @@ -41,6 +43,16 @@ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); // --- Internal Engine --- +interface SerializeContext { + naming: NamingStrategyFn; +} + +interface DeserializeContext { + naming: NamingStrategyFn; + namingKey: unknown; + unknownKeys: UnknownKeyPolicy; +} + /** * Resolves the metadata lookup target for a value. * @@ -52,7 +64,84 @@ function prototypeOf(obj: any): any { return Object.getPrototypeOf(obj) ?? undefined; } -async function serialize(obj: any, ancestors: Set): Promise { +function accessOf(target: any, key: string): PropertyAccess { + return (target ? metadataStorage.getMetadata(METADATA_KEYS.ACCESS, target, key) : undefined) ?? 'readwrite'; +} + +/** The name this property takes in JSON: an explicit @JsonProperty, else the naming strategy. */ +function outboundName(target: any, key: string, naming: NamingStrategyFn): string { + const explicit = target ? metadataStorage.getMetadata(METADATA_KEYS.NAME, target, key) : undefined; + return explicit ?? naming(key); +} + +interface InboundNames { + /** JSON name -> property key, for properties this payload is allowed to set. */ + accept: Map; + /** + * JSON names that belong to a declared property the payload may NOT set + * (`@JsonIgnore` / `@JsonReadOnly`). They are dropped rather than treated as unknown + * keys — otherwise the default `unknownKeys: 'allow'` policy would copy them straight + * back onto the instance and undo the protection. + */ + blocked: Set; +} + +// Name maps are derived purely from decorator metadata, which is fixed once a class is +// declared, so they are cached per (prototype, naming strategy). +const inboundCache = new WeakMap>(); + +/** + * Builds the JSON-name -> property-key lookup used when reading a payload. + * + * Only names the class actually declares are accepted: the `@JsonProperty` name (or the + * naming strategy's rendering of the property name) plus any `@JsonAlias`. Renaming a + * property therefore stops the old name from being silently accepted — add `@JsonAlias` to + * keep it working for older clients. + */ +function inboundNameMap(target: any, ctx: DeserializeContext): InboundNames { + let byStrategy = inboundCache.get(target); + if (!byStrategy) { + byStrategy = new Map(); + inboundCache.set(target, byStrategy); + } + const cached = byStrategy.get(ctx.namingKey); + if (cached) return cached; + + const accept = new Map(); + const blocked = new Set(); + + const claim = (external: string, key: string) => { + const owner = accept.get(external); + if (owner && owner !== key) { + throw new JsonMappingError( + `Properties "${owner}" and "${key}" both map to the JSON name ${JSON.stringify(external)}. ` + + `Give one of them a distinct @JsonProperty name.` + ); + } + accept.set(external, key); + }; + + for (const key of metadataStorage.getProperties(target)) { + const names = [ + outboundName(target, key, ctx.naming), + ...(metadataStorage.getMetadata(METADATA_KEYS.ALIASES, target, key) || []), + ]; + + const access = accessOf(target, key); + if (access === 'none' || access === 'readonly') { + for (const name of names) blocked.add(name); + continue; + } + + for (const name of names) claim(name, key); + } + + const result = { accept, blocked }; + byStrategy.set(ctx.namingKey, result); + return result; +} + +async function serialize(obj: any, ancestors: Set, ctx: SerializeContext): Promise { if (obj === null || obj === undefined || typeof obj !== 'object') { return obj; } @@ -73,7 +162,7 @@ async function serialize(obj: any, ancestors: Set): Promise { if (Array.isArray(obj)) { const out: any[] = []; for (const item of obj) { - out.push(await serialize(item, ancestors)); + out.push(await serialize(item, ancestors, ctx)); } return out; } @@ -82,16 +171,21 @@ async function serialize(obj: any, ancestors: Set): Promise { const result: any = {}; for (const key of Object.keys(obj)) { + const access = accessOf(target, key); + // `writeonly` is accepted on input but must never be echoed back out. + if (access === 'none' || access === 'writeonly') continue; + const value = obj[key]; + const name = outboundName(target, key, ctx.naming); // Custom serializers only see real values. Handing a serializer `undefined` for a // property that was simply never set turns an optional field into a crash. const serializerCls = target ? metadataStorage.getMetadata(METADATA_KEYS.SERIALIZER, target, key) : undefined; if (serializerCls && value !== null && value !== undefined) { const serializer = new serializerCls(); - result[key] = await serializer.serialize(value); + result[name] = await serializer.serialize(value); } else { - result[key] = await serialize(value, ancestors); + result[name] = await serialize(value, ancestors, ctx); } } @@ -103,11 +197,11 @@ async function serialize(obj: any, ancestors: Set): Promise { } } -async function deserialize(clazz: ClassConstructor, plain: any): Promise { +async function deserialize(clazz: ClassConstructor, plain: any, ctx: DeserializeContext): Promise { if (plain === null || plain === undefined) return plain; if (Array.isArray(plain)) { - const results = await Promise.all(plain.map(item => deserialize(clazz, item))); + const results = await Promise.all(plain.map(item => deserialize(clazz, item, ctx))); return results as any; } @@ -115,12 +209,31 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise JSON.stringify(k)).join(', ') || '(none declared)'}.` + ); + } + instance[incoming as keyof T] = plain[incoming]; + continue; + } + + const value = plain[incoming]; // Custom Deserializer const deserializerCls = metadataStorage.getMetadata(METADATA_KEYS.DESERIALIZER, target, key); @@ -138,8 +251,8 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise => { if (item === null || item === undefined || typeof item !== 'object') return item; const subTypeInfo = subTypes.find((s: any) => item[discriminator] === s.name); - if (subTypeInfo) return deserialize(subTypeInfo.value, item); - if (fallback) return deserialize(fallback, item); + if (subTypeInfo) return deserialize(subTypeInfo.value, item, ctx); + if (fallback) return deserialize(fallback, item, ctx); if (onUnknown === 'error') { throw new JsonMappingError( `Unknown discriminator value ${JSON.stringify(item[discriminator])} for property ` + @@ -160,7 +273,7 @@ async function deserialize(clazz: ClassConstructor, plain: any): Promise): Promise { } /** - * Converts a class instance to a plain object with validation. - * @param obj The class instance to transform - * @returns Plain object + * Validates an object and throws {@link JsonValidationError} if it fails. + * + * The counterpart to {@link validate} for callers who want an exception rather than an + * array they have to remember to check. */ -export async function toPlain(obj: T): Promise { - if (obj === null || obj === undefined) return obj; - +export async function validateOrReject(obj: any): Promise { const errors = await validate(obj); if (errors.length > 0) { - throw new JsonValidationError('Validation failed during serialization', errors); + throw new JsonValidationError('Validation failed', errors); } - - return serialize(obj, new Set()); } /** - * Converts a class instance to a JSON string with validation. + * Converts a class instance to a plain object. + * + * Validates first and throws {@link JsonValidationError} on failure, unless + * `{ validate: false }` is passed. + * * @param obj The class instance to transform + * @param options Per-call transform options + * @returns Plain object + */ +export async function toPlain(obj: T, options?: TransformOptions): Promise { + if (obj === null || obj === undefined) return obj; + + if (resolveOptions(options).validate) { + const errors = await validate(obj); + if (errors.length > 0) { + throw new JsonValidationError('Validation failed during serialization', errors); + } + } + + return serialize(obj, new Set(), serializeContext(options)); +} + +/** + * Converts a class instance to a JSON string. + * @param obj The class instance to transform + * @param options Per-call transform options * @returns JSON string */ -export async function toJson(obj: T): Promise { - const plain = await toPlain(obj); +export async function toJson(obj: T, options?: TransformOptions): Promise { + const plain = await toPlain(obj, options); return JSON.stringify(plain); } /** - * Converts a plain object to a class instance with validation. + * Converts a plain object to a class instance. + * + * Validates the result and throws {@link JsonValidationError} on failure, unless + * `{ validate: false }` is passed. + * * @param clazz The class constructor * @param plain The plain object to transform - * @returns Validated class instance + * @param options Per-call transform options + * @returns Class instance */ -export async function toInstance(clazz: ClassConstructor, plain: any): Promise { - const instance = await deserialize(clazz, plain); +export async function toInstance(clazz: ClassConstructor, plain: any, options?: TransformOptions): Promise { + const instance = await deserialize(clazz, plain, deserializeContext(options)); - const errors = await validate(instance); - if (errors.length > 0) { - throw new JsonValidationError('Validation failed during deserialization', errors); + if (resolveOptions(options).validate) { + const errors = await validate(instance); + if (errors.length > 0) { + throw new JsonValidationError('Validation failed during deserialization', errors); + } } return instance; } /** - * Converts an array of plain objects to an array of class instances with validation. + * Converts an array of plain objects to an array of class instances. * * `toInstance` also accepts arrays at runtime, but its return type says `T`. Use this when * the payload is a collection so the static type matches what you actually get back. * * @param clazz The class constructor * @param plain The array of plain objects to transform - * @returns Validated array of class instances + * @param options Per-call transform options + * @returns Array of class instances */ -export async function toInstanceArray(clazz: ClassConstructor, plain: any[]): Promise { +export async function toInstanceArray( + clazz: ClassConstructor, + plain: any[], + options?: TransformOptions +): Promise { if (!Array.isArray(plain)) { throw new JsonMappingError(`Expected an array to map to ${clazz.name}[], received ${typeof plain}.`); } - return (await toInstance(clazz, plain)) as unknown as T[]; + return (await toInstance(clazz, plain, options)) as unknown as T[]; } /** - * Parses a JSON string to a class instance with validation. + * Parses a JSON string to a class instance. * @param clazz The class constructor * @param json JSON string - * @returns Validated class instance + * @param options Per-call transform options + * @returns Class instance */ -export async function fromJson(clazz: ClassConstructor, json: string): Promise { - const plain = JSON.parse(json); - return toInstance(clazz, plain); +export async function fromJson(clazz: ClassConstructor, json: string, options?: TransformOptions): Promise { + return toInstance(clazz, parseJson(json), options); } /** - * Parses a JSON string containing an array into validated class instances. + * Parses a JSON string containing an array into class instances. * @param clazz The class constructor * @param json JSON string holding an array - * @returns Validated array of class instances + * @param options Per-call transform options + * @returns Array of class instances */ -export async function fromJsonArray(clazz: ClassConstructor, json: string): Promise { - return toInstanceArray(clazz, JSON.parse(json)); +export async function fromJsonArray( + clazz: ClassConstructor, + json: string, + options?: TransformOptions +): Promise { + return toInstanceArray(clazz, parseJson(json), options); +} + +function parseJson(json: string): any { + try { + return JSON.parse(json); + } catch (error) { + throw new JsonMappingError( + `Input is not valid JSON: ${error instanceof Error ? error.message : String(error)}` + ); + } } /** * Helper for Fetch-based frameworks (Next.js, Hono, etc.) - * Extracts JSON from a Request and transforms it to a validated instance. + * Extracts JSON from a Request and transforms it to a class instance. * @param clazz The class constructor * @param request Web Request object - * @returns Validated class instance + * @param options Per-call transform options + * @returns Class instance */ -export async function fromRequest(clazz: ClassConstructor, request: Request): Promise { +export async function fromRequest( + clazz: ClassConstructor, + request: Request, + options?: TransformOptions +): Promise { let plain: any; try { plain = await request.json(); @@ -429,7 +609,7 @@ export async function fromRequest(clazz: ClassConstructor, request: Reques `Request body is not valid JSON: ${error instanceof Error ? error.message : String(error)}` ); } - return toInstance(clazz, plain); + return toInstance(clazz, plain, options); } /** @@ -444,4 +624,5 @@ export class JsonMapper { static fromJsonArray = fromJsonArray; static fromRequest = fromRequest; static validate = validate; + static validateOrReject = validateOrReject; } From 44c8f28f4b8dbd701e09a8c2fc7f75ae11081594 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:48:40 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8=20feat:=20add=2030=20validation?= =?UTF-8?q?=20decorators,=20@ValidateIf=20and=20@Allow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rounds out the validator set with the rules an application actually reaches for, all following the existing decorator style and honouring each/message options. - equality and presence: @Equals, @NotEquals, @IsEmpty, @IsEnum, @IsInstance - strings: @Length, @IsAlpha, @IsAlphanumeric, @IsNumberString, @IsLowercase, @IsUppercase, @Contains, @NotContains, @StartsWith, @EndsWith - formats: @IsUUID, @IsJSON, @IsDateString, @IsSemVer, @IsHexColor, @IsIP - numbers: @IsDivisibleBy, @IsPort, @IsLatitude, @IsLongitude, @IsBigInt - dates: @MinDate, @MaxDate - arrays: @ArrayUnique, @ArrayContains, @ArrayNotContains @ValidateIf(o => ...) makes a property's rules conditional on the rest of the object, and @Allow() declares a property that needs no rules of its own so it survives unknownKeys: 'strip'. Two details worth noting. @IsEnum filters the reverse mapping a numeric enum compiles to, so 'Low' is not accepted as a value of enum { Low, High }. @MinDate/@MaxDate accept a thunk, so "not in the past" is evaluated per validation instead of being frozen when the class was declared. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK --- src/decorators.ts | 478 +++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 6 + src/validators.test.ts | 323 ++++++++++++++++++++++++++++ 3 files changed, 807 insertions(+) create mode 100644 src/validators.test.ts diff --git a/src/decorators.ts b/src/decorators.ts index 2215226..74a841c 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -13,6 +13,7 @@ export const METADATA_KEYS = { NAME: 'cereale:name', ALIASES: 'cereale:aliases', ACCESS: 'cereale:access', + CONDITION: 'cereale:condition', }; /** @@ -565,6 +566,483 @@ export function IsDate(options?: ValidationOptions) { }; } +// --- Equality and presence --- + +/** + * @Equals(comparison: any) + */ +export function Equals(comparison: any, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'equals', + validate: (v) => v === comparison, + message: `${propertyKey} must be equal to ${JSON.stringify(comparison)}`, + constraints: [comparison] + }, options); + }; +} + +/** + * @NotEquals(comparison: any) + */ +export function NotEquals(comparison: any, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'notEquals', + validate: (v) => v !== comparison, + message: `${propertyKey} must not be equal to ${JSON.stringify(comparison)}`, + constraints: [comparison] + }, options); + }; +} + +/** + * @IsEmpty() + * Passes for null, undefined, '', [] and {} — the mirror of `@IsNotEmpty`. + */ +export function IsEmpty(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isEmpty', + validate: (v) => { + if (v === null || v === undefined || v === '') return true; + if (Array.isArray(v)) return v.length === 0; + if (typeof v === 'object') return Object.keys(v).length === 0; + return false; + }, + message: `${propertyKey} must be empty` + }, options); + }; +} + +/** + * @IsEnum(entity: object) + * Checks the value is a member of a TypeScript enum (string or numeric). + */ +export function IsEnum(entity: Record, options?: ValidationOptions) { + // A numeric enum compiles to a two-way map ({ A: 0, '0': 'A' }), so the reverse-mapped + // names have to be filtered out or 'A' would validate as a legal value. + const values = Object.keys(entity) + .filter(key => typeof entity[entity[key]] !== 'number') + .map(key => entity[key]); + + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isEnum', + validate: (v) => values.includes(v), + message: `${propertyKey} must be one of the following values: ${values.join(', ')}`, + constraints: [values] + }, options); + }; +} + +/** + * @IsInstance(target: ClassConstructor) + */ +export function IsInstance(clazz: ClassConstructor, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isInstance', + validate: (v) => v instanceof clazz, + message: `${propertyKey} must be an instance of ${clazz.name}`, + constraints: [clazz] + }, options); + }; +} + +// --- Strings --- + +/** + * @Length(min: number, max?: number) + */ +export function Length(min: number, max?: number, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'length', + validate: (v) => typeof v === 'string' && v.length >= min && (max === undefined || v.length <= max), + message: max === undefined + ? `${propertyKey} must be at least ${min} characters` + : `${propertyKey} must be between ${min} and ${max} characters`, + constraints: max === undefined ? [min] : [min, max] + }, options); + }; +} + +function stringPattern(name: string, regex: RegExp, describe: (property: string) => string) { + return (options?: ValidationOptions) => (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name, + validate: (v) => typeof v === 'string' && regex.test(v), + message: describe(propertyKey) + }, options); + }; +} + +/** @IsAlpha() — letters only. */ +export const IsAlpha = stringPattern('isAlpha', /^[A-Za-z]+$/, p => `${p} must contain only letters`); + +/** @IsAlphanumeric() — letters and digits only. */ +export const IsAlphanumeric = stringPattern( + 'isAlphanumeric', /^[A-Za-z0-9]+$/, p => `${p} must contain only letters and numbers` +); + +/** @IsNumberString() — a string that parses as a finite number. */ +export function IsNumberString(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isNumberString', + validate: (v) => typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)), + message: `${propertyKey} must be a number string` + }, options); + }; +} + +/** @IsLowercase() */ +export function IsLowercase(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isLowercase', + validate: (v) => typeof v === 'string' && v === v.toLowerCase(), + message: `${propertyKey} must be lowercase` + }, options); + }; +} + +/** @IsUppercase() */ +export function IsUppercase(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isUppercase', + validate: (v) => typeof v === 'string' && v === v.toUpperCase(), + message: `${propertyKey} must be uppercase` + }, options); + }; +} + +/** @Contains(seed: string) */ +export function Contains(seed: string, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'contains', + validate: (v) => typeof v === 'string' && v.includes(seed), + message: `${propertyKey} must contain ${JSON.stringify(seed)}`, + constraints: [seed] + }, options); + }; +} + +/** @NotContains(seed: string) */ +export function NotContains(seed: string, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'notContains', + validate: (v) => typeof v === 'string' && !v.includes(seed), + message: `${propertyKey} must not contain ${JSON.stringify(seed)}`, + constraints: [seed] + }, options); + }; +} + +/** @StartsWith(prefix: string) */ +export function StartsWith(prefix: string, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'startsWith', + validate: (v) => typeof v === 'string' && v.startsWith(prefix), + message: `${propertyKey} must start with ${JSON.stringify(prefix)}`, + constraints: [prefix] + }, options); + }; +} + +/** @EndsWith(suffix: string) */ +export function EndsWith(suffix: string, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'endsWith', + validate: (v) => typeof v === 'string' && v.endsWith(suffix), + message: `${propertyKey} must end with ${JSON.stringify(suffix)}`, + constraints: [suffix] + }, options); + }; +} + +const NIL_UUID = '00000000-0000-0000-0000-000000000000'; +const MAX_UUID = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; + +/** + * @IsUUID(version?: 1|2|3|4|5|6|7|8) + * Without a version, accepts any RFC 9562 UUID plus the nil and max UUIDs. + */ +export function IsUUID(version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, options?: ValidationOptions) { + const pattern = version + ? new RegExp(`^[0-9a-f]{8}-[0-9a-f]{4}-${version}[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 (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isUuid', + validate: (v) => { + if (typeof v !== 'string') return false; + if (!version && (v.toLowerCase() === NIL_UUID || v.toLowerCase() === MAX_UUID)) return true; + return pattern.test(v); + }, + message: `${propertyKey} must be a valid UUID${version ? ` (version ${version})` : ''}`, + ...(version ? { constraints: [version] } : {}) + }, options); + }; +} + +/** @IsJSON() — a string that JSON.parse accepts. */ +export function IsJSON(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isJson', + validate: (v) => { + if (typeof v !== 'string') return false; + try { + JSON.parse(v); + return true; + } catch { + return false; + } + }, + message: `${propertyKey} must be a JSON string` + }, options); + }; +} + +/** @IsDateString() — an ISO-8601 string that parses to a real date. */ +export function IsDateString(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isDateString', + validate: (v) => typeof v === 'string' && !isNaN(Date.parse(v)), + message: `${propertyKey} must be a valid ISO 8601 date string` + }, options); + }; +} + +/** @IsSemVer() */ +export const IsSemVer = stringPattern( + '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-]+)*))?$/, + p => `${p} must be a valid semantic version` +); + +/** @IsHexColor() — #rgb, #rrggbb or #rrggbbaa. */ +export const IsHexColor = stringPattern( + 'isHexColor', /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i, p => `${p} must be a hex color` +); + +const IPV4 = /^(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}$/; + +/** + * @IsIP(version?: 4 | 6) + */ +export function IsIP(version?: 4 | 6, options?: ValidationOptions) { + const isV6 = (v: string) => { + // Node's URL parser is the most reliable IPv6 validator available without a dependency. + try { + return new URL(`http://[${v}]`).hostname === `[${v.toLowerCase()}]` || /^[0-9a-f:.]+$/i.test(v) && v.includes(':'); + } catch { + return false; + } + }; + + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isIp', + validate: (v) => { + if (typeof v !== 'string') return false; + if (version === 4) return IPV4.test(v); + if (version === 6) return isV6(v); + return IPV4.test(v) || isV6(v); + }, + message: `${propertyKey} must be a valid IP${version ? `v${version}` : ''} address`, + ...(version ? { constraints: [version] } : {}) + }, options); + }; +} + +// --- Numbers --- + +/** @IsDivisibleBy(divisor: number) */ +export function IsDivisibleBy(divisor: number, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isDivisibleBy', + validate: (v) => typeof v === 'number' && Number.isFinite(v) && divisor !== 0 && v % divisor === 0, + message: `${propertyKey} must be divisible by ${divisor}`, + constraints: [divisor] + }, options); + }; +} + +/** @IsPort() — an integer in 0..65535, as a number or a numeric string. */ +export function IsPort(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isPort', + validate: (v) => { + const n = typeof v === 'string' && v.trim() !== '' ? Number(v) : v; + return typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 65535; + }, + message: `${propertyKey} must be a valid port number` + }, options); + }; +} + +/** @IsLatitude() */ +export function IsLatitude(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isLatitude', + validate: (v) => typeof v === 'number' && Number.isFinite(v) && v >= -90 && v <= 90, + message: `${propertyKey} must be a latitude between -90 and 90` + }, options); + }; +} + +/** @IsLongitude() */ +export function IsLongitude(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isLongitude', + validate: (v) => typeof v === 'number' && Number.isFinite(v) && v >= -180 && v <= 180, + message: `${propertyKey} must be a longitude between -180 and 180` + }, options); + }; +} + +/** @IsBigInt() */ +export function IsBigInt(options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'isBigInt', + validate: (v) => typeof v === 'bigint', + message: `${propertyKey} must be a bigint` + }, options); + }; +} + +// --- Dates --- + +type DateBound = Date | (() => Date); + +const boundOf = (bound: DateBound): Date => (typeof bound === 'function' ? bound() : bound); + +/** + * @MinDate(date: Date | (() => Date)) + * Accepts a thunk so a moving boundary — "not in the past" — is evaluated per validation + * rather than frozen when the class was declared. + */ +export function MinDate(min: DateBound, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'minDate', + validate: (v) => v instanceof Date && !isNaN(v.getTime()) && v.getTime() >= boundOf(min).getTime(), + message: (args) => `${args.property} must not be earlier than ${boundOf(min).toISOString()}`, + constraints: [min] + }, options); + }; +} + +/** + * @MaxDate(date: Date | (() => Date)) + */ +export function MaxDate(max: DateBound, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'maxDate', + validate: (v) => v instanceof Date && !isNaN(v.getTime()) && v.getTime() <= boundOf(max).getTime(), + message: (args) => `${args.property} must not be later than ${boundOf(max).toISOString()}`, + constraints: [max] + }, options); + }; +} + +// --- Arrays --- + +/** + * @ArrayUnique(identifier?: (item: any) => any) + * Pass an extractor to deduplicate objects by a key rather than by reference. + */ +export function ArrayUnique(identifier?: (item: any) => any, options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'arrayUnique', + validate: (v) => { + if (!Array.isArray(v)) return false; + const keys = identifier ? v.map(identifier) : v; + return new Set(keys).size === keys.length; + }, + message: `${propertyKey} must not contain duplicate values` + }, options); + }; +} + +/** @ArrayContains(values: any[]) — the array must contain every listed value. */ +export function ArrayContains(values: any[], options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'arrayContains', + validate: (v) => Array.isArray(v) && values.every(value => v.includes(value)), + message: `${propertyKey} must contain the following values: ${values.join(', ')}`, + constraints: [values] + }, options); + }; +} + +/** @ArrayNotContains(values: any[]) — the array must contain none of the listed values. */ +export function ArrayNotContains(values: any[], options?: ValidationOptions) { + return (target: any, propertyKey: string) => { + addValidation(target, propertyKey, { + name: 'arrayNotContains', + validate: (v) => Array.isArray(v) && values.every(value => !v.includes(value)), + message: `${propertyKey} must not contain any of the following values: ${values.join(', ')}`, + constraints: [values] + }, options); + }; +} + +// --- Control flow --- + +/** + * @ValidateIf(condition: (object: any) => boolean) + * Skips every constraint on this property when the condition returns false. + * + * ```ts + * class Payment { + * @IsIn(['card', 'invoice']) + * method: string; + * + * @ValidateIf(o => o.method === 'card') + * @IsString() + * cardNumber?: string; + * } + * ``` + */ +export function ValidateIf(condition: (object: any) => boolean) { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + metadataStorage.defineMetadata(METADATA_KEYS.CONDITION, condition, target, propertyKey); + }; +} + +/** + * @Allow() + * Declares a property with no constraints of its own. + * + * Useful with `unknownKeys: 'strip'` or `'error'`, where a property has to be declared to + * survive the payload even though nothing about its value needs checking. + */ +export function Allow() { + return (target: any, propertyKey: string) => { + registerProperty(target, propertyKey); + }; +} + /** * @ValidateNested(options?: ValidationOptions) * Recursively validates the value of this property. diff --git a/src/utils.ts b/src/utils.ts index 911feac..04ccf7c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -365,6 +365,12 @@ async function validateInternal(obj: any, ancestors: Set): Promise void, value: any): Promise { + class Subject { + val: any; + } + decorate(Subject.prototype, 'val'); + + const subject = new Subject(); + subject.val = value; + + const errors = await validate(subject); + return errors.length ? Object.keys(errors[0]!.constraints) : []; +} + +const passes = async (decorator: any, value: any) => expect(await check(decorator, value)).toEqual([]); +const fails = async (decorator: any, value: any) => expect((await check(decorator, value)).length).toBeGreaterThan(0); + +describe('equality and presence', () => { + it('@Equals / @NotEquals', async () => { + await passes(Equals('x'), 'x'); + await fails(Equals('x'), 'y'); + await passes(NotEquals('x'), 'y'); + await fails(NotEquals('x'), 'x'); + }); + + it('@IsEmpty', async () => { + for (const empty of [null, undefined, '', [], {}]) await passes(IsEmpty(), empty); + for (const filled of ['a', [1], { a: 1 }, 0]) await fails(IsEmpty(), filled); + }); + + it('@IsInstance', async () => { + class Thing {} + await passes(IsInstance(Thing), new Thing()); + await fails(IsInstance(Thing), {}); + }); +}); + +describe('@IsEnum', () => { + enum StringRole { Admin = 'admin', User = 'user' } + enum NumericLevel { Low, High } + + it('accepts members of a string enum', async () => { + await passes(IsEnum(StringRole), 'admin'); + await fails(IsEnum(StringRole), 'root'); + }); + + it('accepts members of a numeric enum without accepting its reverse-mapped names', async () => { + await passes(IsEnum(NumericLevel), 0); + await passes(IsEnum(NumericLevel), 1); + await fails(IsEnum(NumericLevel), 2); + // 'Low' is the reverse mapping, not a legal value + await fails(IsEnum(NumericLevel), 'Low'); + }); +}); + +describe('strings', () => { + it('@Length with and without a maximum', async () => { + await passes(Length(2), 'ab'); + await fails(Length(3), 'ab'); + await passes(Length(2, 4), 'abc'); + await fails(Length(2, 4), 'abcde'); + }); + + it('@IsAlpha / @IsAlphanumeric', async () => { + await passes(IsAlpha(), 'abcDEF'); + await fails(IsAlpha(), 'abc1'); + await passes(IsAlphanumeric(), 'abc123'); + await fails(IsAlphanumeric(), 'abc-123'); + }); + + it('@IsNumberString', async () => { + await passes(IsNumberString(), '42'); + await passes(IsNumberString(), '-1.5'); + await fails(IsNumberString(), 'abc'); + await fails(IsNumberString(), ''); + await fails(IsNumberString(), 42); + }); + + it('@IsLowercase / @IsUppercase', async () => { + await passes(IsLowercase(), 'abc'); + await fails(IsLowercase(), 'Abc'); + await passes(IsUppercase(), 'ABC'); + await fails(IsUppercase(), 'Abc'); + }); + + it('@Contains / @NotContains / @StartsWith / @EndsWith', async () => { + await passes(Contains('ell'), 'hello'); + await fails(Contains('xyz'), 'hello'); + await passes(NotContains('xyz'), 'hello'); + await fails(NotContains('ell'), 'hello'); + await passes(StartsWith('he'), 'hello'); + await fails(StartsWith('lo'), 'hello'); + await passes(EndsWith('lo'), 'hello'); + await fails(EndsWith('he'), 'hello'); + }); +}); + +describe('@IsUUID', () => { + const v4 = '9b2e4c1a-77bd-4f2e-8c33-1d9a6b0e5f21'; + + it('accepts any version when unversioned', async () => { + await passes(IsUUID(), v4); + await passes(IsUUID(), '00000000-0000-0000-0000-000000000000'); // nil + await fails(IsUUID(), 'not-a-uuid'); + await fails(IsUUID(), 42); + }); + + it('enforces a requested version', async () => { + await passes(IsUUID(4), v4); + await fails(IsUUID(1), v4); + }); +}); + +describe('formats', () => { + it('@IsJSON', async () => { + await passes(IsJSON(), '{"a":1}'); + await passes(IsJSON(), '[1,2]'); + await fails(IsJSON(), '{a:1}'); + await fails(IsJSON(), { a: 1 }); + }); + + it('@IsDateString', async () => { + await passes(IsDateString(), '2026-08-03T00:00:00Z'); + await fails(IsDateString(), 'not a date'); + }); + + it('@IsSemVer', async () => { + await passes(IsSemVer(), '1.2.3'); + await passes(IsSemVer(), '1.0.0-alpha.1+build.5'); + await fails(IsSemVer(), '1.2'); + await fails(IsSemVer(), 'v1.2.3'); + }); + + it('@IsHexColor', async () => { + await passes(IsHexColor(), '#fff'); + await passes(IsHexColor(), '#A1B2C3'); + await passes(IsHexColor(), '#A1B2C3FF'); + await fails(IsHexColor(), 'fff'); + await fails(IsHexColor(), '#ggg'); + }); + + it('@IsIP', async () => { + await passes(IsIP(4), '192.168.0.1'); + await fails(IsIP(4), '256.0.0.1'); + await fails(IsIP(4), '::1'); + await passes(IsIP(6), '::1'); + await passes(IsIP(), '10.0.0.1'); + await fails(IsIP(), 'nope'); + }); +}); + +describe('numbers', () => { + it('@IsDivisibleBy', async () => { + await passes(IsDivisibleBy(5), 10); + await fails(IsDivisibleBy(5), 11); + await fails(IsDivisibleBy(5), '10'); + }); + + it('@IsPort', async () => { + await passes(IsPort(), 8080); + await passes(IsPort(), '443'); + await fails(IsPort(), 70000); + await fails(IsPort(), -1); + await fails(IsPort(), 1.5); + }); + + it('@IsLatitude / @IsLongitude', async () => { + await passes(IsLatitude(), 48.85); + await fails(IsLatitude(), 91); + await passes(IsLongitude(), 2.35); + await fails(IsLongitude(), 181); + }); + + it('@IsBigInt', async () => { + await passes(IsBigInt(), 10n); + await fails(IsBigInt(), 10); + }); +}); + +describe('dates', () => { + it('@MinDate / @MaxDate with a fixed bound', async () => { + const bound = new Date('2026-01-01T00:00:00Z'); + await passes(MinDate(bound), new Date('2026-06-01T00:00:00Z')); + await fails(MinDate(bound), new Date('2025-06-01T00:00:00Z')); + await passes(MaxDate(bound), new Date('2025-06-01T00:00:00Z')); + await fails(MaxDate(bound), new Date('2026-06-01T00:00:00Z')); + }); + + it('@MinDate accepts a thunk so the bound moves', async () => { + await passes(MinDate(() => new Date(Date.now() - 1000)), new Date()); + await fails(MinDate(() => new Date(Date.now() + 60_000)), new Date()); + }); + + it('rejects a non-date', async () => { + await fails(MinDate(new Date(0)), '2026-01-01'); + }); +}); + +describe('arrays', () => { + it('@ArrayUnique by value', async () => { + await passes(ArrayUnique(), [1, 2, 3]); + await fails(ArrayUnique(), [1, 2, 2]); + }); + + it('@ArrayUnique by extracted key', async () => { + const byId = (item: any) => item.id; + await passes(ArrayUnique(byId), [{ id: 1 }, { id: 2 }]); + await fails(ArrayUnique(byId), [{ id: 1 }, { id: 1 }]); + }); + + it('@ArrayContains / @ArrayNotContains', async () => { + await passes(ArrayContains(['a']), ['a', 'b']); + await fails(ArrayContains(['c']), ['a', 'b']); + await passes(ArrayNotContains(['c']), ['a', 'b']); + await fails(ArrayNotContains(['a']), ['a', 'b']); + }); +}); + +describe('@ValidateIf', () => { + class Payment { + @IsIn(['card', 'invoice']) + method: string; + + @ValidateIf(o => o.method === 'card') + @IsString() + cardNumber?: string; + } + + it('skips the constraint when the condition is false', async () => { + const p = new Payment(); + p.method = 'invoice'; + expect(await validate(p)).toEqual([]); + }); + + it('applies the constraint when the condition is true', async () => { + const p = new Payment(); + p.method = 'card'; + + const errors = await validate(p); + expect(errors).toHaveLength(1); + expect(errors[0]!.property).toBe('cardNumber'); + }); + + it('passes when the condition is true and the value is valid', async () => { + const p = new Payment(); + p.method = 'card'; + p.cardNumber = '4111111111111111'; + expect(await validate(p)).toEqual([]); + }); +}); + +describe('@Allow', () => { + it('declares a property so strict unknown-key policies keep it', async () => { + class Dto { + @IsString() + name: string; + + @Allow() + metadata: unknown; + } + + const d = await toInstance( + Dto, + { name: 'x', metadata: { anything: true }, stray: 1 }, + { unknownKeys: 'strip' } + ); + expect(d.metadata).toEqual({ anything: true }); + expect((d as any).stray).toBeUndefined(); + }); +}); + +describe('new validators cooperate with existing options', () => { + it('honours each: true', async () => { + class T { + @IsUUID(4, { each: true }) + ids: string[]; + } + const t = new T(); + t.ids = ['9b2e4c1a-77bd-4f2e-8c33-1d9a6b0e5f21']; + expect(await validate(t)).toEqual([]); + + t.ids = ['9b2e4c1a-77bd-4f2e-8c33-1d9a6b0e5f21', 'nope']; + expect(await validate(t)).toHaveLength(1); + }); + + it('honours @IsOptional', async () => { + class T { + @IsOptional() + @IsSemVer() + version?: string; + } + const t = new T(); + expect(await validate(t)).toEqual([]); + + t.version = 'bad'; + expect(await validate(t)).toHaveLength(1); + }); + + it('honours a custom message', async () => { + class T { + @IsPort({ message: 'give me a real port' }) + port: number; + } + const t = new T(); + t.port = -1; + + const errors = await validate(t); + expect(errors[0]!.constraints['isPort']).toBe('give me a real port'); + }); +}); From 83ad2a289d5474c83f4ae66f7ec3e786f8e41d00 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:53:09 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=93=9D=20docs:=20bring=20README,=20de?= =?UTF-8?q?mo=20and=20playground=20in=20line=20with=20the=20code;=20releas?= =?UTF-8?q?e=200.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README described a library that did not exist in places. It showed @ValidateNested({ each: true }), which did not compile; told users to enable emitDecoratorMetadata, which the library never reads; and documented none of the mapping API. Its Quick Start now runs verbatim — verified by compiling and executing it against the local source. - README: document field-name mapping, access control, options, error helpers and the 30 new validators; drop the emitDecoratorMetadata instruction; add a Notes and Limitations section covering circular references, validate() on plain objects, and the fact that @JsonProperty stops the original name from being accepted unless you add @JsonAlias - CHANGELOG.md: new, covering 0.1.0 - example.ts: rewritten as a tour of the current API — read-only ids, write-only secrets, renamed fields, conditional validation, flattened errors, and a base-class rule reaching a subclass - docs: the playground hand-listed its symbol table in three parallel places and exposed IsEmail, which is not an export. It now derives scope from the bundle, so new decorators work there as soon as they ship. Bundle regenerated - version 0.1.0 136 tests, 97% statement and 100% function coverage. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK --- CHANGELOG.md | 118 ++++++++++++++++++++++ README.md | 255 +++++++++++++++++++++++++++++++++++++----------- docs/cereale.js | 3 +- docs/index.html | 82 ++++++++-------- package.json | 2 +- src/example.ts | 194 ++++++++++++++++++++++-------------- 6 files changed, 481 insertions(+), 173 deletions(-) create mode 100644 CHANGELOG.md 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 c576e02..13e6de6 100644 --- a/docs/cereale.js +++ b/docs/cereale.js @@ -1 +1,2 @@ -"use strict";var Cereale=(()=>{var v=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var L=(t,n)=>{for(var e in n)v(t,e,{get:n[e],enumerable:!0})},D=(t,n,e,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of N(n))!R.call(t,i)&&i!==e&&v(t,i,{get:()=>n[i],enumerable:!(a=S(n,i))||a.enumerable});return t};var z=t=>D(v({},"__esModule",{value:!0}),t);var pt={};L(pt,{ArrayMaxSize:()=>ot,ArrayMinSize:()=>rt,ArrayNotEmpty:()=>lt,Email:()=>et,IsArray:()=>st,IsBoolean:()=>H,IsDate:()=>dt,IsDefined:()=>_,IsIn:()=>ut,IsInt:()=>B,IsNotEmpty:()=>G,IsNotIn:()=>ct,IsNumber:()=>W,IsObject:()=>F,IsOptional:()=>q,IsString:()=>U,IsUrl:()=>at,JsonDeserialize:()=>J,JsonMapper:()=>C,JsonPolymorphic:()=>Z,JsonSerialize:()=>k,JsonType:()=>Y,JsonValidationError:()=>O,METADATA_KEYS:()=>f,Matches:()=>it,Max:()=>X,MaxLength:()=>nt,Min:()=>Q,MinLength:()=>tt,Negative:()=>K,Positive:()=>j,Validate:()=>ft,ValidateNested:()=>gt,fromJson:()=>w,fromRequest:()=>T,registerDecorator:()=>mt,toInstance:()=>M,toJson:()=>P,toPlain:()=>E,validate:()=>I});var x=class t{static instance;properties=new WeakMap;propertyMetadata=new WeakMap;classMetadata=new WeakMap;constructor(){}static getInstance(){return t.instance||(t.instance=new t),t.instance}defineMetadata(n,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(n,e)}else{let s=this.classMetadata.get(a);s||(s=new Map,this.classMetadata.set(a,s)),s.set(n,e)}}getMetadata(n,e,a){let i=e;for(;i;){let s=this.getOwnMetadata(n,i,a);if(s!==void 0)return s;i=Object.getPrototypeOf(i)}}getOwnMetadata(n,e,a){return a?this.propertyMetadata.get(e)?.get(a)?.get(n):this.classMetadata.get(e)?.get(n)}registerProperty(n,e){let a=this.properties.get(n);a||(a=[],this.properties.set(n,a)),a.includes(e)||a.push(e)}getProperties(n){let e=new Set,a=n;for(;a;){let i=this.properties.get(a);i&&i.forEach(s=>e.add(s)),a=Object.getPrototypeOf(a)}return Array.from(e)}},l=x.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"};function y(t,n){l.registerProperty(t,n)}function o(t,n,e,a){y(t,n),a&&(a.each&&(e.each=!0),a.message&&(e.message=a.message));let i=l.getOwnMetadata(f.VALIDATION,t,n)||[];i.push(e),l.defineMetadata(f.VALIDATION,i,t,n)}function k(t){return(n,e)=>{y(n,e),l.defineMetadata(f.SERIALIZER,t,n,e)}}function J(t){return(n,e)=>{y(n,e),l.defineMetadata(f.DESERIALIZER,t,n,e)}}function Y(t){return(n,e)=>{y(n,e),l.defineMetadata(f.TYPE,t,n,e)}}function Z(t,n){return(e,a)=>{y(e,a),l.defineMetadata(f.POLYMORPHIC,{discriminator:t,subTypes:n},e,a)}}function q(){return(t,n)=>{y(t,n),l.defineMetadata(f.IS_OPTIONAL,!0,t,n)}}function U(t){return(n,e)=>{o(n,e,{name:"isString",validate:a=>typeof a=="string",message:`${e} must be a string`},t)}}function H(t){return(n,e)=>{o(n,e,{name:"isBoolean",validate:a=>typeof a=="boolean",message:`${e} must be a boolean`},t)}}function W(t){return(n,e)=>{o(n,e,{name:"isNumber",validate:a=>typeof a=="number"&&!isNaN(a),message:`${e} must be a number`},t)}}function B(t){return(n,e)=>{o(n,e,{name:"isInt",validate:a=>Number.isInteger(a),message:`${e} must be an integer`},t)}}function F(t){return(n,e)=>{o(n,e,{name:"isObject",validate:a=>typeof a=="object"&&a!==null&&!Array.isArray(a),message:`${e} must be an object`},t)}}function _(t){return(n,e)=>{o(n,e,{name:"isDefined",validate:a=>a!=null,message:`${e} should not be null or undefined`},t)}}function G(t){return(n,e)=>{o(n,e,{name:"isNotEmpty",validate:a=>a!=null&&a!=="",message:`${e} should not be empty`},t)}}function Q(t,n){return(e,a)=>{o(e,a,{name:"min",validate:i=>typeof i=="number"&&i>=t,message:`${a} must be at least ${t}`,constraints:[t]},n)}}function X(t,n){return(e,a)=>{o(e,a,{name:"max",validate:i=>typeof i=="number"&&i<=t,message:`${a} must be at most ${t}`,constraints:[t]},n)}}function j(t){return(n,e)=>{o(n,e,{name:"positive",validate:a=>typeof a=="number"&&a>0,message:`${e} must be positive`},t)}}function K(t){return(n,e)=>{o(n,e,{name:"negative",validate:a=>typeof a=="number"&&a<0,message:`${e} must be negative`},t)}}function tt(t,n){return(e,a)=>{o(e,a,{name:"minLength",validate:i=>typeof i=="string"&&i.length>=t,message:`${a} must be longer than or equal to ${t} characters`,constraints:[t]},n)}}function nt(t,n){return(e,a)=>{o(e,a,{name:"maxLength",validate:i=>typeof i=="string"&&i.length<=t,message:`${a} must be shorter than or equal to ${t} characters`,constraints:[t]},n)}}function et(t){let n=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return(e,a)=>{o(e,a,{name:"isEmail",validate:i=>typeof i=="string"&&n.test(i),message:`${a} must be a valid email`},t)}}function at(t){return(n,e)=>{o(n,e,{name:"isUrl",validate:a=>{try{return new URL(a),!0}catch{return!1}},message:`${e} must be a valid URL`},t)}}function it(t,n){return(e,a)=>{o(e,a,{name:"matches",validate:i=>typeof i=="string"&&t.test(i),message:`${a} must match ${t} regular expression`,constraints:[t]},n)}}function st(t){return(n,e)=>{o(n,e,{name:"isArray",validate:a=>Array.isArray(a),message:`${e} must be an array`},t)}}function rt(t,n){return(e,a)=>{o(e,a,{name:"arrayMinSize",validate:i=>Array.isArray(i)&&i.length>=t,message:`${a} must contain at least ${t} elements`,constraints:[t]},n)}}function ot(t,n){return(e,a)=>{o(e,a,{name:"arrayMaxSize",validate:i=>Array.isArray(i)&&i.length<=t,message:`${a} must contain at most ${t} elements`,constraints:[t]},n)}}function lt(t){return(n,e)=>{o(n,e,{name:"arrayNotEmpty",validate:a=>Array.isArray(a)&&a.length>0,message:`${e} should not be empty`},t)}}function ut(t,n){return(e,a)=>{o(e,a,{name:"isIn",validate:i=>t.includes(i),message:`${a} must be one of the following values: ${t.join(", ")}`,constraints:[t]},n)}}function ct(t,n){return(e,a)=>{o(e,a,{name:"isNotIn",validate:i=>!t.includes(i),message:`${a} must not be one of the following values: ${t.join(", ")}`,constraints:[t]},n)}}function dt(t){return(n,e)=>{o(n,e,{name:"isDate",validate:a=>a instanceof Date&&!isNaN(a.getTime()),message:`${e} must be a valid Date object`},t)}}function gt(){return(t,n)=>{y(t,n),l.defineMetadata("cereale:nested",!0,t,n)}}function ft(t,n,e){return(a,i)=>{let s=[],r;if(Array.isArray(n)?(s=n,r=e):typeof n=="object"&&(r=n),typeof t=="function"&&!t.prototype?.validate)o(a,i,{name:"custom",validate:t,message:c=>`${c.property} is invalid`,constraints:s},r);else{let c=new t;o(a,i,{name:t.name,validate:(d,g)=>c.validate(d,g),message:d=>c.defaultMessage?c.defaultMessage(d):`${d.property} is invalid`,constraints:s},r)}}}function mt(t){let{name:n,target:e,propertyName:a,options:i,constraints:s,validator:r}=t,c;if(typeof r=="function"&&!r.prototype?.validate)c={name:n,validate:r,message:d=>`${d.property} is invalid`,...s?{constraints:s}:{}};else{let d=typeof r=="function"?new r:r;c={name:n,validate:(g,m)=>d.validate(g,m),message:g=>d.defaultMessage?d.defaultMessage(g):`${g.property} is invalid`,...s?{constraints:s}:{}}}o(e.prototype,a,c,i)}var O=class extends Error{constructor(e,a){super(e);this.errors=a;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}};async function b(t){if(t==null||typeof t!="object")return t;if(Array.isArray(t))return Promise.all(t.map(i=>b(i)));if(t instanceof Date)return t.toISOString();let n=t.constructor.prototype,e={},a=Object.keys(t);for(let i of a){let s=t[i],r=l.getMetadata(f.SERIALIZER,n,i);if(r){let c=new r;e[i]=await c.serialize(s)}else e[i]=await b(s)}return e}async function A(t,n){if(n==null)return n;if(Array.isArray(n))return await Promise.all(n.map(s=>A(t,s)));let e=new t,a=t.prototype;for(let i of Object.keys(n)){let s=n[i],r=l.getMetadata(f.DESERIALIZER,a,i);if(r){let g=new r;e[i]=await g.deserialize(s);continue}let c=l.getMetadata(f.POLYMORPHIC,a,i);if(c&&s!==null&&s!==void 0){let{discriminator:g,subTypes:m}=c;if(Array.isArray(s))e[i]=await Promise.all(s.map(async p=>{let u=m.find(h=>p[g]===h.name);return u?A(u.value,p):p}));else{let p=m.find(u=>s[g]===u.name);if(p){e[i]=await A(p.value,s);continue}}continue}let d=l.getMetadata(f.TYPE,a,i);if(d&&s!==null&&s!==void 0){let g=d();e[i]=await A(g,s);continue}e[i]=s}return e}async function I(t){let n=[];if(t==null||typeof t!="object")return n;if(Array.isArray(t)){for(let i=0;i0&&n.push({property:`[${i}]`,value:t[i],constraints:{},children:s})}return n}let e=Object.getPrototypeOf(t),a=l.getProperties(e);for(let i of a){let s=t[i],r={property:i,value:s,constraints:{}},c=l.getMetadata(f.IS_OPTIONAL,e,i),d=s==null;if(c&&d)continue;let g=l.getMetadata(f.VALIDATION,e,i)||[],m={value:s,object:t,property:i,constraints:[]};for(let u of g){m.constraints=u.constraints||[];let h=!0;if(u.each&&Array.isArray(s))for(let V of s){let $={...m,value:V};if(!await u.validate(V,$)){h=!1;break}}else h=await u.validate(s,m);if(!h){let V=typeof u.message=="function"?u.message(m):u.message;u.each&&(V=`each element in ${V}`),r.constraints[u.name]=V}}if(l.getMetadata("cereale:nested",e,i)&&s!==null&&s!==void 0){let u=await I(s);u.length>0&&(r.children=u)}(Object.keys(r.constraints).length>0||r.children)&&n.push(r)}return n}async function E(t){if(t==null)return t;let n=await I(t);if(n.length>0)throw new O("Validation failed during serialization",n);return b(t)}async function P(t){let n=await E(t);return JSON.stringify(n)}async function M(t,n){let e=await A(t,n),a=await I(e);if(a.length>0)throw new O("Validation failed during deserialization",a);return e}async function w(t,n){let e=JSON.parse(n);return M(t,e)}async function T(t,n){let e=await n.json();return M(t,e)}var C=class{static toPlain=E;static toJson=P;static toInstance=M;static fromJson=w;static fromRequest=T;static validate=I};return z(pt);})(); +"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 @@