@raven.js/cli 1.1.2 → 1.2.1
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 +10 -7
- package/dist/raven +33 -109
- package/dist/raven.map +3 -3
- package/dist/registry.json +1 -9
- package/dist/source/core/GUIDE.md +14 -0
- package/dist/source/core/PLUGIN.md +225 -0
- package/dist/source/core/README.md +427 -0
- package/dist/source/core/index.ts +624 -0
- package/dist/source/core/router.ts +128 -0
- package/dist/source/schema-validator/GUIDE.md +12 -0
- package/dist/source/schema-validator/README.md +229 -0
- package/dist/source/schema-validator/index.ts +139 -0
- package/dist/source/schema-validator/standard-schema.ts +76 -0
- package/dist/source/sql/GUIDE.md +12 -0
- package/dist/source/sql/README.md +271 -0
- package/dist/source/sql/index.ts +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Radix Router - Efficient URL pattern matching
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Result of a successful route match.
|
|
7
|
+
* @template T The type of data associated with the route (e.g., handler and pipeline).
|
|
8
|
+
*/
|
|
9
|
+
export interface RouteMatch<T> {
|
|
10
|
+
/** The data payload stored for the matched route. */
|
|
11
|
+
data: T;
|
|
12
|
+
/** Extracted path parameters. */
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Internal tree node for the Radix router.
|
|
18
|
+
* @template T The type of data stored at the node.
|
|
19
|
+
*/
|
|
20
|
+
class RouterNode<T> {
|
|
21
|
+
children: Map<string, RouterNode<T>> = new Map();
|
|
22
|
+
paramChild: RouterNode<T> | null = null;
|
|
23
|
+
wildcardChild: RouterNode<T> | null = null;
|
|
24
|
+
paramName: string | null = null;
|
|
25
|
+
handlers: Map<string, T> = new Map();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inserts a route into the node tree.
|
|
29
|
+
*/
|
|
30
|
+
insert(segments: string[], method: string, data: T) {
|
|
31
|
+
let current: RouterNode<T> = this;
|
|
32
|
+
|
|
33
|
+
for (const segment of segments) {
|
|
34
|
+
if (segment.startsWith(":")) {
|
|
35
|
+
if (!current.paramChild) {
|
|
36
|
+
current.paramChild = new RouterNode<T>();
|
|
37
|
+
current.paramName = segment.slice(1);
|
|
38
|
+
}
|
|
39
|
+
current = current.paramChild;
|
|
40
|
+
} else if (segment === "*") {
|
|
41
|
+
if (!current.wildcardChild) {
|
|
42
|
+
current.wildcardChild = new RouterNode<T>();
|
|
43
|
+
}
|
|
44
|
+
current = current.wildcardChild;
|
|
45
|
+
break;
|
|
46
|
+
} else {
|
|
47
|
+
if (!current.children.has(segment)) {
|
|
48
|
+
current.children.set(segment, new RouterNode<T>());
|
|
49
|
+
}
|
|
50
|
+
current = current.children.get(segment)!;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
current.handlers.set(method.toUpperCase(), data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Searches for a matching route in the node tree.
|
|
59
|
+
*/
|
|
60
|
+
search(
|
|
61
|
+
segments: string[],
|
|
62
|
+
method: string,
|
|
63
|
+
params: Record<string, string>,
|
|
64
|
+
): T | null {
|
|
65
|
+
let current: RouterNode<T> = this;
|
|
66
|
+
|
|
67
|
+
for (const segment of segments) {
|
|
68
|
+
const next = current.children.get(segment);
|
|
69
|
+
if (next) {
|
|
70
|
+
current = next;
|
|
71
|
+
} else if (current.paramChild) {
|
|
72
|
+
if (current.paramName) {
|
|
73
|
+
params[current.paramName] = segment;
|
|
74
|
+
}
|
|
75
|
+
current = current.paramChild;
|
|
76
|
+
} else if (current.wildcardChild) {
|
|
77
|
+
current = current.wildcardChild;
|
|
78
|
+
return current.handlers.get(method.toUpperCase()) || null;
|
|
79
|
+
} else {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return current.handlers.get(method.toUpperCase()) || null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Radix tree based router for efficient URL pattern matching.
|
|
90
|
+
* @template T The type of data associated with each route.
|
|
91
|
+
*/
|
|
92
|
+
export class RadixRouter<T> {
|
|
93
|
+
private root = new RouterNode<T>();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adds a new route to the router.
|
|
97
|
+
* @param method HTTP method.
|
|
98
|
+
* @param path URL path pattern.
|
|
99
|
+
* @param data Data payload to store with the route.
|
|
100
|
+
*/
|
|
101
|
+
add(method: string, path: string, data: T) {
|
|
102
|
+
const segments = this.splitPath(path);
|
|
103
|
+
this.root.insert(segments, method, data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Finds a matching route for the given method and path.
|
|
108
|
+
* @param method HTTP method.
|
|
109
|
+
* @param path URL path to match.
|
|
110
|
+
* @returns RouteMatch containing the data and extracted parameters, or null if not found.
|
|
111
|
+
*/
|
|
112
|
+
find(method: string, path: string): RouteMatch<T> | null {
|
|
113
|
+
const segments = this.splitPath(path);
|
|
114
|
+
const params: Record<string, string> = {};
|
|
115
|
+
const data = this.root.search(segments, method, params);
|
|
116
|
+
|
|
117
|
+
if (data === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { data, params };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Helper to split a path into normalized segments. */
|
|
125
|
+
private splitPath(path: string): string[] {
|
|
126
|
+
return path.split("/").filter((s) => s.length > 0);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# REQUIRED READING
|
|
2
|
+
|
|
3
|
+
| File | When to Read |
|
|
4
|
+
| ------------------------ | ----------------------------------------------------- |
|
|
5
|
+
| [README.md](./README.md) | Quick start, understand basic usage |
|
|
6
|
+
| [index.ts](./index.ts) | Understand core API (`withSchema`, `ValidationError`) |
|
|
7
|
+
|
|
8
|
+
# OPTIONAL READING
|
|
9
|
+
|
|
10
|
+
| File | When to Read |
|
|
11
|
+
| ------------------------------------------ | -------------------------------------------------------------------- |
|
|
12
|
+
| [standard-schema.ts](./standard-schema.ts) | Learn Standard Schema interface spec, or implement custom validators |
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# OVERVIEW
|
|
2
|
+
|
|
3
|
+
@raven.js/schema-validator is a framework-agnostic validation module for RavenJS, built on [Standard Schema](https://standardschema.dev).
|
|
4
|
+
|
|
5
|
+
**Features**:
|
|
6
|
+
- **Library Agnostic**: Works with Zod, Valibot, ArkType, or any Standard Schema compliant library.
|
|
7
|
+
- **Full Request Validation**: Validates Body, Query, Params, and Headers.
|
|
8
|
+
- **Type Safety**: Infers types from schemas for the handler context.
|
|
9
|
+
- **Automatic Error Handling**: Throws structured `ValidationError` on failure.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ARCHITECTURE
|
|
14
|
+
|
|
15
|
+
**Validation lifecycle**:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
incoming request
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
[processStates] ← Core populates BodyState / QueryState / etc.
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
[withSchema wrapper] ← Intercepts execution
|
|
25
|
+
│
|
|
26
|
+
├─► Validates Body/Query/Params/Headers against schemas
|
|
27
|
+
│ │
|
|
28
|
+
│ ▼
|
|
29
|
+
│ Validation Failed ──► Throws ValidationError ──► [onError hook]
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
Validation Passed
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
[handler(ctx)] ← Receives typed context with validated data
|
|
36
|
+
│
|
|
37
|
+
▼
|
|
38
|
+
outgoing response
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# CORE CONCEPTS
|
|
44
|
+
|
|
45
|
+
## Standard Schema
|
|
46
|
+
|
|
47
|
+
This module relies on the [Standard Schema](https://standardschema.dev) interface. This means it doesn't depend on a specific validation library. You can use:
|
|
48
|
+
|
|
49
|
+
- **Zod**: `bun add zod`
|
|
50
|
+
- **Valibot**: `bun add valibot`
|
|
51
|
+
- **ArkType**: `bun add arktype`
|
|
52
|
+
|
|
53
|
+
Any library that implements the Standard Schema spec works out of the box.
|
|
54
|
+
|
|
55
|
+
## `withSchema` Wrapper
|
|
56
|
+
|
|
57
|
+
The primary API is a higher-order function that wraps your handler. It transforms a **schema-aware handler** (which accepts a context argument) into a **standard RavenJS handler** (zero-argument).
|
|
58
|
+
|
|
59
|
+
It is recommended to define the handler directly within `withSchema` to leverage type inference automatically:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const schema = {
|
|
63
|
+
body: z.object({ name: z.string() })
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Define handler inline for automatic type inference
|
|
67
|
+
const createHandler = withSchema(schema, (ctx) => {
|
|
68
|
+
// ctx.body is automatically typed as { name: string }
|
|
69
|
+
return Response.json(ctx.body);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.post("/route", createHandler);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Context Integration
|
|
76
|
+
|
|
77
|
+
While RavenJS Core uses global state (`BodyState`, etc.) for dependency injection, `schema-validator` passes a **typed context object** to your handler. This provides the best of both worlds:
|
|
78
|
+
|
|
79
|
+
- **Type Inference**: The `ctx` argument matches your schema types.
|
|
80
|
+
- **Runtime Integration**: The validator reads from RavenJS's internal states automatically.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# DESIGN DECISIONS
|
|
85
|
+
|
|
86
|
+
## Why Standard Schema?
|
|
87
|
+
|
|
88
|
+
By adopting Standard Schema, RavenJS avoids vendor lock-in. You are not forced to use Zod or any specific library. This aligns with RavenJS's philosophy of being a "reference implementation" that is flexible and adaptable.
|
|
89
|
+
|
|
90
|
+
## Why a wrapper function instead of middleware?
|
|
91
|
+
|
|
92
|
+
RavenJS hooks (`beforeHandle`) are void functions that cannot easily pass typed data to the handler.
|
|
93
|
+
|
|
94
|
+
- **Middleware approach**: Validation runs in a hook, puts result in a weak-map or untyped state. Handler manually casts data.
|
|
95
|
+
- **Wrapper approach**: The wrapper function *knows* the schema types and passes them directly to the handler function as an argument. This enables 100% type safety without manual casting.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# GOTCHAS
|
|
100
|
+
|
|
101
|
+
## 1. Validation throws `ValidationError`
|
|
102
|
+
|
|
103
|
+
When validation fails, `withSchema` throws a `ValidationError`. It does **not** return a 400 Response automatically. You must handle this error, typically in a global `onError` hook.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { isValidationError } from "@raven.js/schema-validator";
|
|
107
|
+
|
|
108
|
+
app.onError((err) => {
|
|
109
|
+
if (isValidationError(err)) {
|
|
110
|
+
return Response.json({ issues: err.bodyIssues }, { status: 400 });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 2. Handler signature changes
|
|
116
|
+
|
|
117
|
+
When using `withSchema`, your handler function **must** accept a context argument, unlike standard RavenJS handlers which are zero-argument.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Standard handler
|
|
121
|
+
const standard = () => new Response();
|
|
122
|
+
|
|
123
|
+
// Schema handler
|
|
124
|
+
const schemaHandler = (ctx) => new Response();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 3. Schema libraries must be installed separately
|
|
128
|
+
|
|
129
|
+
This package does not bundle Zod or Valibot. You must install your preferred validation library in your project.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
# USAGE EXAMPLES
|
|
134
|
+
|
|
135
|
+
## Minimal (Zod)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { z } from "zod";
|
|
139
|
+
import { withSchema } from "@raven.js/schema-validator";
|
|
140
|
+
import { Raven } from "@raven.js/core";
|
|
141
|
+
|
|
142
|
+
const schema = {
|
|
143
|
+
body: z.object({
|
|
144
|
+
username: z.string(),
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Define handler directly inside withSchema for best developer experience
|
|
149
|
+
const createUser = withSchema(schema, (ctx) => {
|
|
150
|
+
// ctx.body is typed: { username: string }
|
|
151
|
+
return new Response(`Hello ${ctx.body.username}`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const app = new Raven();
|
|
155
|
+
app.post("/user", createUser);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Validating Multiple Sources
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const schema = {
|
|
162
|
+
body: z.object({ name: z.string() }),
|
|
163
|
+
query: z.object({ page: z.string().transform(Number) }),
|
|
164
|
+
headers: z.object({ "x-api-key": z.string() }),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handler = withSchema(schema, (ctx) => {
|
|
168
|
+
const { name } = ctx.body;
|
|
169
|
+
const { page } = ctx.query;
|
|
170
|
+
const apiKey = ctx.headers["x-api-key"];
|
|
171
|
+
return Response.json({ name, page, apiKey });
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Global Error Handling
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { isValidationError } from "@raven.js/schema-validator";
|
|
179
|
+
|
|
180
|
+
app.onError((error) => {
|
|
181
|
+
if (isValidationError(error)) {
|
|
182
|
+
return new Response(JSON.stringify({
|
|
183
|
+
error: "Validation Failed",
|
|
184
|
+
details: {
|
|
185
|
+
body: error.bodyIssues,
|
|
186
|
+
query: error.queryIssues,
|
|
187
|
+
}
|
|
188
|
+
}), {
|
|
189
|
+
status: 400,
|
|
190
|
+
headers: { "Content-Type": "application/json" }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return new Response("Internal Error", { status: 500 });
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
# ANTI-PATTERNS
|
|
200
|
+
|
|
201
|
+
## Do not validate manually inside handler
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// ❌ Manual validation loses type inference benefits and clutters logic
|
|
205
|
+
app.post("/user", async () => {
|
|
206
|
+
const rawBody = BodyState.getOrFailed();
|
|
207
|
+
const result = UserSchema.safeParse(rawBody); // ❌
|
|
208
|
+
if (!result.success) return new Response("Error", { status: 400 });
|
|
209
|
+
// ...
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ✓ Use withSchema to separate validation from logic
|
|
213
|
+
app.post("/user", withSchema({ body: UserSchema }, (ctx) => {
|
|
214
|
+
// ...
|
|
215
|
+
}));
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Do not separate handler definition from schema
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// ❌ Defining handler separately requires manual type definition or complex inference
|
|
222
|
+
const handler = (ctx: any) => { ... };
|
|
223
|
+
const wrapped = withSchema(schema, handler);
|
|
224
|
+
|
|
225
|
+
// ✓ Define inline for automatic type inference
|
|
226
|
+
const wrapped = withSchema(schema, (ctx) => {
|
|
227
|
+
// ctx is fully typed here!
|
|
228
|
+
});
|
|
229
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BodyState,
|
|
3
|
+
QueryState,
|
|
4
|
+
ParamsState,
|
|
5
|
+
HeadersState,
|
|
6
|
+
} from "@raven.js/core";
|
|
7
|
+
import type { StandardSchemaV1 } from "./standard-schema";
|
|
8
|
+
|
|
9
|
+
export interface Context<
|
|
10
|
+
B = unknown,
|
|
11
|
+
Q = Record<string, string>,
|
|
12
|
+
P = Record<string, string>,
|
|
13
|
+
H = Record<string, string>,
|
|
14
|
+
> {
|
|
15
|
+
body: B;
|
|
16
|
+
query: Q;
|
|
17
|
+
params: P;
|
|
18
|
+
headers: H;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Schemas<
|
|
22
|
+
B = unknown,
|
|
23
|
+
Q = Record<string, string>,
|
|
24
|
+
P = Record<string, string>,
|
|
25
|
+
H = Record<string, string>,
|
|
26
|
+
> {
|
|
27
|
+
body?: StandardSchemaV1<unknown, B>;
|
|
28
|
+
query?: StandardSchemaV1<Record<string, string>, Q>;
|
|
29
|
+
params?: StandardSchemaV1<Record<string, string>, P>;
|
|
30
|
+
headers?: StandardSchemaV1<Record<string, string>, H>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SchemaHandler<B, Q, P, H> = (
|
|
34
|
+
ctx: Context<B, Q, P, H>,
|
|
35
|
+
) => Response | Promise<Response>;
|
|
36
|
+
|
|
37
|
+
export type ValidationSource = "body" | "query" | "params" | "headers";
|
|
38
|
+
|
|
39
|
+
export class ValidationError extends Error {
|
|
40
|
+
public readonly bodyIssues?: readonly StandardSchemaV1.Issue[];
|
|
41
|
+
public readonly queryIssues?: readonly StandardSchemaV1.Issue[];
|
|
42
|
+
public readonly paramsIssues?: readonly StandardSchemaV1.Issue[];
|
|
43
|
+
public readonly headersIssues?: readonly StandardSchemaV1.Issue[];
|
|
44
|
+
|
|
45
|
+
constructor(results: {
|
|
46
|
+
body?: StandardSchemaV1.FailureResult;
|
|
47
|
+
query?: StandardSchemaV1.FailureResult;
|
|
48
|
+
params?: StandardSchemaV1.FailureResult;
|
|
49
|
+
headers?: StandardSchemaV1.FailureResult;
|
|
50
|
+
}) {
|
|
51
|
+
const allIssues = [
|
|
52
|
+
...(results.body?.issues ?? []),
|
|
53
|
+
...(results.query?.issues ?? []),
|
|
54
|
+
...(results.params?.issues ?? []),
|
|
55
|
+
...(results.headers?.issues ?? []),
|
|
56
|
+
];
|
|
57
|
+
const message = allIssues.map((issue) => issue.message).join(", ");
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "ValidationError";
|
|
60
|
+
this.bodyIssues = results.body?.issues;
|
|
61
|
+
this.queryIssues = results.query?.issues;
|
|
62
|
+
this.paramsIssues = results.params?.issues;
|
|
63
|
+
this.headersIssues = results.headers?.issues;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function validateSchema<T>(
|
|
68
|
+
schema: StandardSchemaV1<unknown, T> | undefined,
|
|
69
|
+
value: unknown,
|
|
70
|
+
): Promise<StandardSchemaV1.Result<T> | undefined> {
|
|
71
|
+
if (!schema) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return schema["~standard"].validate(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isValidationError(value: unknown): value is ValidationError {
|
|
79
|
+
return value instanceof ValidationError;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function withSchema<B, Q, P, H>(
|
|
83
|
+
schemas: Schemas<B, Q, P, H>,
|
|
84
|
+
handler: SchemaHandler<B, Q, P, H>,
|
|
85
|
+
): () => Promise<Response> {
|
|
86
|
+
return async () => {
|
|
87
|
+
const body = BodyState.get();
|
|
88
|
+
const query = QueryState.get() ?? {};
|
|
89
|
+
const params = ParamsState.get() ?? {};
|
|
90
|
+
const headers = HeadersState.get() ?? {};
|
|
91
|
+
|
|
92
|
+
const [bodyResult, queryResult, paramsResult, headersResult] =
|
|
93
|
+
await Promise.all([
|
|
94
|
+
validateSchema(schemas.body, body),
|
|
95
|
+
validateSchema(schemas.query, query),
|
|
96
|
+
validateSchema(schemas.params, params),
|
|
97
|
+
validateSchema(schemas.headers, headers),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
bodyResult?.issues ||
|
|
102
|
+
queryResult?.issues ||
|
|
103
|
+
paramsResult?.issues ||
|
|
104
|
+
headersResult?.issues
|
|
105
|
+
) {
|
|
106
|
+
throw new ValidationError({
|
|
107
|
+
body: bodyResult?.issues
|
|
108
|
+
? (bodyResult as StandardSchemaV1.FailureResult)
|
|
109
|
+
: undefined,
|
|
110
|
+
query: queryResult?.issues
|
|
111
|
+
? (queryResult as StandardSchemaV1.FailureResult)
|
|
112
|
+
: undefined,
|
|
113
|
+
params: paramsResult?.issues
|
|
114
|
+
? (paramsResult as StandardSchemaV1.FailureResult)
|
|
115
|
+
: undefined,
|
|
116
|
+
headers: headersResult?.issues
|
|
117
|
+
? (headersResult as StandardSchemaV1.FailureResult)
|
|
118
|
+
: undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ctx: Context<B, Q, P, H> = {
|
|
123
|
+
body: bodyResult
|
|
124
|
+
? (bodyResult as StandardSchemaV1.SuccessResult<B>).value
|
|
125
|
+
: (body as B),
|
|
126
|
+
query: queryResult
|
|
127
|
+
? (queryResult as StandardSchemaV1.SuccessResult<Q>).value
|
|
128
|
+
: (query as Q),
|
|
129
|
+
params: paramsResult
|
|
130
|
+
? (paramsResult as StandardSchemaV1.SuccessResult<P>).value
|
|
131
|
+
: (params as P),
|
|
132
|
+
headers: headersResult
|
|
133
|
+
? (headersResult as StandardSchemaV1.SuccessResult<H>).value
|
|
134
|
+
: (headers as H),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return handler(ctx);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** The Standard Schema interface. */
|
|
2
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
3
|
+
/** The Standard Schema properties. */
|
|
4
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export declare namespace StandardSchemaV1 {
|
|
8
|
+
/** The Standard Schema properties interface. */
|
|
9
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
10
|
+
/** The version number of the standard. */
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
/** The vendor name of the schema library. */
|
|
13
|
+
readonly vendor: string;
|
|
14
|
+
/** Validates unknown input values. */
|
|
15
|
+
readonly validate: (
|
|
16
|
+
value: unknown,
|
|
17
|
+
options?: StandardSchemaV1.Options | undefined,
|
|
18
|
+
) => Result<Output> | Promise<Result<Output>>;
|
|
19
|
+
/** Inferred types associated with the schema. */
|
|
20
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The result interface of the validate function. */
|
|
24
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
25
|
+
|
|
26
|
+
/** The result interface if validation succeeds. */
|
|
27
|
+
export interface SuccessResult<Output> {
|
|
28
|
+
/** The typed output value. */
|
|
29
|
+
readonly value: Output;
|
|
30
|
+
/** A falsy value for `issues` indicates success. */
|
|
31
|
+
readonly issues?: undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Options {
|
|
35
|
+
/** Explicit support for additional vendor-specific parameters, if needed. */
|
|
36
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The result interface if validation fails. */
|
|
40
|
+
export interface FailureResult {
|
|
41
|
+
/** The issues of failed validation. */
|
|
42
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The issue interface of the failure output. */
|
|
46
|
+
export interface Issue {
|
|
47
|
+
/** The error message of the issue. */
|
|
48
|
+
readonly message: string;
|
|
49
|
+
/** The path of the issue, if any. */
|
|
50
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The path segment interface of the issue. */
|
|
54
|
+
export interface PathSegment {
|
|
55
|
+
/** The key representing a path segment. */
|
|
56
|
+
readonly key: PropertyKey;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The Standard Schema types interface. */
|
|
60
|
+
export interface Types<Input = unknown, Output = Input> {
|
|
61
|
+
/** The input type of the schema. */
|
|
62
|
+
readonly input: Input;
|
|
63
|
+
/** The output type of the schema. */
|
|
64
|
+
readonly output: Output;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Infers the input type of a Standard Schema. */
|
|
68
|
+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
|
|
69
|
+
Schema["~standard"]["types"]
|
|
70
|
+
>["input"];
|
|
71
|
+
|
|
72
|
+
/** Infers the output type of a Standard Schema. */
|
|
73
|
+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
|
|
74
|
+
Schema["~standard"]["types"]
|
|
75
|
+
>["output"];
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# REQUIRED READING
|
|
2
|
+
|
|
3
|
+
| File | When to Read |
|
|
4
|
+
| ------------------------ | --------------------------------------- |
|
|
5
|
+
| [README.md](./README.md) | Quick start, understand basic usage |
|
|
6
|
+
| [index.ts](./index.ts) | Understand implementation (`sqlPlugin`) |
|
|
7
|
+
|
|
8
|
+
# OPTIONAL READING
|
|
9
|
+
|
|
10
|
+
| File | When to Read |
|
|
11
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
12
|
+
| [SQL](https://bun.com/docs/runtime/sql.md) | If you don't know how to use Bun native SQL bindings, read this file to learn |
|