✨ feat!: v2 — strongly typed decorators on the TC39 standard
BREAKING CHANGE: cereale moves from legacy `experimentalDecorators` to TC39
standard decorators, which is what makes validation rules type-checked against
the fields they are attached to.
class User {
@IsString() name!: string; // fine
@IsString() age!: number; // Type 'number' is not assignable to 'string'
}
Legacy decorators receive (target: any, key: string) and lose the field type
entirely, so this was impossible in v1. Standard decorators receive
ClassFieldDecoratorContext<This, Value>, which carries it. Rules now checked:
scalar rules against scalar fields; { each: true } against arrays, in both
directions; @JsonType against the field's class; @JsonSerialize/@JsonDeserialize
against the field's type; @IsIn and @IsEnum against the field's value type.
17 tests invoke the real compiler to assert the wrong code stays rejected — a
guarantee nobody checks is one that quietly stops holding.
Positioning follows the capability: validated domain objects, not validated
data. The README now leads with the Zod comparison. Cereale does not infer your
type from a schema — you still write the field type and the rule — but it
guarantees the two cannot disagree, which is what class-validator never offered.
Removed
- metadata-storage.ts and its WeakMap singleton. Metadata lives on
context.metadata now, which also removes the dual ESM/CJS double-singleton
hazard. Inheritance merging becomes structural rather than reconstructed on
every read, so the subclass-shadowing defect fixed by hand in 0.1.0 cannot
reoccur by construction.
- registerDecorator, replaced by defineRule(Class, 'field', constraint).
Unchanged: the engine, options, naming strategies, access control, error
helpers, the sync API, and the performance work. 193 tests pass.
Toolchain note: standard decorators are transformed by tsc and esbuild, but not
yet by oxc. The library builds with tsc and consumers on esbuild/Vite are fine;
Vitest 4 uses oxc, so the test runner needs an esbuild transform plugin. This is
recorded in vitest.config.ts and the README, and is the reason 1.x should stay
available for oxc-based toolchains.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
+22
-5
@@ -11,12 +11,29 @@ import {
|
||||
validate, toInstance,
|
||||
} from './index.js';
|
||||
|
||||
/** Builds a one-property class, assigns `value`, and returns the constraint keys that failed. */
|
||||
async function check(decorate: (target: any, key: string) => void, value: any): Promise<string[]> {
|
||||
/**
|
||||
* Applies a decorator to a synthetic one-field class and reports which rules failed.
|
||||
*
|
||||
* Standard decorators are invoked as `(undefined, context)` rather than against a prototype,
|
||||
* so the context is built by hand here. Only `name` and `metadata` are read by the library;
|
||||
* the rest satisfies the shape.
|
||||
*/
|
||||
async function check(decorator: any, value: any): Promise<string[]> {
|
||||
const metadata = Object.create(null) as DecoratorMetadata;
|
||||
decorator(undefined, {
|
||||
kind: 'field',
|
||||
name: 'val',
|
||||
static: false,
|
||||
private: false,
|
||||
metadata,
|
||||
access: { has: () => true, get: (o: any) => o.val, set: (o: any, v: any) => { o.val = v; } },
|
||||
addInitializer: () => undefined,
|
||||
});
|
||||
|
||||
class Subject {
|
||||
val: any;
|
||||
}
|
||||
decorate(Subject.prototype, 'val');
|
||||
(Subject as any)[Symbol.metadata] = metadata;
|
||||
|
||||
const subject = new Subject();
|
||||
subject.val = value;
|
||||
@@ -232,9 +249,9 @@ describe('arrays', () => {
|
||||
describe('@ValidateIf', () => {
|
||||
class Payment {
|
||||
@IsIn(['card', 'invoice'])
|
||||
method: string;
|
||||
method!: 'card' | 'invoice';
|
||||
|
||||
@ValidateIf(o => o.method === 'card')
|
||||
@ValidateIf<Payment>(o => o.method === 'card')
|
||||
@IsString()
|
||||
cardNumber?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user