@nifrajs/schema 3.5.0 → 4.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/README.md +47 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +14 -2
- package/dist/adapter.js.map +1 -1
- package/dist/file-kind.d.ts +27 -0
- package/dist/file-kind.d.ts.map +1 -0
- package/dist/file-kind.js +35 -0
- package/dist/file-kind.js.map +1 -0
- package/dist/form.d.ts +119 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +301 -0
- package/dist/form.js.map +1 -0
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +3 -1
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +135 -89
- package/dist/openapi.js.map +1 -1
- package/dist/t.d.ts +31 -4
- package/dist/t.d.ts.map +1 -1
- package/dist/t.js +86 -18
- package/dist/t.js.map +1 -1
- package/package.json +11 -2
- package/src/adapter.ts +17 -2
- package/src/file-kind.ts +48 -0
- package/src/form.ts +394 -0
- package/src/formats.ts +3 -1
- package/src/index.ts +1 -1
- package/src/openapi.ts +164 -95
- package/src/t.ts +119 -19
package/dist/t.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import { Type, } from "@sinclair/typebox";
|
|
1
|
+
import { CloneType, Kind, Type, TypeRegistry, } from "@sinclair/typebox";
|
|
2
2
|
import { fromTypeBox } from "./adapter.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { fileOps, refuseFile } from "./file-kind.js";
|
|
4
|
+
const DEFERRED_KIND = "NifraDeferred";
|
|
5
|
+
/** Where `@nifrajs/web`'s output guard finds the inner schema's validate. Registered for two copies. */
|
|
6
|
+
const DEFERRED_VALIDATE = Symbol.for("nifra.schema.deferredValidate");
|
|
7
|
+
const isDeferredValue = (_, value) => typeof value === "object" &&
|
|
8
|
+
value !== null &&
|
|
9
|
+
value.__nifra_deferred === true &&
|
|
10
|
+
typeof value.promise?.then === "function";
|
|
11
|
+
/**
|
|
12
|
+
* Pull each property's raw TypeBox schema out of its `NifraSchema` wrapper. `where` names the calling
|
|
13
|
+
* constructor: these objects are checked as JSON, so a file field is refused here (it belongs in
|
|
14
|
+
* `t.form` from `@nifrajs/schema/form`) instead of failing every request later.
|
|
15
|
+
*/
|
|
16
|
+
function unwrap(where, props) {
|
|
5
17
|
const out = {};
|
|
6
18
|
// Object.entries → own enumerable string keys only (no prototype walk).
|
|
7
|
-
for (const [key, schema] of Object.entries(props))
|
|
19
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
20
|
+
refuseFile(where, schema.jsonSchema);
|
|
8
21
|
out[key] = schema.jsonSchema;
|
|
22
|
+
}
|
|
9
23
|
return out;
|
|
10
24
|
}
|
|
25
|
+
/** An open object whose scalar fields are coerced from strings - the shape of every request input
|
|
26
|
+
* that arrives as name/value text (a query string, a `Cookie` header). */
|
|
27
|
+
const textFields = (where) => (props, options) => fromTypeBox(Type.Object(unwrap(where, props), { additionalProperties: true, ...options }), {
|
|
28
|
+
coerce: true,
|
|
29
|
+
});
|
|
11
30
|
/**
|
|
12
31
|
* The built-in schema builder. Each constructor returns a `NifraSchema` - a
|
|
13
32
|
* Standard Schema whose validated output type flows into `c.body`/`c.query`, and
|
|
@@ -31,28 +50,46 @@ export const t = {
|
|
|
31
50
|
// rule. A body with extra keys fails validation (a 422), so `c.body` never carries attacker-supplied
|
|
32
51
|
// properties (no mass-assignment). Use `t.looseObject` (or pass `{ additionalProperties: true }`) to
|
|
33
52
|
// opt into an open object; an explicit `options.additionalProperties` always wins over the default.
|
|
34
|
-
object: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: false, ...options })),
|
|
53
|
+
object: (props, options) => fromTypeBox(Type.Object(unwrap("t.object", props), { additionalProperties: false, ...options })),
|
|
35
54
|
/** Like `t.object` but ACCEPTS (and passes through) unknown fields - the explicit opt-out of the
|
|
36
55
|
* strict default. Prefer `t.object` unless you genuinely need an open object. */
|
|
37
|
-
looseObject: (props, options) => fromTypeBox(Type.Object(unwrap(props), { additionalProperties: true, ...options })),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
looseObject: (props, options) => fromTypeBox(Type.Object(unwrap("t.looseObject", props), { additionalProperties: true, ...options })),
|
|
57
|
+
/**
|
|
58
|
+
* A list. A list of `t.file()` (from `@nifrajs/schema/form`) honors `minItems` / `maxItems` and is
|
|
59
|
+
* used inside `t.form`.
|
|
60
|
+
*/
|
|
61
|
+
array: (item, options) => fileOps("t.array", item)?.array(item, options) ??
|
|
62
|
+
fromTypeBox(Type.Array(item.jsonSchema, options)),
|
|
63
|
+
/** Marks a property optional inside `t.object` / `t.form`; standalone it is `T | undefined`. */
|
|
64
|
+
optional: (schema) => {
|
|
65
|
+
const plain = () => fromTypeBox(Type.Optional(schema.jsonSchema));
|
|
66
|
+
return (fileOps("t.optional", schema)?.optional(schema) ??
|
|
67
|
+
plain());
|
|
68
|
+
},
|
|
41
69
|
// `const S` captures the argument as a tuple; the explicit return type then maps
|
|
42
70
|
// over that captured tuple (`S[K]["jsonSchema"]`) so the union's `Static` is
|
|
43
71
|
// `A | B`, not `unknown` - the value-level `.map` can't preserve per-element
|
|
44
72
|
// types, so the output type is derived from `S` and the result cast to match
|
|
45
73
|
// (order/length are preserved by `map`, so the tuple shape is sound).
|
|
46
|
-
union: (schemas) => fromTypeBox(Type.Union(schemas.map((schema) =>
|
|
47
|
-
|
|
74
|
+
union: (schemas) => fromTypeBox(Type.Union(schemas.map((schema) => {
|
|
75
|
+
refuseFile("t.union", schema.jsonSchema);
|
|
76
|
+
return schema.jsonSchema;
|
|
77
|
+
}))),
|
|
78
|
+
record: (value, options) => {
|
|
79
|
+
refuseFile("t.record", value.jsonSchema);
|
|
80
|
+
return fromTypeBox(Type.Record(Type.String(), value.jsonSchema, options));
|
|
81
|
+
},
|
|
48
82
|
// Composed from TypeBox directly (not `t.object`/`t.array`) so the `t` literal doesn't reference
|
|
49
83
|
// itself during inference. Cursor pagination - not OFFSET - is the production default: stable under
|
|
50
84
|
// concurrent inserts and O(1) per page. Build pages with `paginate()` + `encodeCursor`/`decodeCursor`.
|
|
51
85
|
/** A cursor-pagination response envelope: `{ items: T[]; nextCursor: string | null }` (`null` = last page). */
|
|
52
|
-
paginated: (item, options) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
86
|
+
paginated: (item, options) => {
|
|
87
|
+
refuseFile("t.paginated", item.jsonSchema);
|
|
88
|
+
return fromTypeBox(Type.Object({
|
|
89
|
+
items: Type.Array(item.jsonSchema),
|
|
90
|
+
nextCursor: Type.Union([Type.String(), Type.Null()]),
|
|
91
|
+
}, { additionalProperties: false, ...options }));
|
|
92
|
+
},
|
|
56
93
|
/** A request query schema for cursor pagination: `{ cursor?: string; limit?: number }`. `maxLimit`
|
|
57
94
|
* caps `limit` - a larger value fails validation (a 422), so a client can't request an unbounded page.
|
|
58
95
|
* `coerce` is on because query values arrive as strings (`?limit=20` → `"20"`); it's what makes `limit`
|
|
@@ -70,8 +107,39 @@ export const t = {
|
|
|
70
107
|
* traffic appends params your schema never declares, causing 422s that never appear in dev/CI). Pass
|
|
71
108
|
* `{ additionalProperties: false }` to enforce a strict allowlist. This is the query-slot constructor;
|
|
72
109
|
* `t.object` stays the constructor for body slots (no coercion, a JSON body is already typed). */
|
|
73
|
-
query: (
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
query: textFields("t.query"),
|
|
111
|
+
/** A request-cookie schema for the `cookies` route slot, with the same string->scalar COERCION as
|
|
112
|
+
* `t.query` (cookie values always arrive as strings). **Open by default**: a browser sends every
|
|
113
|
+
* cookie the site has set - analytics, consent, other routes' sessions - so a strict allowlist
|
|
114
|
+
* would 422 real traffic. Declare the cookies the route reads; the rest pass through. */
|
|
115
|
+
cookies: textFields("t.cookies"),
|
|
116
|
+
/**
|
|
117
|
+
* A loader value marked with `defer()`, streamed in after the shell. `inner` describes what the
|
|
118
|
+
* promise resolves to; `@nifrajs/web` projects and validates the resolved value by it before it
|
|
119
|
+
* streams, so a deferred value is held to the same output contract as the rest of the data.
|
|
120
|
+
*/
|
|
121
|
+
deferred: (inner) => {
|
|
122
|
+
refuseFile("t.deferred", inner.jsonSchema);
|
|
123
|
+
if (!TypeRegistry.Has(DEFERRED_KIND))
|
|
124
|
+
TypeRegistry.Set(DEFERRED_KIND, isDeferredValue);
|
|
125
|
+
return fromTypeBox(Type.Unsafe({
|
|
126
|
+
[Kind]: DEFERRED_KIND,
|
|
127
|
+
"x-nifra-deferred": true,
|
|
128
|
+
inner: inner.jsonSchema,
|
|
129
|
+
[DEFERRED_VALIDATE]: inner["~standard"].validate,
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* Allow a field with a sensitive name (`token`, `password`, `apiKey`, ...) in an output schema.
|
|
134
|
+
* An output schema that declares one without this fails at route load; `reason` records why this
|
|
135
|
+
* value may reach the browser, for the reviewer who finds it later.
|
|
136
|
+
*/
|
|
137
|
+
declassified: (reason, schema) => {
|
|
138
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
139
|
+
throw new TypeError("t.declassified: give the reason this field may reach the browser");
|
|
140
|
+
}
|
|
141
|
+
refuseFile("t.declassified", schema.jsonSchema);
|
|
142
|
+
return fromTypeBox(CloneType(schema.jsonSchema, { "x-nifra-declassified": reason }));
|
|
143
|
+
},
|
|
76
144
|
};
|
|
77
145
|
//# sourceMappingURL=t.js.map
|
package/dist/t.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"t.js","sourceRoot":"","sources":["../src/t.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"t.js","sourceRoot":"","sources":["../src/t.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EAET,IAAI,EAUJ,IAAI,EACJ,YAAY,GACb,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAoB,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAWpD,MAAM,aAAa,GAAG,eAAe,CAAA;AACrC,wGAAwG;AACxG,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;AAErE,MAAM,eAAe,GAAG,CAAC,CAAU,EAAE,KAAc,EAAW,EAAE,CAC9D,OAAO,KAAK,KAAK,QAAQ;IACzB,KAAK,KAAK,IAAI;IACb,KAAiD,CAAC,gBAAgB,KAAK,IAAI;IAC5E,OAAQ,KAA4D,CAAC,OAAO,EAAE,IAAI,KAAK,UAAU,CAAA;AAEnG;;;;GAIG;AACH,SAAS,MAAM,CAAkB,KAAa,EAAE,KAAQ;IACtD,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,wEAAwE;IACxE,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;QACpC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,UAAU,CAAA;IAC9B,CAAC;IACD,OAAO,GAA6C,CAAA;AACtD,CAAC;AAED;0EAC0E;AAC1E,MAAM,UAAU,GACd,CAAC,KAAa,EAAE,EAAE,CAClB,CAAkB,KAAQ,EAAE,OAAuB,EAAE,EAAE,CACrD,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,EAAE;IACzF,MAAM,EAAE,IAAI;CACb,CAAC,CAAA;AAEN;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,CAAC,GAAG;IACf,MAAM,EAAE,CAAC,OAAuB,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtE,MAAM,EAAE,CAAC,OAAuB,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO,EAAE,CAAC,OAAwB,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;IAC1C,IAAI,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACpC,OAAO,EAAE,CAAgC,KAAQ,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAEtF,oGAAoG;IACpG,qGAAqG;IACrG,qGAAqG;IACrG,oGAAoG;IACpG,MAAM,EAAE,CAAkB,KAAQ,EAAE,OAAuB,EAAE,EAAE,CAC7D,WAAW,CACT,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,CACpF;IACH;qFACiF;IACjF,WAAW,EAAE,CAAkB,KAAQ,EAAE,OAAuB,EAAE,EAAE,CAClE,WAAW,CACT,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CACxF;IACH;;;OAGG;IACH,KAAK,EAAE,CACL,IAAoB,EACpB,OAAsB,EACE,EAAE,CACzB,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,CAAwC;QACtF,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACnD,gGAAgG;IAChG,QAAQ,EAAE,CAAoB,MAAsB,EAAE,EAAE;QACtD,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;QACjE,OAAO,CACJ,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAA0C;YACzF,KAAK,EAAE,CACR,CAAA;IACH,CAAC;IACD,iFAAiF;IACjF,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,sEAAsE;IACtE,KAAK,EAAE,CAAyC,OAAU,EAAE,EAAE,CAC5D,WAAW,CACT,IAAI,CAAC,KAAK,CACP,OAAkC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACjD,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;QACxC,OAAO,MAAM,CAAC,UAAU,CAAA;IAC1B,CAAC,CAAC,CACH,CACuE;IAC5E,MAAM,EAAE,CAAoB,KAAqB,EAAE,OAAuB,EAAE,EAAE;QAC5E,UAAU,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QACxC,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAA;IAC3E,CAAC;IAED,iGAAiG;IACjG,oGAAoG;IACpG,uGAAuG;IACvG,+GAA+G;IAC/G,SAAS,EAAE,CAAoB,IAAoB,EAAE,OAAuB,EAAE,EAAE;QAC9E,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QAC1C,OAAO,WAAW,CAChB,IAAI,CAAC,MAAM,CACT;YACE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;YAClC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;SACrD,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAC5C,CACF,CAAA;IACH,CAAC;IACD;;;yEAGqE;IACrE,SAAS,EAAE,CAAC,OAA+B,EAAE,EAAE,CAC7C,WAAW,CACT,IAAI,CAAC,MAAM,CACT;QACE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACpC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,CAAC,CAAC;KACtF,EACD,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAChC,EACD,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB;IACH;;;;;;;;sGAQkG;IAClG,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC;IAC5B;;;6FAGyF;IACzF,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC;IAEhC;;;;OAIG;IACH,QAAQ,EAAE,CACR,KAAqB,EAC2B,EAAE;QAClD,UAAU,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QAC1C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC;YAAE,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;QACtF,OAAO,WAAW,CAChB,IAAI,CAAC,MAAM,CAA2B;YACpC,CAAC,IAAI,CAAC,EAAE,aAAa;YACrB,kBAAkB,EAAE,IAAI;YACxB,KAAK,EAAE,KAAK,CAAC,UAAU;YACvB,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,QAAQ;SACjD,CAAC,CACH,CAAA;IACH,CAAC;IACD;;;;OAIG;IACH,YAAY,EAAE,CAAoB,MAAc,EAAE,MAAsB,EAAkB,EAAE;QAC1F,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACvD,MAAM,IAAI,SAAS,CAAC,kEAAkE,CAAC,CAAA;QACzF,CAAC;QACD,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;QAC/C,OAAO,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,sBAAsB,EAAE,MAAM,EAAE,CAAM,CAAC,CAAA;IAC3F,CAAC;CACO,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/schema",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Built-in schema builder `t` - TypeBox-backed (free JSON Schema + compiled validators), implements Standard Schema so nifra routes validate it natively.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"standard-schema"
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
|
+
"nifra": {
|
|
20
|
+
"environment": "library"
|
|
21
|
+
},
|
|
19
22
|
"main": "./dist/index.js",
|
|
20
23
|
"module": "./dist/index.js",
|
|
21
24
|
"types": "./dist/index.d.ts",
|
|
@@ -25,6 +28,11 @@
|
|
|
25
28
|
"types": "./dist/index.d.ts",
|
|
26
29
|
"default": "./dist/index.js"
|
|
27
30
|
},
|
|
31
|
+
"./form": {
|
|
32
|
+
"bun": "./src/form.ts",
|
|
33
|
+
"types": "./dist/form.d.ts",
|
|
34
|
+
"default": "./dist/form.js"
|
|
35
|
+
},
|
|
28
36
|
"./openapi": {
|
|
29
37
|
"bun": "./src/openapi.ts",
|
|
30
38
|
"types": "./dist/openapi.d.ts",
|
|
@@ -45,10 +53,11 @@
|
|
|
45
53
|
"access": "public"
|
|
46
54
|
},
|
|
47
55
|
"dependencies": {
|
|
56
|
+
"@nifrajs/uploads": "4.0.0",
|
|
48
57
|
"@sinclair/typebox": "^0.34.49"
|
|
49
58
|
},
|
|
50
59
|
"peerDependencies": {
|
|
51
|
-
"@nifrajs/core": "^
|
|
60
|
+
"@nifrajs/core": "^4.0.0"
|
|
52
61
|
},
|
|
53
62
|
"devDependencies": {
|
|
54
63
|
"@nifrajs/core": "workspace:*",
|
package/src/adapter.ts
CHANGED
|
@@ -34,6 +34,21 @@ function toIssue(error: { readonly message: string; readonly path: string }): St
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// The issues past this are never reported (a 422 lists 100), so walking them is wasted work on a
|
|
38
|
+
// large hostile body.
|
|
39
|
+
const MAX_ISSUES = 100
|
|
40
|
+
|
|
41
|
+
function firstIssues(
|
|
42
|
+
errors: Iterable<{ readonly message: string; readonly path: string }>,
|
|
43
|
+
): StandardIssue[] {
|
|
44
|
+
const issues: StandardIssue[] = []
|
|
45
|
+
for (const error of errors) {
|
|
46
|
+
issues.push(toIssue(error))
|
|
47
|
+
if (issues.length === MAX_ISSUES) break
|
|
48
|
+
}
|
|
49
|
+
return issues
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
// ---------------------------------------------------------------------------------------------
|
|
38
53
|
// Precompiled coercion for flat scalar objects - the shape every real query schema has.
|
|
39
54
|
//
|
|
@@ -230,10 +245,10 @@ export function fromTypeBox<T extends TSchema>(
|
|
|
230
245
|
}
|
|
231
246
|
if (compiled !== undefined) {
|
|
232
247
|
if (compiled.Check(input)) return { value: input as Static<T> }
|
|
233
|
-
return { issues:
|
|
248
|
+
return { issues: firstIssues(compiled.Errors(input)) }
|
|
234
249
|
}
|
|
235
250
|
if (Value.Check(schema, input)) return { value: input as Static<T> }
|
|
236
|
-
return { issues:
|
|
251
|
+
return { issues: firstIssues(Value.Errors(schema, input)) }
|
|
237
252
|
},
|
|
238
253
|
// Phantom: `types` carries no runtime value; this cast supplies the
|
|
239
254
|
// compile-time `Static<T>` that nifra's `InferOutput` reads to type `c.body`.
|
package/src/file-kind.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `t` tells a file field apart without carrying the code that validates one.
|
|
3
|
+
*
|
|
4
|
+
* File fields come from `@nifrajs/schema/form`. They mark their JSON Schema, and the schema object
|
|
5
|
+
* brings its own list and optional constructors along, so `t.array` and `t.optional` handle a file
|
|
6
|
+
* while an app that never takes an upload ships none of that code.
|
|
7
|
+
*/
|
|
8
|
+
import { type ArrayOptions, Kind, type TSchema } from "@sinclair/typebox"
|
|
9
|
+
import type { NifraSchema } from "./adapter.ts"
|
|
10
|
+
|
|
11
|
+
/** Marks the JSON Schema of a file field. Registered, so two copies of this package agree on it. */
|
|
12
|
+
export const FILE: unique symbol = Symbol.for("nifra.schema.file")
|
|
13
|
+
|
|
14
|
+
/** Carries the file-aware constructors on a file schema. Registered for the same reason. */
|
|
15
|
+
export const FILE_OPS: unique symbol = Symbol.for("nifra.schema.fileOps")
|
|
16
|
+
|
|
17
|
+
export interface FileOps {
|
|
18
|
+
optional(schema: NifraSchema): NifraSchema
|
|
19
|
+
array(item: NifraSchema, options?: ArrayOptions): NifraSchema
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Marked = { readonly [FILE]?: true; readonly [Kind]?: string; readonly items?: TSchema }
|
|
23
|
+
|
|
24
|
+
/** Whether a JSON Schema is a file field, or a list of them. */
|
|
25
|
+
export function isFileSchema(schema: TSchema): boolean {
|
|
26
|
+
const node = schema as unknown as Marked
|
|
27
|
+
return (
|
|
28
|
+
node[FILE] === true ||
|
|
29
|
+
(node[Kind] === "Array" && node.items !== undefined && isFileSchema(node.items))
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Throw when a constructor that validates JSON is handed a file field. */
|
|
34
|
+
export function refuseFile(where: string, schema: TSchema): void {
|
|
35
|
+
if (isFileSchema(schema)) {
|
|
36
|
+
throw new TypeError(`${where}: a file field belongs in t.form() from "@nifrajs/schema/form"`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The file-aware constructors of a file schema, or `undefined` for any other schema. A schema whose
|
|
42
|
+
* JSON Schema says "file" but which lost its validator is refused: TypeBox cannot check a file.
|
|
43
|
+
*/
|
|
44
|
+
export function fileOps(where: string, schema: NifraSchema): FileOps | undefined {
|
|
45
|
+
const ops = (schema as { readonly [FILE_OPS]?: FileOps })[FILE_OPS]
|
|
46
|
+
if (ops === undefined) refuseFile(where, schema.jsonSchema)
|
|
47
|
+
return ops
|
|
48
|
+
}
|