This file is for AI coding agents (and humans who like density) working on
@imqueue/validation. It captures how the codebase is built, tested and
structured, plus the invariants that are easy to get wrong. Read it before
making changes. For contribution process/terms see
CONTRIBUTING.md; for end-user docs see the
README and https://imqueue.org/.
@imqueue/validation is the input-validation layer of the @imqueue framework:
Zod-backed, field- and method-level validation exposed as native (TC39)
decorators (@validate, @validatable, @validated, plus schemaOf). It is
used by @imqueue/rpc services and by code
generated by @imqueue/pg-prisma to
validate RPC inputs.
- ESM only,
"type": "module". Useimport, notrequire(). Import sibling modules with the.jsextension (NodeNext resolves it to the.tssource), e.g.import { validate } from './validate.js'. - TypeScript,
module/moduleResolution: nodenext,target: es2024,verbatimModuleSyntax: true,isolatedModules: true,strict: true. Useimport type/import { type X }for type-only imports. - Node ≥ 22.12.
- Native TC39 decorators — the code relies on the standard decorator emit,
not
experimentalDecoratorsand notreflect-metadata. Do not enableexperimentalDecorators; it changes decorator semantics and would break the buffer/seal mechanism. - Single runtime dependency:
zod. Do not add heavyweight deps; this package is meant to stay small. - Lint/format:
oxlint+oxfmt. Runnpm run formatbefore committing; CI checksnpm run format:check. - Build emits
.js/.d.ts/.js.mapnext to sources; these are gitignored, not committed (buildrunsclean-compiledfirst). Never commit compiled output. removeCommentsis intentionallyfalse— downstream tooling relies on doc-blocks surviving compilation. Keep it that way.
npm install
npm run build # clean-compiled + tsc (emits alongside sources)
npm test # build + node:test over every test/**/*.spec.js
npm run lint # oxlint
npm run format # oxfmt (write) | npm run format:check (verify)
npm run test-coverage # tests + experimental coverage summary
npm run test-lcov # writes coverage/lcov.info| Path | Role |
|---|---|
index.ts |
Public entry: export * from './src/index.js' |
src/index.ts |
Barrel re-exporting the public API |
src/validate.ts |
The whole implementation: @validate, @validatable, @validated, schemaOf, and the Validator type. |
test/** |
node:test specs (*.spec.ts, run compiled). |
- Field decorators buffer; the class decorator seals. TC39 metadata
(
Symbol.metadata) is not reliably populated by esbuild/tsx, so@validatewrites into a module-level buffer and@validatable()moves that buffer into aWeakMapregistry keyed by the class. This is safe only because field decorators run before the class decorator and class bodies evaluate sequentially — do not introduce async between them or reorder. - A class field decorated with
@validaterequires the class to be@validatable()forschemaOfto see it. - Validators resolve lazily — but that does not buy mutual references. A
@validatableclass used as a validator is resolved viaschemaOfon first use, which defers the schema assembly. The reference itself is not deferred: the decorator argument is evaluated when the referencing class is defined, so a class named there must be declared above it. Bothtsc(TS2449) and the runtime (ReferenceError: Cannot access 'X' before initialization) reject the other order, and two classes therefore cannot reference each other. An earlier version of this file claimed otherwise; verified 2026-08-01. - An unsealed class contaminates the next sealed one. Because
@validatebuffers into module state that@validatableclaims, a class using@validatewithout@validatableleaves its fields for whichever class is sealed next — which then requires properties it never declared, while the class with the actual mistake validates nothing. Test files therefore need care about declaration order, and each spec file gets its own process (node --test), which is what keeps them isolated. @validatedvalidates without substituting.schema.parse()'s return value is discarded and the original argument is passed to the method, so transforming schemas (z.coerce.number(),.trim(),.default(...)) change nothing the body sees, and object schemas do not strip unknown properties.- Invalid input throws the raw
ZodError. Do not wrap or swallow it. Note what a remote caller actually receives, though:@imqueue/rpc'sIMQServiceconverts anything a method throws into anIMQRPCErrorpayload, so the client seescode: 'IMQ_RPC_CALL_ERROR'(aZodErrorcarries no code) with Zod's issue list as the message string.instanceof ZodErrorholds in-process only.
GPL-3.0. Commercial licensing for closed-source products: https://imqueue.com/.