@lankajs/typebox 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/skills/lanka-typebox/SKILL.md +120 -0
- package/skills/lanka-typebox/reference.md +173 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lankajs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @lankajs/typebox
|
|
2
|
+
|
|
3
|
+
**▸ module** · TypeBox conveniences
|
|
4
|
+
|
|
5
|
+
> A bridge for the one library in the family with no Standard Schema, and a compiled checker it caches.
|
|
6
|
+
|
|
7
|
+
A library in the same box. The app imports and calls it; core does not know it exists.
|
|
8
|
+
|
|
9
|
+
**Runs in:** the browser, node and React Native — everywhere.
|
|
10
|
+
|
|
11
|
+
**How to use it:** [GUIDE.md](./GUIDE.md) — the user guide, with examples. **How to change it:** [SKILL.md](./SKILL.md).
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- `lankaTypeBoxValidator` — the bridge, over a compiled checker cached per schema
|
|
16
|
+
- `TLankaInferred<S>` — schema type inference
|
|
17
|
+
|
|
18
|
+
## Why a bridge rather than a name
|
|
19
|
+
|
|
20
|
+
TypeBox publishes no `~standard` at all — not asynchronously like yup, not at all —
|
|
21
|
+
so core's port cannot be handed a TypeBox schema. The bridge reads
|
|
22
|
+
`Value`/`TypeCompiler` and produces the same two lists every package in the family
|
|
23
|
+
produces.
|
|
24
|
+
|
|
25
|
+
## Why the compiled checker is cached, and why that is not premature
|
|
26
|
+
|
|
27
|
+
`TypeCompiler.Compile(schema)` turns a schema into a function, and the function is the
|
|
28
|
+
fastest validator in JavaScript. Compilation itself is not fast. Compiling on every
|
|
29
|
+
call would make this the SLOWEST package in the family while advertising the
|
|
30
|
+
opposite — the exact shape of a claim that measures well in a microbenchmark and
|
|
31
|
+
loses in an application.
|
|
32
|
+
|
|
33
|
+
So a `WeakMap` keyed by the schema object holds the compiled checker and whether the
|
|
34
|
+
schema contains a transform. Weak because the key is the consumer's schema: a strong
|
|
35
|
+
map here would keep every schema a screen ever built alive for the life of the tab.
|
|
36
|
+
|
|
37
|
+
## Why `Decode` is not simply always called
|
|
38
|
+
|
|
39
|
+
`Value.Decode` applies transforms AND re-checks, so calling it after the compiled
|
|
40
|
+
check would validate every body twice. `HasTransform` is asked once per schema and
|
|
41
|
+
cached beside the checker, so a schema without transforms — which is most of them —
|
|
42
|
+
pays for one pass.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
Repository map: [../../../README.md](../../../README.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
+
import { TLankaValidationResult } from 'lanka/validation';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The validator's shape, typed by a TypeBox schema rather than by core's port.
|
|
6
|
+
*
|
|
7
|
+
* `ILankaValidator` takes a `TLankaSchema` — a Standard Schema — and TypeBox
|
|
8
|
+
* publishes none, so a validator declared as the port would reject every schema a
|
|
9
|
+
* consumer of this package has. The SHAPE is the port's, member for member, and
|
|
10
|
+
* `check:family` holds the barrel to the family's surface; what differs is the
|
|
11
|
+
* one type the library makes impossible to share.
|
|
12
|
+
*/
|
|
13
|
+
interface ILankaTypeBoxValidator {
|
|
14
|
+
/** Validates and returns the parsed value, or throws. */
|
|
15
|
+
validate<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown, context: string): Static<TSchemaType>;
|
|
16
|
+
/** Validates and returns an outcome, throwing nothing. */
|
|
17
|
+
validateSafe<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown): TLankaValidationResult<Static<TSchemaType>>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The validator for TypeBox schemas.
|
|
21
|
+
*
|
|
22
|
+
* ## Why this one is a bridge
|
|
23
|
+
*
|
|
24
|
+
* TypeBox publishes no `~standard` at all — not asynchronously as yup does, not
|
|
25
|
+
* at all — so core's port cannot be handed a TypeBox schema and the package is
|
|
26
|
+
* what makes the library usable here.
|
|
27
|
+
*
|
|
28
|
+
* ## What it costs, and why the cache is not premature
|
|
29
|
+
*
|
|
30
|
+
* `TypeCompiler.Compile(schema)` produces the fastest validator in JavaScript,
|
|
31
|
+
* and compiling is the slow part. Compiled per call, this would be the slowest
|
|
32
|
+
* package in the family while claiming to be the fastest. The checker is cached
|
|
33
|
+
* per schema instead — see `compiledTypeBoxSchema`.
|
|
34
|
+
*
|
|
35
|
+
* A schema built inside a component body is a new object on every render and
|
|
36
|
+
* therefore a new cache key. Declare schemas at module level, which is where they
|
|
37
|
+
* belong for every other reason too.
|
|
38
|
+
*/
|
|
39
|
+
declare const lankaTypeBoxValidator: ILankaTypeBoxValidator;
|
|
40
|
+
|
|
41
|
+
/** The type a schema infers. Shorter than `Static<typeof schema>` everywhere. */
|
|
42
|
+
type TLankaInferred<TSchemaType extends TSchema> = Static<TSchemaType>;
|
|
43
|
+
|
|
44
|
+
export { type TLankaInferred, lankaTypeBoxValidator };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/lanka-type-box-validator/lankaTypeBoxValidator.ts
|
|
2
|
+
import { KindGuard } from "@sinclair/typebox";
|
|
3
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
|
+
import { LankaValidationError } from "lanka/validation";
|
|
5
|
+
import { lankaForeignSchemaMessage, lankaValueOrThrow } from "lanka/internal";
|
|
6
|
+
|
|
7
|
+
// src/_internal/compiled-type-box-schema/compiledTypeBoxSchema.ts
|
|
8
|
+
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
9
|
+
import { HasTransform } from "@sinclair/typebox/value";
|
|
10
|
+
var compiled = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
var compiledTypeBoxSchema = (schema) => {
|
|
12
|
+
const known = compiled.get(schema);
|
|
13
|
+
if (known) return known;
|
|
14
|
+
const fresh = {
|
|
15
|
+
check: TypeCompiler.Compile(schema),
|
|
16
|
+
transforms: HasTransform(schema, [])
|
|
17
|
+
};
|
|
18
|
+
compiled.set(schema, fresh);
|
|
19
|
+
return fresh;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/_utils/type-box-pointer-segments/typeBoxPointerSegments.ts
|
|
23
|
+
var typeBoxPointerSegments = (pointer) => {
|
|
24
|
+
if (!pointer) return [];
|
|
25
|
+
return pointer.split("/").slice(1).map((segment) => {
|
|
26
|
+
const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
27
|
+
return /^\d+$/.test(key) ? Number(key) : key;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/lanka-type-box-validator/lankaTypeBoxValidator.ts
|
|
32
|
+
var lankaTypeBoxValidator = Object.freeze({
|
|
33
|
+
validate(schema, data, context) {
|
|
34
|
+
return lankaValueOrThrow(runCompiled(schema, data), context);
|
|
35
|
+
},
|
|
36
|
+
validateSafe(schema, data) {
|
|
37
|
+
return runCompiled(schema, data);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
function runCompiled(schema, data) {
|
|
41
|
+
if (!KindGuard.IsSchema(schema)) throw notATypeBoxSchema(schema);
|
|
42
|
+
const { check, transforms } = compiledTypeBoxSchema(schema);
|
|
43
|
+
if (!check.Check(data)) return describeFailure([...check.Errors(data)]);
|
|
44
|
+
if (!transforms) return { success: true, data };
|
|
45
|
+
return decode(schema, data);
|
|
46
|
+
}
|
|
47
|
+
function notATypeBoxSchema(schema) {
|
|
48
|
+
return new LankaValidationError(
|
|
49
|
+
lankaForeignSchemaMessage(schema, {
|
|
50
|
+
lead: "This is not a TypeBox schema: it carries no `Kind`."
|
|
51
|
+
}),
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function decode(schema, data) {
|
|
56
|
+
try {
|
|
57
|
+
return { success: true, data: Value.Decode(schema, data) };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = decodeFailureMessage(error);
|
|
60
|
+
return { success: false, errors: [message], fields: [{ path: [], message }] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function decodeFailureMessage(error) {
|
|
64
|
+
const original = error.error ?? error;
|
|
65
|
+
return original instanceof Error ? original.message : String(original);
|
|
66
|
+
}
|
|
67
|
+
function describeFailure(errors) {
|
|
68
|
+
const fields = errors.map(toFieldError);
|
|
69
|
+
return { success: false, errors: fields.map(describeField), fields };
|
|
70
|
+
}
|
|
71
|
+
function toFieldError(error) {
|
|
72
|
+
return { path: typeBoxPointerSegments(error.path), message: error.message };
|
|
73
|
+
}
|
|
74
|
+
function describeField(field) {
|
|
75
|
+
const path = field.path.join(".");
|
|
76
|
+
return path ? `${path}: ${field.message}` : field.message;
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
lankaTypeBoxValidator
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lanka-type-box-validator/lankaTypeBoxValidator.ts","../src/_internal/compiled-type-box-schema/compiledTypeBoxSchema.ts","../src/_utils/type-box-pointer-segments/typeBoxPointerSegments.ts"],"sourcesContent":["import { KindGuard } from \"@sinclair/typebox\";\nimport { Value } from \"@sinclair/typebox/value\";\nimport { LankaValidationError } from \"lanka/validation\";\nimport { lankaForeignSchemaMessage, lankaValueOrThrow } from \"lanka/internal\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { ValueError } from \"@sinclair/typebox/value\";\nimport type { TLankaValidationResult } from \"lanka/validation\";\nimport type { ILankaFieldError } from \"lanka/errors\";\nimport { compiledTypeBoxSchema } from \"../_internal/compiled-type-box-schema/compiledTypeBoxSchema\";\nimport { typeBoxPointerSegments } from \"../_utils/type-box-pointer-segments/typeBoxPointerSegments\";\n\n/**\n * The validator's shape, typed by a TypeBox schema rather than by core's port.\n *\n * `ILankaValidator` takes a `TLankaSchema` — a Standard Schema — and TypeBox\n * publishes none, so a validator declared as the port would reject every schema a\n * consumer of this package has. The SHAPE is the port's, member for member, and\n * `check:family` holds the barrel to the family's surface; what differs is the\n * one type the library makes impossible to share.\n */\nexport interface ILankaTypeBoxValidator {\n\t/** Validates and returns the parsed value, or throws. */\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): Static<TSchemaType>;\n\t/** Validates and returns an outcome, throwing nothing. */\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<Static<TSchemaType>>;\n}\n\n/**\n * The validator for TypeBox schemas.\n *\n * ## Why this one is a bridge\n *\n * TypeBox publishes no `~standard` at all — not asynchronously as yup does, not\n * at all — so core's port cannot be handed a TypeBox schema and the package is\n * what makes the library usable here.\n *\n * ## What it costs, and why the cache is not premature\n *\n * `TypeCompiler.Compile(schema)` produces the fastest validator in JavaScript,\n * and compiling is the slow part. Compiled per call, this would be the slowest\n * package in the family while claiming to be the fastest. The checker is cached\n * per schema instead — see `compiledTypeBoxSchema`.\n *\n * A schema built inside a component body is a new object on every render and\n * therefore a new cache key. Declare schemas at module level, which is where they\n * belong for every other reason too.\n */\nexport const lankaTypeBoxValidator: ILankaTypeBoxValidator = Object.freeze<ILankaTypeBoxValidator>({\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): Static<TSchemaType> {\n\t\treturn lankaValueOrThrow(runCompiled(schema, data), context);\n\t},\n\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<Static<TSchemaType>> {\n\t\treturn runCompiled(schema, data);\n\t},\n});\n\n/**\n * One pass through the compiled checker, and a second only if the schema asks.\n *\n * `Value.Decode` applies transforms AND re-checks, so calling it unconditionally\n * would validate every body twice. Whether the schema transforms at all is\n * answered once per schema and cached beside the checker.\n */\nfunction runCompiled<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<Static<TSchemaType>> {\n\tif (!KindGuard.IsSchema(schema)) throw notATypeBoxSchema(schema);\n\n\tconst { check, transforms } = compiledTypeBoxSchema(schema);\n\n\tif (!check.Check(data)) return describeFailure([...check.Errors(data)]);\n\tif (!transforms) return { success: true, data: data as Static<TSchemaType> };\n\n\treturn decode(schema, data);\n}\n\n/**\n * The refusal for a schema from another library.\n *\n * An application whose schemas come from two libraries eventually hands one to\n * the wrong validator. Unguarded, `TypeCompiler.Compile` was handed a zod schema\n * and threw `TypeCompilerTypeGuardError: Preflight validation check failed` —\n * accurate, and useless to anyone who has not read TypeBox's source. It escaped\n * `validateSafe`, which promises to throw nothing, as a raw library error.\n *\n * `KindGuard.IsSchema` is TypeBox's own answer to the question, so the guard\n * cannot drift from what `Compile` will accept.\n *\n * It throws from `validateSafe` too, deliberately: a refused VALUE is an outcome\n * a form renders, while a schema this package cannot read is a wiring mistake,\n * and putting it in `errors` would show a programmer's error to a user beside an\n * input.\n */\nfunction notATypeBoxSchema(schema: unknown): LankaValidationError {\n\treturn new LankaValidationError(\n\t\tlankaForeignSchemaMessage(schema, {\n\t\t\tlead: \"This is not a TypeBox schema: it carries no `Kind`.\",\n\t\t}),\n\t\t[],\n\t);\n}\n\n/**\n * The transform pass, for a schema that has one.\n *\n * A decode function is consumer code and may throw — a date that does not parse,\n * an enum with no case for the value. That is a refusal of the body, not a crash\n * of the validator, so it comes back as one: `validateSafe` promises to throw\n * nothing.\n */\nfunction decode<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<Static<TSchemaType>> {\n\ttry {\n\t\treturn { success: true, data: Value.Decode(schema, data) };\n\t} catch (error) {\n\t\tconst message = decodeFailureMessage(error);\n\n\t\treturn { success: false, errors: [message], fields: [{ path: [], message }] };\n\t}\n}\n\n/**\n * What a failed decode should SAY.\n *\n * TypeBox wraps whatever the decode function threw in a `TransformDecodeError`\n * and copies across a message only when the thrown value was an `Error` —\n * anything else becomes the literal string \"Unknown error\", which tells a\n * consumer nothing about their own code. The original is kept on `.error`, so\n * that is what is read first.\n */\nfunction decodeFailureMessage(error: unknown): string {\n\tconst original = (error as { error?: unknown }).error ?? error;\n\n\treturn original instanceof Error ? original.message : String(original);\n}\n\n/** A TypeBox failure as the two lists the port promises: one for a banner, one for a form. */\nfunction describeFailure<TOutput>(errors: ValueError[]): TLankaValidationResult<TOutput> {\n\tconst fields = errors.map(toFieldError);\n\n\treturn { success: false, errors: fields.map(describeField), fields };\n}\n\nfunction toFieldError(error: ValueError): ILankaFieldError {\n\treturn { path: typeBoxPointerSegments(error.path), message: error.message };\n}\n\n/**\n * A field path plus its message, for a banner.\n *\n * Dots rather than TypeBox's pointer spelling, because this string is read beside\n * the other five packages' and the family reads one way.\n */\nfunction describeField(field: ILankaFieldError): string {\n\tconst path = field.path.join(\".\");\n\n\treturn path ? `${path}: ${field.message}` : field.message;\n}\n","import { TypeCompiler } from \"@sinclair/typebox/compiler\";\nimport { HasTransform } from \"@sinclair/typebox/value\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport type { TypeCheck } from \"@sinclair/typebox/compiler\";\n\n/** What is known about one schema once, and re-read on every call after that. */\nexport interface ICompiledTypeBoxSchema {\n\t/** The compiled checker: a function TypeBox generated for this schema. */\n\tcheck: TypeCheck<TSchema>;\n\t/**\n\t * Whether anything in the schema transforms.\n\t *\n\t * Asked once because the answer cannot change — a schema is a value — and\n\t * because it decides whether a second pass is needed at all. Most schemas do\n\t * not transform, and those must not pay for `Value.Decode`'s re-check.\n\t */\n\ttransforms: boolean;\n}\n\n/**\n * The compiled checker for a schema, compiled at most once.\n *\n * ## Why this exists\n *\n * `TypeCompiler.Compile` turns a schema into a generated function, and that\n * function is the fastest validator in JavaScript. Compiling is not fast.\n * Compiling per call would make this the SLOWEST package in the family while the\n * README advertised the opposite — the exact shape of a claim that wins a\n * microbenchmark and loses in an application.\n *\n * ## Why a WeakMap\n *\n * The key is the CONSUMER'S schema object. A strong map would keep every schema\n * any screen ever built alive for the life of the tab, which is a leak the\n * consumer cannot see, cannot measure and cannot clear. Weak, the entry goes when\n * the schema does.\n *\n * A schema rebuilt on every render defeats the cache — it is a new key each\n * time — and that is a fact worth knowing rather than a case to work around:\n * declare schemas at module level, which is where they belong anyway.\n */\nconst compiled = new WeakMap<TSchema, ICompiledTypeBoxSchema>();\n\nexport const compiledTypeBoxSchema = (schema: TSchema): ICompiledTypeBoxSchema => {\n\tconst known = compiled.get(schema);\n\tif (known) return known;\n\n\tconst fresh: ICompiledTypeBoxSchema = {\n\t\tcheck: TypeCompiler.Compile(schema),\n\t\ttransforms: HasTransform(schema, []),\n\t};\n\n\tcompiled.set(schema, fresh);\n\n\treturn fresh;\n};\n","/**\n * A TypeBox error pointer as segments: `/tags/0/id` → `[\"tags\", 0, \"id\"]`.\n *\n * TypeBox addresses a field with a JSON Pointer (RFC 6901). `ILankaFieldError.path`\n * is segments, on purpose: the two form libraries the shape was designed against\n * spell the same address differently, and neither can be parsed back out of a\n * joined string safely.\n *\n * The pointer's two escapes are not decoration. `~1` is a literal `/` in a key\n * and `~0` a literal `~`; unescaped, a key containing a slash would split into\n * two segments addressing nothing. **In this order** — `~1` first, then `~0` —\n * because the reverse turns `~01` into `/` instead of the literal `~1` it is.\n *\n * A segment of digits becomes a NUMBER. A form distinguishes the second element\n * of a list from a key spelled `\"1\"`, and JSON Pointer does not carry the\n * difference itself — it is recovered from the shape of the segment, which is\n * what every JSON Pointer implementation does.\n *\n * The root pointer is the empty string, and an empty array is what the port's\n * consumers already read as \"the value as a whole\".\n */\nexport const typeBoxPointerSegments = (pointer: string | undefined): (string | number)[] => {\n\tif (!pointer) return [];\n\n\treturn pointer\n\t\t.split(\"/\")\n\t\t.slice(1)\n\t\t.map((segment) => {\n\t\t\tconst key = segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n\n\t\t\treturn /^\\d+$/.test(key) ? Number(key) : key;\n\t\t});\n};\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,aAAa;AACtB,SAAS,4BAA4B;AACrC,SAAS,2BAA2B,yBAAyB;;;ACH7D,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAwC7B,IAAM,WAAW,oBAAI,QAAyC;AAEvD,IAAM,wBAAwB,CAAC,WAA4C;AACjF,QAAM,QAAQ,SAAS,IAAI,MAAM;AACjC,MAAI,MAAO,QAAO;AAElB,QAAM,QAAgC;AAAA,IACrC,OAAO,aAAa,QAAQ,MAAM;AAAA,IAClC,YAAY,aAAa,QAAQ,CAAC,CAAC;AAAA,EACpC;AAEA,WAAS,IAAI,QAAQ,KAAK;AAE1B,SAAO;AACR;;;AClCO,IAAM,yBAAyB,CAAC,YAAqD;AAC3F,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,SAAO,QACL,MAAM,GAAG,EACT,MAAM,CAAC,EACP,IAAI,CAAC,YAAY;AACjB,UAAM,MAAM,QAAQ,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AAE1D,WAAO,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;AAAA,EAC1C,CAAC;AACH;;;AFsBO,IAAM,wBAAgD,OAAO,OAA+B;AAAA,EAClG,SACC,QACA,MACA,SACsB;AACtB,WAAO,kBAAkB,YAAY,QAAQ,IAAI,GAAG,OAAO;AAAA,EAC5D;AAAA,EAEA,aACC,QACA,MAC8C;AAC9C,WAAO,YAAY,QAAQ,IAAI;AAAA,EAChC;AACD,CAAC;AASD,SAAS,YACR,QACA,MAC8C;AAC9C,MAAI,CAAC,UAAU,SAAS,MAAM,EAAG,OAAM,kBAAkB,MAAM;AAE/D,QAAM,EAAE,OAAO,WAAW,IAAI,sBAAsB,MAAM;AAE1D,MAAI,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO,gBAAgB,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,CAAC;AACtE,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,MAAM,KAAkC;AAE3E,SAAO,OAAO,QAAQ,IAAI;AAC3B;AAmBA,SAAS,kBAAkB,QAAuC;AACjE,SAAO,IAAI;AAAA,IACV,0BAA0B,QAAQ;AAAA,MACjC,MAAM;AAAA,IACP,CAAC;AAAA,IACD,CAAC;AAAA,EACF;AACD;AAUA,SAAS,OACR,QACA,MAC8C;AAC9C,MAAI;AACH,WAAO,EAAE,SAAS,MAAM,MAAM,MAAM,OAAO,QAAQ,IAAI,EAAE;AAAA,EAC1D,SAAS,OAAO;AACf,UAAM,UAAU,qBAAqB,KAAK;AAE1C,WAAO,EAAE,SAAS,OAAO,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,EAC7E;AACD;AAWA,SAAS,qBAAqB,OAAwB;AACrD,QAAM,WAAY,MAA8B,SAAS;AAEzD,SAAO,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ;AACtE;AAGA,SAAS,gBAAyB,QAAuD;AACxF,QAAM,SAAS,OAAO,IAAI,YAAY;AAEtC,SAAO,EAAE,SAAS,OAAO,QAAQ,OAAO,IAAI,aAAa,GAAG,OAAO;AACpE;AAEA,SAAS,aAAa,OAAqC;AAC1D,SAAO,EAAE,MAAM,uBAAuB,MAAM,IAAI,GAAG,SAAS,MAAM,QAAQ;AAC3E;AAQA,SAAS,cAAc,OAAiC;AACvD,QAAM,OAAO,MAAM,KAAK,KAAK,GAAG;AAEhC,SAAO,OAAO,GAAG,IAAI,KAAK,MAAM,OAAO,KAAK,MAAM;AACnD;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lankajs/typebox",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "module: A bridge for the one library in the family with no Standard Schema, and a compiled checker it caches.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/lankajs/lanka.git",
|
|
10
|
+
"directory": "modules/validators/typebox"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/lankajs/lanka/tree/main/modules/validators/typebox#readme",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md",
|
|
26
|
+
"skills"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"lanka": "^1.3.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@lankajs/tool-testing": "^1.2.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@sinclair/typebox": "^0.34.52"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"lint": "eslint src _playground --max-warnings=0",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"bench": "vitest bench --run",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lanka-typebox
|
|
3
|
+
description: Validate server responses with TypeBox schemas in a lanka application. Use when adding validation to a gateway, when a lanka app's schemas are TypeBox, when core's validator cannot accept a TypeBox schema, when a response shape must be mapped into the application's own, or when reviewing code that imports `@lankajs/typebox`.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: lankajs
|
|
7
|
+
package: @lankajs/typebox
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# @lankajs/typebox
|
|
12
|
+
|
|
13
|
+
A bridge over TypeBox's compiled checker, plus `TLankaInferred`. `reference.md`
|
|
14
|
+
beside this file is the full guide.
|
|
15
|
+
|
|
16
|
+
> [!NOTE]
|
|
17
|
+
> Only what the framework or a gate refuses is binding. Everything else here is a
|
|
18
|
+
> recommendation you can adapt.
|
|
19
|
+
|
|
20
|
+
## Do you need it?
|
|
21
|
+
|
|
22
|
+
**Yes.** TypeBox publishes no `~standard` at all, and lanka's validation port
|
|
23
|
+
speaks Standard Schema and nothing else. There is no version of "just use
|
|
24
|
+
`lankaStandardValidator`" that compiles or runs.
|
|
25
|
+
|
|
26
|
+
Install this **or** another package from `modules/validators/`, never two.
|
|
27
|
+
|
|
28
|
+
## Declare schemas at MODULE level
|
|
29
|
+
|
|
30
|
+
This is the one rule that costs real performance if ignored.
|
|
31
|
+
`TypeCompiler.Compile(schema)` produces the fastest validator in JavaScript, and
|
|
32
|
+
compiling is the slow part. The package compiles each schema once and caches it
|
|
33
|
+
in a `WeakMap` keyed by the schema OBJECT.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// ✗ a new object every render — recompiled every render
|
|
37
|
+
const Screen = () => {
|
|
38
|
+
const schema = Type.Object({ id: Type.Number() });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ✓ compiled once
|
|
42
|
+
const screenSchema = Type.Object({ id: Type.Number() });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Use
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const todoSchema = Type.Object({
|
|
49
|
+
id: Type.Number(),
|
|
50
|
+
title: Type.String(),
|
|
51
|
+
done: Type.Boolean(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// in a gateway: a failing body is a broken contract, not a branch
|
|
55
|
+
const todo = lankaTypeBoxValidator.validate(todoSchema, body, "todos.byId");
|
|
56
|
+
|
|
57
|
+
// in a form: failure is ordinary
|
|
58
|
+
const result = lankaTypeBoxValidator.validateSafe(todoSchema, body);
|
|
59
|
+
if (!result.success) showErrors(result.errors);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
type ITodo = TLankaInferred<typeof todoSchema>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The third argument is the label. It appears in the error and the log, and it is
|
|
67
|
+
what turns "invalid response" into "which call".
|
|
68
|
+
|
|
69
|
+
## What a failure looks like
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
result.errors; // ["tags.0.id: Expected number"] — for a banner
|
|
73
|
+
result.fields; // [{ path: ["tags", 0, "id"], message }] — for a form
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
TypeBox reports a JSON Pointer — `/tags/0/id`. `path` is segments with the index
|
|
77
|
+
as a **number**, and `~1` / `~0` escapes are decoded. A root failure has an
|
|
78
|
+
**empty** path.
|
|
79
|
+
|
|
80
|
+
## Mapping a wire format
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const todoFromApi = Type.Transform(Type.Object({ todo_id: Type.Number() }))
|
|
84
|
+
.Decode((wire) => ({ id: wire.todo_id }))
|
|
85
|
+
.Encode((domain) => ({ todo_id: domain.id }));
|
|
86
|
+
|
|
87
|
+
const domain = lankaTypeBoxValidator.validate(todoFromApi, wire, "todos.map");
|
|
88
|
+
return lankaTypeBoxValidator.validate(todoSchema, domain, "todos.check");
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A schema without transforms never pays for the decode pass. A decode function
|
|
92
|
+
that throws is a refused body, not a crash.
|
|
93
|
+
|
|
94
|
+
## Never do these
|
|
95
|
+
|
|
96
|
+
- **Never build a schema inside a component body.** It is a new cache key on
|
|
97
|
+
every render.
|
|
98
|
+
- **Never reach for `lankaStandardValidator` with a TypeBox schema.** It cannot
|
|
99
|
+
take one.
|
|
100
|
+
- **Never validate outside the gateway.**
|
|
101
|
+
- **Never install two validation packages.**
|
|
102
|
+
- **Never expect `validate` to return a result object.** It throws;
|
|
103
|
+
`validateSafe` returns.
|
|
104
|
+
- **Never trust `format: "email"` without registering the format.** Unregistered,
|
|
105
|
+
it is reported as `Unknown format` and checks nothing. Use `pattern`, or
|
|
106
|
+
register it at start-up.
|
|
107
|
+
|
|
108
|
+
## Symptom → cause
|
|
109
|
+
|
|
110
|
+
| What you see | What it is |
|
|
111
|
+
| ------------------------------------------- | ------------------------------------------------ |
|
|
112
|
+
| validation slower than expected | a schema rebuilt per render — nothing is cached |
|
|
113
|
+
| "Unknown format" in a message | a `format` keyword nothing registered |
|
|
114
|
+
| a path like `/a/b` reaching your form | not from here — this package returns segments |
|
|
115
|
+
| "invalid response" with no idea which | a label that does not identify the call |
|
|
116
|
+
|
|
117
|
+
## More
|
|
118
|
+
|
|
119
|
+
`reference.md` — the full guide, including what the bridge does with JSON
|
|
120
|
+
Pointer escapes.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<!-- Generated from modules/validators/typebox/GUIDE.md by scripts/skills.mjs. Edit the guide. -->
|
|
2
|
+
|
|
3
|
+
> **`@lankajs/typebox@1.0.0`** — this document describes that version.
|
|
4
|
+
>
|
|
5
|
+
> Install: `npm install @lankajs/typebox @sinclair/typebox react zustand` (the peers are not optional; only npm adds a missing one for you).
|
|
6
|
+
>
|
|
7
|
+
> Complete code, compiled and run in CI: [modules/validators/typebox/_playground/playground.test.ts](https://github.com/lankajs/lanka/blob/main/modules/validators/typebox/_playground/playground.test.ts)
|
|
8
|
+
|
|
9
|
+
# @lankajs/typebox — user guide
|
|
10
|
+
|
|
11
|
+
The bridge for the one library in the family with no Standard Schema, over a
|
|
12
|
+
compiled checker it caches, plus a `TLankaInferred` helper.
|
|
13
|
+
|
|
14
|
+
## You will learn
|
|
15
|
+
|
|
16
|
+
- why this package is required rather than a matter of taste
|
|
17
|
+
- why your schemas must live at module level, and what it costs when they do not
|
|
18
|
+
- how to map a wire format with `Type.Transform`
|
|
19
|
+
|
|
20
|
+
## When to reach for this
|
|
21
|
+
|
|
22
|
+
The moment your schemas are TypeBox. Core's validator cannot be handed one.
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> Everything below is how this package is _meant_ to be used, not how it must
|
|
26
|
+
> be. The framework bends at the seams it publishes — see
|
|
27
|
+
> [ARCHITECTURE.md](https://github.com/lankajs/lanka/blob/main/ARCHITECTURE.md) for what is checked and what is
|
|
28
|
+
> merely advice.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @lankajs/typebox @sinclair/typebox
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Do I need it?
|
|
37
|
+
|
|
38
|
+
Yes. TypeBox publishes no `~standard` at all — not asynchronously as yup does,
|
|
39
|
+
not at all — and core's port speaks
|
|
40
|
+
[Standard Schema](https://standardschema.dev) and nothing else. There is no
|
|
41
|
+
version of "just use `lankaStandardValidator`" that compiles or runs.
|
|
42
|
+
|
|
43
|
+
## Use
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { lankaTypeBoxValidator } from "@lankajs/typebox";
|
|
47
|
+
import { Type } from "@sinclair/typebox";
|
|
48
|
+
|
|
49
|
+
// at MODULE level — see below, this is not a style point
|
|
50
|
+
const todoSchema = Type.Object({
|
|
51
|
+
id: Type.Number(),
|
|
52
|
+
title: Type.String(),
|
|
53
|
+
done: Type.Boolean(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// throws a LankaValidationError, with the label in the message
|
|
57
|
+
const todo = lankaTypeBoxValidator.validate(todoSchema, body, "todos.byId");
|
|
58
|
+
|
|
59
|
+
// or ask instead of throwing
|
|
60
|
+
const result = lankaTypeBoxValidator.validateSafe(todoSchema, body);
|
|
61
|
+
if (!result.success) showErrors(result.errors);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`validate` is what a gateway wants: a body that fails is a broken contract, not a
|
|
65
|
+
branch to handle. `validateSafe` is for a form, where failure is ordinary.
|
|
66
|
+
|
|
67
|
+
## Declare schemas at module level
|
|
68
|
+
|
|
69
|
+
`TypeCompiler.Compile(schema)` turns a schema into a generated function — the
|
|
70
|
+
fastest validator in JavaScript — and **compiling is the slow part**. This
|
|
71
|
+
package compiles each schema once and keeps the result in a `WeakMap` keyed by
|
|
72
|
+
the schema object.
|
|
73
|
+
|
|
74
|
+
A schema built inside a component body is a **new object on every render**, so it
|
|
75
|
+
is a new cache key and it is compiled again each time. The package's own bench
|
|
76
|
+
measures exactly that row, beside the cached one, so the cost is a number rather
|
|
77
|
+
than a warning.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// ✗ compiled on every render
|
|
81
|
+
const Screen = () => {
|
|
82
|
+
const schema = Type.Object({ id: Type.Number() });
|
|
83
|
+
…
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ✓ compiled once, for the life of the module
|
|
87
|
+
const screenSchema = Type.Object({ id: Type.Number() });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The map is **weak** on purpose: the key is your schema, and a strong map would
|
|
91
|
+
keep every schema any screen ever built alive for the life of the tab.
|
|
92
|
+
|
|
93
|
+
## Types
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import type { TLankaInferred } from "@lankajs/typebox";
|
|
97
|
+
|
|
98
|
+
type ITodo = TLankaInferred<typeof todoSchema>;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## What you get back when it fails
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
result.errors; // ["tags.0.id: Expected number", …] — for a banner
|
|
105
|
+
result.fields; // [{ path: ["tags", 0, "id"], message }] — for a form
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
TypeBox addresses a field with a JSON Pointer — `/tags/0/id`. `fields[].path` is
|
|
109
|
+
**segments**, with the index as a number, and the pointer's `~1` / `~0` escapes
|
|
110
|
+
are decoded, so a key containing a slash stays one field.
|
|
111
|
+
|
|
112
|
+
A failure at the root — `Type.Number()` refusing `"no"` — has an **empty** path:
|
|
113
|
+
the form's root, not an input named `""`.
|
|
114
|
+
|
|
115
|
+
## Mapping a wire format
|
|
116
|
+
|
|
117
|
+
A mapping is a schema, not an adapter layer. In TypeBox it is
|
|
118
|
+
`Type.Transform(...).Decode(...)`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const todoFromApi = Type.Transform(
|
|
122
|
+
Type.Object({ todo_id: Type.Number(), is_done: Type.Union([Type.Literal(0), Type.Literal(1)]) }),
|
|
123
|
+
)
|
|
124
|
+
.Decode((wire) => ({ id: wire.todo_id, done: wire.is_done === 1 }))
|
|
125
|
+
.Encode((domain) => ({ todo_id: domain.id, is_done: domain.done ? 1 : 0 }));
|
|
126
|
+
|
|
127
|
+
const domain = lankaTypeBoxValidator.validate(todoFromApi, wire, "todos.map");
|
|
128
|
+
return lankaTypeBoxValidator.validate(todoSchema, domain, "todos.check");
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A schema **without** transforms never pays for the decode pass: whether it has
|
|
132
|
+
one is asked once and cached beside the compiled checker, because `Value.Decode`
|
|
133
|
+
re-checks and calling it unconditionally would validate every body twice.
|
|
134
|
+
|
|
135
|
+
A decode function that throws is a **refused body**, not a crash — it comes back
|
|
136
|
+
as a failure, because `validateSafe` promises to throw nothing.
|
|
137
|
+
|
|
138
|
+
## Formats are a registry, not a keyword
|
|
139
|
+
|
|
140
|
+
`Type.String({ format: "email" })` checks nothing until the application registers
|
|
141
|
+
that format with TypeBox. Unregistered, it is reported as `Unknown format` — a
|
|
142
|
+
rule that looks present in the schema and is not. Use `pattern`, or register the
|
|
143
|
+
format at start-up.
|
|
144
|
+
|
|
145
|
+
## Where to validate
|
|
146
|
+
|
|
147
|
+
In the gateway. A gateway is where a body stops being `unknown`; validating in
|
|
148
|
+
the screen instead spreads the same three guards over every consumer, and each
|
|
149
|
+
one gets it slightly differently wrong.
|
|
150
|
+
|
|
151
|
+
## Common mistakes
|
|
152
|
+
|
|
153
|
+
**Building the schema inside the component.** It compiles every render. Module
|
|
154
|
+
level.
|
|
155
|
+
|
|
156
|
+
**Installing two packages from `modules/validators/`.** Pick one.
|
|
157
|
+
|
|
158
|
+
**Expecting `validate` to return a result object.** It throws. `validateSafe`
|
|
159
|
+
returns.
|
|
160
|
+
|
|
161
|
+
**Trusting `format` without registering it.**
|
|
162
|
+
|
|
163
|
+
## Recap
|
|
164
|
+
|
|
165
|
+
- TypeBox has no Standard Schema, so this package is what makes it work with lanka.
|
|
166
|
+
- Schemas at module level: the compiled checker is cached by object identity.
|
|
167
|
+
- Paths come back as segments, with JSON Pointer escapes decoded.
|
|
168
|
+
- A mapping is `Type.Transform`, and only a schema that has one pays for the decode pass.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Maintaining this package: [SKILL.md](https://github.com/lankajs/lanka/blob/main/modules/validators/typebox/SKILL.md) · What it is:
|
|
173
|
+
[README.md](https://github.com/lankajs/lanka/blob/main/modules/validators/typebox/README.md) · Repository map: [../../../README.md](https://github.com/lankajs/lanka/blob/main/README.md)
|