yedra 0.12.12 → 0.13.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.
@@ -17,7 +17,11 @@ export declare class Yedra {
17
17
  * @param req - The HTTP request.
18
18
  * @returns The HTTP response.
19
19
  */
20
- handle(req: Request): Promise<Response>;
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
  */
@@ -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;
@@ -29,6 +29,10 @@ export class Yedra {
29
29
  const newPath = route.path.withPrefix(path);
30
30
  this.restRoutes.push({ path: newPath, endpoint: route.endpoint });
31
31
  }
32
+ for (const route of endpoint.wsRoutes) {
33
+ const newPath = route.path.withPrefix(path);
34
+ this.wsRoutes.push({ path: newPath, endpoint: route.endpoint });
35
+ }
32
36
  }
33
37
  else if (endpoint instanceof RestEndpoint) {
34
38
  this.restRoutes.push({ path: new Path(path), endpoint });
@@ -67,19 +71,21 @@ export class Yedra {
67
71
  * @param req - The HTTP request.
68
72
  * @returns The HTTP response.
69
73
  */
70
- async handle(req) {
71
- const url = new URL(req.url).pathname;
72
- if (req.method !== 'GET' &&
73
- req.method !== 'POST' &&
74
- req.method !== 'PUT' &&
75
- req.method !== 'DELETE') {
76
- return Yedra.errorResponse(405, `Method '${req.method}' not allowed.`);
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.`);
77
82
  }
78
- const match = this.matchRestRoute(url, req.method);
83
+ const match = this.matchRestRoute(parsedUrl.pathname, method);
79
84
  if (!match.result) {
80
- if (req.method === 'GET') {
85
+ if (method === 'GET') {
81
86
  // try returning a static file
82
- const staticFile = this.staticFiles.get(url) ?? this.staticFiles.get('__fallback');
87
+ const staticFile = this.staticFiles.get(parsedUrl.pathname) ??
88
+ this.staticFiles.get('__fallback');
83
89
  if (staticFile !== undefined) {
84
90
  return new Response(staticFile.data, {
85
91
  headers: {
@@ -89,12 +95,12 @@ export class Yedra {
89
95
  }
90
96
  }
91
97
  if (match.invalidMethod) {
92
- return Yedra.errorResponse(405, `Method '${req.method}' not allowed for path '${url}'.`);
98
+ return Yedra.errorResponse(405, `Method '${method}' not allowed for path '${parsedUrl.pathname}'.`);
93
99
  }
94
- return Yedra.errorResponse(404, `Path '${url}' not found.`);
100
+ return Yedra.errorResponse(404, `Path '${parsedUrl.pathname}' not found.`);
95
101
  }
96
102
  try {
97
- return await match.result.endpoint.handle(req, match.result.params);
103
+ return await match.result.endpoint.handle(parsedUrl.pathname, options?.body ?? '', match.result.params, Object.fromEntries(parsedUrl.searchParams), options?.headers ?? {});
98
104
  }
99
105
  catch (error) {
100
106
  if (error instanceof HttpError) {
@@ -134,14 +140,14 @@ export class Yedra {
134
140
  });
135
141
  req.on('end', async () => {
136
142
  const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
137
- const response = await this.handle(new Request(url, {
143
+ const response = await this.fetch(url, {
138
144
  method: req.method,
139
145
  body: req.method === 'POST' || req.method === 'PUT' ? body : undefined,
140
- headers: Object.entries(req.headers).map(([key, value]) => [
146
+ headers: Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
141
147
  key,
142
148
  Array.isArray(value) ? value.join(',') : (value ?? ''),
143
- ]),
144
- }));
149
+ ])),
150
+ });
145
151
  res.writeHead(response.status, Object.fromEntries(response.headers));
146
152
  res.end(Buffer.from(await response.arrayBuffer()));
147
153
  const duration = Date.now() - begin;
@@ -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(req: Request, params: Record<string, string>): Promise<Response>;
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(req: Request, params: Record<string, string>): Promise<Response>;
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> {
@@ -17,17 +17,16 @@ class ConcreteRestEndpoint extends RestEndpoint {
17
17
  get method() {
18
18
  return this._method;
19
19
  }
20
- async handle(req, params) {
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 url = new URL(req.url);
26
25
  try {
27
- parsedBody = this.options.req.deserialize(Buffer.from(await req.arrayBuffer()), req.headers.get('content-type') ?? 'application/octet-stream');
26
+ parsedBody = this.options.req.deserialize(typeof body === 'string' ? Buffer.from(body) : body, headers['content-type'] ?? 'application/octet-stream');
28
27
  parsedParams = this.paramsSchema.parse(params);
29
- parsedQuery = this.querySchema.parse(Object.fromEntries(url.searchParams));
30
- parsedHeaders = this.headersSchema.parse(Object.fromEntries(req.headers));
28
+ parsedQuery = this.querySchema.parse(query);
29
+ parsedHeaders = this.headersSchema.parse(headers);
31
30
  }
32
31
  catch (error) {
33
32
  if (error instanceof SyntaxError) {
@@ -39,7 +38,7 @@ class ConcreteRestEndpoint extends RestEndpoint {
39
38
  throw error;
40
39
  }
41
40
  const response = await this.options.do({
42
- url: url.pathname,
41
+ url,
43
42
  params: parsedParams,
44
43
  query: parsedQuery,
45
44
  headers: parsedHeaders,
@@ -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 {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "yedra",
3
- "version": "0.12.12",
3
+ "version": "0.13.0",
4
4
  "repository": "github:0codekit/yedra",
5
5
  "main": "dist/index.js",
6
6
  "devDependencies": {
7
- "@biomejs/biome": "^1.9.3",
8
- "@types/node": "^22.7.5",
7
+ "@biomejs/biome": "^1.9.4",
8
+ "@types/node": "^22.7.9",
9
9
  "@types/uuid": "^10.0.0",
10
10
  "typescript": "^5.6.3"
11
11
  },