yedra 0.12.13 → 0.13.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/dist/routing/app.d.ts +5 -1
- package/dist/routing/app.js +23 -21
- package/dist/routing/rest.d.ts +2 -2
- package/dist/routing/rest.js +51 -11
- package/dist/routing/websocket.d.ts +1 -1
- package/dist/validation/error.d.ts +1 -1
- package/dist/validation/error.js +1 -0
- package/package.json +3 -3
package/dist/routing/app.d.ts
CHANGED
|
@@ -17,7 +17,11 @@ export declare class Yedra {
|
|
|
17
17
|
* @param req - The HTTP request.
|
|
18
18
|
* @returns The HTTP response.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
fetch(url: URL | string, options?: {
|
|
21
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
22
|
+
body?: string | Buffer;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
}): Promise<Response>;
|
|
21
25
|
/**
|
|
22
26
|
* Generate OpenAPI documentation for the app.
|
|
23
27
|
*/
|
package/dist/routing/app.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import mime from 'mime';
|
|
1
5
|
import { WebSocketServer } from 'ws';
|
|
2
6
|
import { HttpError } from './errors.js';
|
|
3
7
|
import { Path } from './path.js';
|
|
4
|
-
import { createServer } from 'node:http';
|
|
5
8
|
import { RestEndpoint } from './rest.js';
|
|
6
9
|
import { WsEndpoint } from './websocket.js';
|
|
7
|
-
import { extname, join } from 'node:path';
|
|
8
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
9
|
-
import mime from 'mime';
|
|
10
10
|
class Context {
|
|
11
11
|
constructor(server) {
|
|
12
12
|
this.server = server;
|
|
@@ -71,19 +71,21 @@ export class Yedra {
|
|
|
71
71
|
* @param req - The HTTP request.
|
|
72
72
|
* @returns The HTTP response.
|
|
73
73
|
*/
|
|
74
|
-
async
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
async fetch(url, options) {
|
|
75
|
+
const parsedUrl = typeof url === 'string' ? new URL(url, 'http://localhost') : url;
|
|
76
|
+
const method = options?.method ?? 'GET';
|
|
77
|
+
if (method !== 'GET' &&
|
|
78
|
+
method !== 'POST' &&
|
|
79
|
+
method !== 'PUT' &&
|
|
80
|
+
method !== 'DELETE') {
|
|
81
|
+
return Yedra.errorResponse(405, `Method '${method}' not allowed.`);
|
|
81
82
|
}
|
|
82
|
-
const match = this.matchRestRoute(
|
|
83
|
+
const match = this.matchRestRoute(parsedUrl.pathname, method);
|
|
83
84
|
if (!match.result) {
|
|
84
|
-
if (
|
|
85
|
+
if (method === 'GET') {
|
|
85
86
|
// try returning a static file
|
|
86
|
-
const staticFile = this.staticFiles.get(
|
|
87
|
+
const staticFile = this.staticFiles.get(parsedUrl.pathname) ??
|
|
88
|
+
this.staticFiles.get('__fallback');
|
|
87
89
|
if (staticFile !== undefined) {
|
|
88
90
|
return new Response(staticFile.data, {
|
|
89
91
|
headers: {
|
|
@@ -93,12 +95,12 @@ export class Yedra {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
if (match.invalidMethod) {
|
|
96
|
-
return Yedra.errorResponse(405, `Method '${
|
|
98
|
+
return Yedra.errorResponse(405, `Method '${method}' not allowed for path '${parsedUrl.pathname}'.`);
|
|
97
99
|
}
|
|
98
|
-
return Yedra.errorResponse(404, `Path '${
|
|
100
|
+
return Yedra.errorResponse(404, `Path '${parsedUrl.pathname}' not found.`);
|
|
99
101
|
}
|
|
100
102
|
try {
|
|
101
|
-
return await match.result.endpoint.handle(
|
|
103
|
+
return await match.result.endpoint.handle(parsedUrl.pathname, options?.body ?? '', match.result.params, Object.fromEntries(parsedUrl.searchParams), options?.headers ?? {});
|
|
102
104
|
}
|
|
103
105
|
catch (error) {
|
|
104
106
|
if (error instanceof HttpError) {
|
|
@@ -138,14 +140,14 @@ export class Yedra {
|
|
|
138
140
|
});
|
|
139
141
|
req.on('end', async () => {
|
|
140
142
|
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
|
|
141
|
-
const response = await this.
|
|
143
|
+
const response = await this.fetch(url, {
|
|
142
144
|
method: req.method,
|
|
143
145
|
body: req.method === 'POST' || req.method === 'PUT' ? body : undefined,
|
|
144
|
-
headers: Object.entries(req.headers).map(([key, value]) => [
|
|
146
|
+
headers: Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
|
|
145
147
|
key,
|
|
146
148
|
Array.isArray(value) ? value.join(',') : (value ?? ''),
|
|
147
|
-
]),
|
|
148
|
-
})
|
|
149
|
+
])),
|
|
150
|
+
});
|
|
149
151
|
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
150
152
|
res.end(Buffer.from(await response.arrayBuffer()));
|
|
151
153
|
const duration = Date.now() - begin;
|
package/dist/routing/rest.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ type EndpointOptions<Params extends Record<string, Schema<unknown>>, Query exten
|
|
|
31
31
|
};
|
|
32
32
|
export declare abstract class RestEndpoint {
|
|
33
33
|
abstract get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
34
|
-
abstract handle(
|
|
34
|
+
abstract handle(pathname: string, body: string | Buffer, params: Record<string, string>, query: Record<string, string>, headers: Record<string, string>): Promise<Response>;
|
|
35
35
|
abstract documentation(): object;
|
|
36
36
|
}
|
|
37
37
|
declare class ConcreteRestEndpoint<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Req extends BodyType<unknown>, Res extends BodyType<unknown>> extends RestEndpoint {
|
|
@@ -42,7 +42,7 @@ declare class ConcreteRestEndpoint<Params extends Record<string, Schema<unknown>
|
|
|
42
42
|
private headersSchema;
|
|
43
43
|
constructor(method: 'GET' | 'POST' | 'PUT' | 'DELETE', options: EndpointOptions<Params, Query, Headers, Req, Res>);
|
|
44
44
|
get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
45
|
-
handle(
|
|
45
|
+
handle(url: string, body: string | Buffer, params: Record<string, string>, query: Record<string, string>, headers: Record<string, string>): Promise<Response>;
|
|
46
46
|
documentation(): object;
|
|
47
47
|
}
|
|
48
48
|
export declare class Get<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Res extends BodyType<unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, NoneBody, Res> {
|
package/dist/routing/rest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { paramDocs } from '../util/docs.js';
|
|
2
|
-
import { ValidationError } from '../validation/error.js';
|
|
2
|
+
import { Issue, ValidationError } from '../validation/error.js';
|
|
3
3
|
import { NoneBody, none } from '../validation/none.js';
|
|
4
4
|
import { object } from '../validation/object.js';
|
|
5
5
|
import { BadRequestError } from './errors.js';
|
|
@@ -17,32 +17,72 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
17
17
|
get method() {
|
|
18
18
|
return this._method;
|
|
19
19
|
}
|
|
20
|
-
async handle(
|
|
20
|
+
async handle(url, body, params, query, headers) {
|
|
21
21
|
let parsedBody;
|
|
22
22
|
let parsedParams;
|
|
23
23
|
let parsedQuery;
|
|
24
24
|
let parsedHeaders;
|
|
25
|
-
const
|
|
25
|
+
const issues = [];
|
|
26
26
|
try {
|
|
27
|
-
parsedBody = this.options.req.deserialize(Buffer.from(
|
|
28
|
-
parsedParams = this.paramsSchema.parse(params);
|
|
29
|
-
parsedQuery = this.querySchema.parse(Object.fromEntries(url.searchParams));
|
|
30
|
-
parsedHeaders = this.headersSchema.parse(Object.fromEntries(req.headers));
|
|
27
|
+
parsedBody = this.options.req.deserialize(typeof body === 'string' ? Buffer.from(body) : body, headers['content-type'] ?? 'application/octet-stream');
|
|
31
28
|
}
|
|
32
29
|
catch (error) {
|
|
33
30
|
if (error instanceof SyntaxError) {
|
|
34
|
-
|
|
31
|
+
issues.push(new Issue('invalidSyntax', ['body'], 'JSON', error.message));
|
|
32
|
+
}
|
|
33
|
+
else if (error instanceof ValidationError) {
|
|
34
|
+
issues.push(...error.withPrefix('body'));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
parsedParams = this.paramsSchema.parse(params);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof ValidationError) {
|
|
45
|
+
issues.push(...error.withPrefix('params'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
parsedQuery = this.querySchema.parse(query);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof ValidationError) {
|
|
56
|
+
issues.push(...error.withPrefix('query'));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw error;
|
|
35
60
|
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
parsedHeaders = this.headersSchema.parse(headers);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
36
66
|
if (error instanceof ValidationError) {
|
|
37
|
-
|
|
67
|
+
issues.push(...error.withPrefix('headers'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw error;
|
|
38
71
|
}
|
|
39
|
-
|
|
72
|
+
}
|
|
73
|
+
if (issues.length > 0) {
|
|
74
|
+
const error = new ValidationError(issues);
|
|
75
|
+
throw new BadRequestError(error.format());
|
|
40
76
|
}
|
|
41
77
|
const response = await this.options.do({
|
|
42
|
-
url
|
|
78
|
+
url,
|
|
79
|
+
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
43
80
|
params: parsedParams,
|
|
81
|
+
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
44
82
|
query: parsedQuery,
|
|
83
|
+
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
45
84
|
headers: parsedHeaders,
|
|
85
|
+
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
46
86
|
body: parsedBody,
|
|
47
87
|
});
|
|
48
88
|
if (response.body instanceof Uint8Array) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { WebSocket as NodeWebSocket } from 'ws';
|
|
1
2
|
import type { Typeof } from '../validation/body.js';
|
|
2
3
|
import { type ObjectSchema } from '../validation/object.js';
|
|
3
4
|
import type { Schema } from '../validation/schema.js';
|
|
4
|
-
import type { WebSocket as NodeWebSocket } from 'ws';
|
|
5
5
|
type MessageCb = (message: Buffer) => Promise<void> | void;
|
|
6
6
|
type CloseCb = (code: number | undefined, reason: string | undefined) => Promise<void> | void;
|
|
7
7
|
declare class YedraWebSocket {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type IssueCode = 'invalidType' | 'tooSmall' | 'tooBig' | 'tooShort' | 'tooLong' | 'invalidPattern' | 'missingProperty' | 'invalidContentType';
|
|
1
|
+
type IssueCode = 'invalidSyntax' | 'invalidType' | 'tooSmall' | 'tooBig' | 'tooShort' | 'tooLong' | 'invalidPattern' | 'missingProperty' | 'invalidContentType';
|
|
2
2
|
export declare class Issue {
|
|
3
3
|
readonly code: IssueCode;
|
|
4
4
|
readonly path: string[];
|
package/dist/validation/error.js
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yedra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"repository": "github:0codekit/yedra",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"@biomejs/biome": "^1.9.
|
|
8
|
-
"@types/node": "^22.
|
|
7
|
+
"@biomejs/biome": "^1.9.4",
|
|
8
|
+
"@types/node": "^22.8.0",
|
|
9
9
|
"@types/uuid": "^10.0.0",
|
|
10
10
|
"typescript": "^5.6.3"
|
|
11
11
|
},
|