@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.
@@ -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 |