yedra 0.10.8 → 0.11.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/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  import * as y from './lib.js';
2
2
  export { y };
3
+ export { App } from './routing/app.js';
4
+ export { Get, Post, Put, Delete } from './routing/rest.js';
5
+ export { Ws } from './routing/websocket.js';
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
1
  import * as y from './lib.js';
2
2
  export { y };
3
+ export { App } from './routing/app.js';
4
+ export { Get, Post, Put, Delete } from './routing/rest.js';
5
+ export { Ws } from './routing/websocket.js';
package/dist/lib.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
2
2
  export { Log } from './routing/log.js';
3
- export { type Endpoint, app } from './routing/app.js';
4
3
  export declare const validatePath: (path: string) => void;
5
4
  export { parseEnv } from './routing/env.js';
6
- export { get, post, put, del, ws } from './routing/endpoints.js';
7
5
  export { array } from './validation/array.js';
8
6
  export { boolean } from './validation/boolean.js';
9
7
  export { date } from './validation/date.js';
@@ -18,7 +16,6 @@ export { BodyType, type Typeof } from './validation/body.js';
18
16
  export { raw } from './validation/raw.js';
19
17
  export { none } from './validation/none.js';
20
18
  export { either } from './validation/either.js';
21
- export { HttpResponse, HttpRequestError, Http } from './routing/http.js';
22
19
  export { string } from './validation/string.js';
23
20
  export { _undefined as undefined } from './validation/undefined.js';
24
21
  export { union } from './validation/union.js';
package/dist/lib.js CHANGED
@@ -2,12 +2,10 @@ import { Path } from './routing/path.js';
2
2
  // routing
3
3
  export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
4
4
  export { Log } from './routing/log.js';
5
- export { app } from './routing/app.js';
6
5
  export const validatePath = (path) => {
7
6
  new Path(path);
8
7
  };
9
8
  export { parseEnv } from './routing/env.js';
10
- export { get, post, put, del, ws } from './routing/endpoints.js';
11
9
  // validation
12
10
  export { array } from './validation/array.js';
13
11
  export { boolean } from './validation/boolean.js';
@@ -23,7 +21,6 @@ export { BodyType } from './validation/body.js';
23
21
  export { raw } from './validation/raw.js';
24
22
  export { none } from './validation/none.js';
25
23
  export { either } from './validation/either.js';
26
- export { HttpResponse, HttpRequestError, Http } from './routing/http.js';
27
24
  export { string } from './validation/string.js';
28
25
  export { _undefined as undefined } from './validation/undefined.js';
29
26
  export { union } from './validation/union.js';
@@ -1,15 +1,8 @@
1
- import type { Path } from './path.js';
2
1
  import type { Server } from 'bun';
3
- export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
4
- export type Endpoint = {
5
- method: Method;
6
- path: Path;
7
- handle: (req: Request, params: Record<string, string>, server: Server) => Promise<Response | undefined>;
8
- documentation: () => object;
9
- };
2
+ import type { Endpoint } from './endpoint.js';
10
3
  export declare class App {
11
- private endpoints;
12
- constructor(endpoints: Endpoint[]);
4
+ private routes;
5
+ use(path: string, endpoint: Endpoint): App;
13
6
  /**
14
7
  * Handle an HTTP request.
15
8
  * @param req - The HTTP request.
@@ -35,4 +28,3 @@ export declare class App {
35
28
  private static handleWebSocketErrors;
36
29
  private matchEndpoint;
37
30
  }
38
- export declare const app: (routes: string) => Promise<App>;
@@ -1,8 +1,12 @@
1
- import { glob } from 'glob';
2
1
  import { HttpError } from './errors.js';
2
+ import { Path } from './path.js';
3
3
  export class App {
4
- constructor(endpoints) {
5
- this.endpoints = endpoints;
4
+ constructor() {
5
+ this.routes = [];
6
+ }
7
+ use(path, endpoint) {
8
+ this.routes.push({ path: new Path(path), endpoint });
9
+ return this;
6
10
  }
7
11
  /**
8
12
  * Handle an HTTP request.
@@ -40,10 +44,11 @@ export class App {
40
44
  */
41
45
  docs(options) {
42
46
  const paths = {};
43
- for (const endpoint of this.endpoints) {
44
- const path = endpoint.path.toString();
47
+ for (const route of this.routes) {
48
+ const path = route.path.toString();
45
49
  const methods = paths[path] ?? {};
46
- methods[endpoint.method.toLowerCase()] = endpoint.documentation();
50
+ methods[route.endpoint.method.toLowerCase()] =
51
+ route.endpoint.documentation();
47
52
  paths[path] = methods;
48
53
  }
49
54
  return {
@@ -105,53 +110,22 @@ export class App {
105
110
  matchEndpoint(url, method) {
106
111
  let invalidMethod = false;
107
112
  let result = undefined;
108
- for (const endpoint of this.endpoints) {
109
- const match = endpoint.path.match(url);
113
+ for (const route of this.routes) {
114
+ const match = route.path.match(url);
110
115
  if (match === undefined) {
111
116
  continue;
112
117
  }
113
118
  const { params, score } = match;
114
- if (endpoint.method !== method) {
119
+ if (route.endpoint.method !== method) {
115
120
  invalidMethod = true;
116
121
  continue;
117
122
  }
118
123
  const previous = result?.score;
119
124
  if (previous === undefined || score < previous) {
120
125
  // if there was no previous match or this one is better, use it
121
- result = { endpoint, params, score };
126
+ result = { endpoint: route.endpoint, params, score };
122
127
  }
123
128
  }
124
129
  return { invalidMethod, result };
125
130
  }
126
131
  }
127
- const shouldIgnore = (path) => {
128
- const extensions = [
129
- '.test.ts',
130
- '.schema.ts',
131
- '.util.ts',
132
- '.d.ts',
133
- '.test.js',
134
- '.schema.js',
135
- '.util.js',
136
- ];
137
- for (const extension of extensions) {
138
- if (path.endsWith(extension)) {
139
- return true;
140
- }
141
- }
142
- return false;
143
- };
144
- export const app = async (routes) => {
145
- const endpoints = [];
146
- const files = await glob([`${routes}/**/*.ts`, `${routes}/**/*.js`], {
147
- absolute: true,
148
- });
149
- for (const file of files) {
150
- if (shouldIgnore(file)) {
151
- continue;
152
- }
153
- const endpoint = await import(file);
154
- endpoints.push(endpoint.default);
155
- }
156
- return new App(endpoints);
157
- };
@@ -0,0 +1,6 @@
1
+ import type { Server } from 'bun';
2
+ export interface Endpoint {
3
+ get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
4
+ handle(req: Request, params: Record<string, string>, server: Server): Promise<Response | undefined>;
5
+ documentation(): object;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import type { Server } from 'bun';
2
+ import type { BodyType, Typeof } from '../validation/body.js';
3
+ import { type ObjectSchema } from '../validation/object.js';
4
+ import type { Schema } from '../validation/schema.js';
5
+ import type { Endpoint } from './endpoint.js';
6
+ import { NoneBody } from '../validation/none.js';
7
+ type ReqObject<Params, Query, Headers, Body> = {
8
+ url: string;
9
+ params: Params;
10
+ query: Query;
11
+ headers: Headers;
12
+ body: Body;
13
+ };
14
+ type ResObject<Body> = Promise<{
15
+ status?: number;
16
+ body: Body;
17
+ headers?: Record<string, string>;
18
+ }> | {
19
+ status?: number;
20
+ body: Body;
21
+ headers?: Record<string, string>;
22
+ };
23
+ type EndpointOptions<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>> = {
24
+ category: string;
25
+ summary: string;
26
+ description?: string;
27
+ params: Params;
28
+ query: Query;
29
+ headers: Headers;
30
+ req: Req;
31
+ res: Res;
32
+ do: (req: ReqObject<Typeof<ObjectSchema<Params>>, Typeof<ObjectSchema<Query>>, Typeof<ObjectSchema<Headers>>, Typeof<Req>>) => ResObject<Typeof<Res>>;
33
+ };
34
+ declare class RestEndpoint<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>> implements Endpoint {
35
+ private _method;
36
+ private options;
37
+ private paramsSchema;
38
+ private querySchema;
39
+ private headersSchema;
40
+ constructor(method: 'GET' | 'POST' | 'PUT' | 'DELETE', options: EndpointOptions<Params, Query, Headers, Req, Res>);
41
+ get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
42
+ handle(req: Request, params: Record<string, string>, _server: Server): Promise<Response | undefined>;
43
+ documentation(): object;
44
+ }
45
+ 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 RestEndpoint<Params, Query, Headers, NoneBody, Res> {
46
+ constructor(options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, 'req'>);
47
+ }
48
+ export declare class Post<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<Params, Query, Headers, Req, Res> {
49
+ constructor(options: EndpointOptions<Params, Query, Headers, Req, Res>);
50
+ }
51
+ export declare class Put<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<Params, Query, Headers, Req, Res> {
52
+ constructor(options: EndpointOptions<Params, Query, Headers, Req, Res>);
53
+ }
54
+ export declare class Delete<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Res extends BodyType<unknown>> extends RestEndpoint<Params, Query, Headers, NoneBody, Res> {
55
+ constructor(options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, 'req'>);
56
+ }
57
+ export {};
@@ -0,0 +1,130 @@
1
+ import { object } from '../validation/object.js';
2
+ import { BadRequestError } from './errors.js';
3
+ import { ValidationError } from '../validation/error.js';
4
+ import { none, NoneBody } from '../validation/none.js';
5
+ class RestEndpoint {
6
+ constructor(method, options) {
7
+ this._method = method;
8
+ this.options = options;
9
+ this.paramsSchema = object(options.params);
10
+ this.querySchema = object(options.query);
11
+ this.headersSchema = object(options.headers);
12
+ }
13
+ get method() {
14
+ return this.method;
15
+ }
16
+ async handle(req, params, _server) {
17
+ let parsedBody;
18
+ let parsedParams;
19
+ let parsedQuery;
20
+ let parsedHeaders;
21
+ const url = new URL(req.url);
22
+ try {
23
+ parsedBody = this.options.req.deserialize(Buffer.from(await req.arrayBuffer()), req.headers.get('content-type') ?? 'application/octet-stream');
24
+ parsedParams = this.paramsSchema.parse(params);
25
+ parsedQuery = this.querySchema.parse(Object.fromEntries(url.searchParams));
26
+ parsedHeaders = this.headersSchema.parse(Object.fromEntries(req.headers));
27
+ }
28
+ catch (error) {
29
+ if (error instanceof SyntaxError) {
30
+ throw new BadRequestError(error.message);
31
+ }
32
+ if (error instanceof ValidationError) {
33
+ throw new BadRequestError(error.format());
34
+ }
35
+ throw error;
36
+ }
37
+ const response = await this.options.do({
38
+ url: url.pathname,
39
+ params: parsedParams,
40
+ query: parsedQuery,
41
+ headers: parsedHeaders,
42
+ body: parsedBody,
43
+ });
44
+ if (response.body instanceof Uint8Array) {
45
+ return new Response(response.body, {
46
+ status: response.status,
47
+ headers: response.headers,
48
+ });
49
+ }
50
+ return Response.json(response.body, {
51
+ status: response.status,
52
+ headers: response.headers,
53
+ });
54
+ }
55
+ documentation() {
56
+ const parameters = [
57
+ ...paramDocs(this.options.params, 'path'),
58
+ ...paramDocs(this.options.query, 'query'),
59
+ ...paramDocs(this.options.headers, 'header'),
60
+ ];
61
+ return {
62
+ tags: [this.options.category],
63
+ summary: this.options.summary,
64
+ description: this.options.description,
65
+ parameters,
66
+ requestBody: {
67
+ required: this.options.req instanceof NoneBody,
68
+ content: this.options.req.bodyDocs(),
69
+ },
70
+ responses: {
71
+ '200': {
72
+ description: 'Success',
73
+ content: this.options.res.bodyDocs(),
74
+ },
75
+ '400': {
76
+ description: 'Bad Request',
77
+ content: {
78
+ 'application/json': {
79
+ schema: {
80
+ type: 'object',
81
+ properties: {
82
+ status: {
83
+ type: 'number',
84
+ },
85
+ errorMessage: {
86
+ type: 'string',
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ };
95
+ }
96
+ }
97
+ export class Get extends RestEndpoint {
98
+ constructor(options) {
99
+ super('GET', { req: none(), ...options });
100
+ }
101
+ }
102
+ export class Post extends RestEndpoint {
103
+ constructor(options) {
104
+ super('POST', options);
105
+ }
106
+ }
107
+ export class Put extends RestEndpoint {
108
+ constructor(options) {
109
+ super('PUT', options);
110
+ }
111
+ }
112
+ export class Delete extends RestEndpoint {
113
+ constructor(options) {
114
+ super('DELETE', { req: none(), ...options });
115
+ }
116
+ }
117
+ const paramDocs = (params, position) => {
118
+ const result = [];
119
+ for (const name in params) {
120
+ const docs = params[name].documentation();
121
+ result.push({
122
+ name,
123
+ in: position,
124
+ description: 'description' in docs ? docs.description : undefined,
125
+ required: !params[name].isOptional(),
126
+ schema: docs,
127
+ });
128
+ }
129
+ return result;
130
+ };
@@ -0,0 +1,66 @@
1
+ import type { Server, ServerWebSocket } from 'bun';
2
+ import type { Schema } from '../validation/schema.js';
3
+ import { type ObjectSchema } from '../validation/object.js';
4
+ import type { Typeof } from '../validation/body.js';
5
+ import type { Endpoint } from './endpoint.js';
6
+ type OpenCb = (ws: ServerWebSocket<{
7
+ handler: WebSocketHandler;
8
+ }>) => Promise<void> | void;
9
+ type MessageCb = (message: Buffer) => Promise<void> | void;
10
+ type CloseCb = (code: number | undefined, reason: string | undefined) => Promise<void> | void;
11
+ export declare class WebSocketHandler {
12
+ open: OpenCb;
13
+ messageCbs: MessageCb[];
14
+ closeCbs: CloseCb[];
15
+ constructor(open: OpenCb);
16
+ message(message: Buffer): Promise<void>;
17
+ close(code: number | undefined, reason: string | undefined): Promise<void>;
18
+ }
19
+ declare class Socket {
20
+ private handler;
21
+ private ws;
22
+ constructor(handler: WebSocketHandler, ws: ServerWebSocket<{
23
+ handler: WebSocketHandler;
24
+ }>);
25
+ set onmessage(cb: MessageCb);
26
+ set onclose(cb: CloseCb);
27
+ /**
28
+ * Send binary data over the WebSocket connection;
29
+ * @param message - The message buffer that will be sent.
30
+ */
31
+ send(message: Buffer): void;
32
+ /**
33
+ * Closes the WebSocket.
34
+ * @param code - The close code. Must be one of:
35
+ * - 1000: normal closure
36
+ * - 1009: message too big
37
+ * - 1011: server encountered error
38
+ * - 1012: server restarting
39
+ * - 1013: server too busy or rate-limiting
40
+ * - 4000-4999: reserved for applications
41
+ * @param reason - The reason the WebSocket was closed.
42
+ */
43
+ close(code?: number, reason?: string): void;
44
+ }
45
+ type WebSocketOptions<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>> = {
46
+ category: string;
47
+ summary: string;
48
+ description?: string;
49
+ params: Params;
50
+ query: Query;
51
+ do: (ws: Socket, req: {
52
+ url: string;
53
+ params: Typeof<ObjectSchema<Params>>;
54
+ query: Typeof<ObjectSchema<Query>>;
55
+ }) => Promise<void> | void;
56
+ };
57
+ export declare class Ws<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>> implements Endpoint {
58
+ private options;
59
+ private paramsSchema;
60
+ private querySchema;
61
+ constructor(options: WebSocketOptions<Params, Query>);
62
+ get method(): 'GET';
63
+ handle(req: Request, params: Record<string, string>, server: Server): Promise<Response | undefined>;
64
+ documentation(): object;
65
+ }
66
+ export {};
@@ -0,0 +1,101 @@
1
+ import { object } from '../validation/object.js';
2
+ import { BadRequestError } from './errors.js';
3
+ import { ValidationError } from '../validation/error.js';
4
+ export class WebSocketHandler {
5
+ constructor(open) {
6
+ this.messageCbs = [];
7
+ this.closeCbs = [];
8
+ this.open = open;
9
+ }
10
+ async message(message) {
11
+ for (const cb of this.messageCbs) {
12
+ await cb(message);
13
+ }
14
+ }
15
+ async close(code, reason) {
16
+ for (const cb of this.closeCbs) {
17
+ await cb(code, reason);
18
+ }
19
+ }
20
+ }
21
+ class Socket {
22
+ constructor(handler, ws) {
23
+ this.handler = handler;
24
+ this.ws = ws;
25
+ }
26
+ set onmessage(cb) {
27
+ this.handler.messageCbs.push(cb);
28
+ }
29
+ set onclose(cb) {
30
+ this.handler.closeCbs.push(cb);
31
+ }
32
+ /**
33
+ * Send binary data over the WebSocket connection;
34
+ * @param message - The message buffer that will be sent.
35
+ */
36
+ send(message) {
37
+ this.ws.send(message);
38
+ }
39
+ /**
40
+ * Closes the WebSocket.
41
+ * @param code - The close code. Must be one of:
42
+ * - 1000: normal closure
43
+ * - 1009: message too big
44
+ * - 1011: server encountered error
45
+ * - 1012: server restarting
46
+ * - 1013: server too busy or rate-limiting
47
+ * - 4000-4999: reserved for applications
48
+ * @param reason - The reason the WebSocket was closed.
49
+ */
50
+ close(code, reason) {
51
+ this.ws.close(code, reason);
52
+ }
53
+ }
54
+ export class Ws {
55
+ constructor(options) {
56
+ this.options = options;
57
+ this.paramsSchema = object(options.params);
58
+ this.querySchema = object(options.query);
59
+ }
60
+ get method() {
61
+ return 'GET';
62
+ }
63
+ async handle(req, params, server) {
64
+ const url = new URL(req.url);
65
+ if (req.headers.get('upgrade') !== 'websocket') {
66
+ throw new BadRequestError(`WebSocket required for ${url.pathname}`);
67
+ }
68
+ const handler = new WebSocketHandler(async (ws) => {
69
+ const socket = new Socket(handler, ws);
70
+ let parsedParams;
71
+ let parsedQuery;
72
+ try {
73
+ parsedParams = this.paramsSchema.parse(params);
74
+ parsedQuery = this.querySchema.parse(Object.fromEntries(url.searchParams));
75
+ }
76
+ catch (error) {
77
+ if (error instanceof ValidationError) {
78
+ throw new BadRequestError(error.format());
79
+ }
80
+ throw error;
81
+ }
82
+ await this.options.do(socket, {
83
+ url: url.pathname,
84
+ params: parsedParams,
85
+ query: parsedQuery,
86
+ });
87
+ });
88
+ if (!server.upgrade(req, {
89
+ data: {
90
+ handler,
91
+ },
92
+ })) {
93
+ throw new BadRequestError('Upgrading to WebSocket failed.');
94
+ }
95
+ return undefined;
96
+ }
97
+ documentation() {
98
+ // TODO
99
+ return {};
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yedra",
3
- "version": "0.10.8",
3
+ "version": "0.11.0",
4
4
  "repository": "github:0codekit/yedra",
5
5
  "main": "dist/index.js",
6
6
  "devDependencies": {
@@ -22,7 +22,6 @@
22
22
  "type": "module",
23
23
  "dependencies": {
24
24
  "@types/uuid": "^10.0.0",
25
- "glob": "^11.0.0",
26
25
  "uuid": "^10.0.0"
27
26
  }
28
27
  }
@@ -1,89 +0,0 @@
1
- import { type BodyType, type Schema, type Typeof } from '../lib.js';
2
- import { NoneBody } from '../validation/none.js';
3
- import { type ObjectSchema } from '../validation/object.js';
4
- import type { Endpoint } from './app.js';
5
- import type { ServerWebSocket } from 'bun';
6
- type ReqObject<Params, Query, Headers, Body> = {
7
- url: string;
8
- params: Params;
9
- query: Query;
10
- headers: Headers;
11
- body: Body;
12
- };
13
- type ResObject<Body> = Promise<{
14
- status?: number;
15
- body: Body;
16
- headers?: Record<string, string>;
17
- }> | {
18
- status?: number;
19
- body: Body;
20
- headers?: Record<string, string>;
21
- };
22
- type EndpointOptions<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>> = {
23
- category: string;
24
- summary: string;
25
- description?: string;
26
- params: Params;
27
- query: Query;
28
- headers: Headers;
29
- req: Req;
30
- res: Res;
31
- do: (req: ReqObject<Typeof<ObjectSchema<Params>>, Typeof<ObjectSchema<Query>>, Typeof<ObjectSchema<Headers>>, Typeof<Req>>) => ResObject<Typeof<Res>>;
32
- };
33
- export declare const get: <Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Res extends BodyType<unknown>>(path: string, options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, "req">) => Endpoint;
34
- export declare const post: <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>>(path: string, options: EndpointOptions<Params, Query, Headers, Req, Res>) => Endpoint;
35
- export declare const put: <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>>(path: string, options: EndpointOptions<Params, Query, Headers, Req, Res>) => Endpoint;
36
- export declare const del: <Path extends string, Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Res extends BodyType<unknown>>(path: Path, options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, "req">) => Endpoint;
37
- type OpenCb = (ws: ServerWebSocket<{
38
- handler: WebSocketHandler;
39
- }>) => Promise<void> | void;
40
- type MessageCb = (message: Buffer) => Promise<void> | void;
41
- type CloseCb = (code: number | undefined, reason: string | undefined) => Promise<void> | void;
42
- export declare class WebSocketHandler {
43
- open: OpenCb;
44
- messageCbs: MessageCb[];
45
- closeCbs: CloseCb[];
46
- constructor(open: OpenCb);
47
- message(message: Buffer): Promise<void>;
48
- close(code: number | undefined, reason: string | undefined): Promise<void>;
49
- }
50
- declare class Socket {
51
- private handler;
52
- private ws;
53
- constructor(handler: WebSocketHandler, ws: ServerWebSocket<{
54
- handler: WebSocketHandler;
55
- }>);
56
- set onmessage(cb: MessageCb);
57
- set onclose(cb: CloseCb);
58
- /**
59
- * Send binary data over the WebSocket connection;
60
- * @param message - The message buffer that will be sent.
61
- */
62
- send(message: Buffer): void;
63
- /**
64
- * Closes the WebSocket.
65
- * @param code - The close code. Must be one of:
66
- * - 1000: normal closure
67
- * - 1009: message too big
68
- * - 1011: server encountered error
69
- * - 1012: server restarting
70
- * - 1013: server too busy or rate-limiting
71
- * - 4000-4999: reserved for applications
72
- * @param reason - The reason the WebSocket was closed.
73
- */
74
- close(code?: number, reason?: string): void;
75
- }
76
- type WebSocketOptions<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>> = {
77
- category: string;
78
- summary: string;
79
- description?: string;
80
- params: Params;
81
- query: Query;
82
- do: (ws: Socket, req: {
83
- url: string;
84
- params: Typeof<ObjectSchema<Params>>;
85
- query: Typeof<ObjectSchema<Query>>;
86
- }) => Promise<void> | void;
87
- };
88
- export declare const ws: <Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>>(path: string, options: WebSocketOptions<Params, Query>) => Endpoint;
89
- export {};
@@ -1,208 +0,0 @@
1
- import { isUint8Array } from 'node:util/types';
2
- import { BadRequestError, ValidationError, } from '../lib.js';
3
- import { none, NoneBody } from '../validation/none.js';
4
- import { object } from '../validation/object.js';
5
- import { Path } from './path.js';
6
- const create = (method, path, options) => {
7
- const paramsSchema = object(options.params);
8
- const querySchema = object(options.query);
9
- const headersSchema = object(options.headers);
10
- return {
11
- method,
12
- path: new Path(path),
13
- async handle(req, params) {
14
- let parsedBody;
15
- let parsedParams;
16
- let parsedQuery;
17
- let parsedHeaders;
18
- const url = new URL(req.url);
19
- try {
20
- parsedBody = options.req.deserialize(Buffer.from(await req.arrayBuffer()), req.headers.get('content-type') ?? 'application/octet-stream');
21
- parsedParams = paramsSchema.parse(params);
22
- parsedQuery = querySchema.parse(Object.fromEntries(url.searchParams));
23
- parsedHeaders = headersSchema.parse(Object.fromEntries(req.headers));
24
- }
25
- catch (error) {
26
- if (error instanceof SyntaxError) {
27
- throw new BadRequestError(error.message);
28
- }
29
- if (error instanceof ValidationError) {
30
- throw new BadRequestError(error.format());
31
- }
32
- throw error;
33
- }
34
- const response = await options.do({
35
- url: url.pathname,
36
- params: parsedParams,
37
- query: parsedQuery,
38
- headers: parsedHeaders,
39
- body: parsedBody,
40
- });
41
- if (isUint8Array(response.body)) {
42
- return new Response(response.body, {
43
- status: response.status,
44
- headers: response.headers,
45
- });
46
- }
47
- return Response.json(response.body, {
48
- status: response.status,
49
- headers: response.headers,
50
- });
51
- },
52
- documentation() {
53
- const parameters = [
54
- ...paramDocs(options.params, 'path'),
55
- ...paramDocs(options.query, 'query'),
56
- ...paramDocs(options.headers, 'header'),
57
- ];
58
- return {
59
- tags: [options.category],
60
- summary: options.summary,
61
- description: options.description,
62
- parameters,
63
- requestBody: {
64
- required: options.req instanceof NoneBody,
65
- content: options.req.bodyDocs(),
66
- },
67
- responses: {
68
- '200': {
69
- description: 'Success',
70
- content: options.res.bodyDocs(),
71
- },
72
- '400': {
73
- description: 'Bad Request',
74
- content: {
75
- 'application/json': {
76
- schema: {
77
- type: 'object',
78
- properties: {
79
- status: {
80
- type: 'number',
81
- },
82
- errorMessage: {
83
- type: 'string',
84
- },
85
- },
86
- },
87
- },
88
- },
89
- },
90
- },
91
- };
92
- },
93
- };
94
- };
95
- const paramDocs = (params, position) => {
96
- const result = [];
97
- for (const name in params) {
98
- const docs = params[name].documentation();
99
- result.push({
100
- name,
101
- in: position,
102
- description: 'description' in docs ? docs.description : undefined,
103
- required: !params[name].isOptional(),
104
- schema: docs,
105
- });
106
- }
107
- return result;
108
- };
109
- export const get = (path, options) => create('GET', path, { ...options, req: none() });
110
- export const post = (path, options) => create('POST', path, options);
111
- export const put = (path, options) => create('PUT', path, options);
112
- export const del = (path, options) => create('DELETE', path, { ...options, req: none() });
113
- export class WebSocketHandler {
114
- constructor(open) {
115
- this.messageCbs = [];
116
- this.closeCbs = [];
117
- this.open = open;
118
- }
119
- async message(message) {
120
- for (const cb of this.messageCbs) {
121
- await cb(message);
122
- }
123
- }
124
- async close(code, reason) {
125
- for (const cb of this.closeCbs) {
126
- await cb(code, reason);
127
- }
128
- }
129
- }
130
- class Socket {
131
- constructor(handler, ws) {
132
- this.handler = handler;
133
- this.ws = ws;
134
- }
135
- set onmessage(cb) {
136
- this.handler.messageCbs.push(cb);
137
- }
138
- set onclose(cb) {
139
- this.handler.closeCbs.push(cb);
140
- }
141
- /**
142
- * Send binary data over the WebSocket connection;
143
- * @param message - The message buffer that will be sent.
144
- */
145
- send(message) {
146
- this.ws.send(message);
147
- }
148
- /**
149
- * Closes the WebSocket.
150
- * @param code - The close code. Must be one of:
151
- * - 1000: normal closure
152
- * - 1009: message too big
153
- * - 1011: server encountered error
154
- * - 1012: server restarting
155
- * - 1013: server too busy or rate-limiting
156
- * - 4000-4999: reserved for applications
157
- * @param reason - The reason the WebSocket was closed.
158
- */
159
- close(code, reason) {
160
- this.ws.close(code, reason);
161
- }
162
- }
163
- export const ws = (path, options) => {
164
- const paramsSchema = object(options.params);
165
- const querySchema = object(options.query);
166
- return {
167
- method: 'GET',
168
- path: new Path(path),
169
- async handle(req, params, server) {
170
- const url = new URL(req.url);
171
- if (req.headers.get('upgrade') !== 'websocket') {
172
- throw new BadRequestError(`WebSocket required for ${url.pathname}`);
173
- }
174
- const handler = new WebSocketHandler(async (ws) => {
175
- const socket = new Socket(handler, ws);
176
- let parsedParams;
177
- let parsedQuery;
178
- try {
179
- parsedParams = paramsSchema.parse(params);
180
- parsedQuery = querySchema.parse(Object.fromEntries(url.searchParams));
181
- }
182
- catch (error) {
183
- if (error instanceof ValidationError) {
184
- throw new BadRequestError(error.format());
185
- }
186
- throw error;
187
- }
188
- await options.do(socket, {
189
- url: url.pathname,
190
- params: parsedParams,
191
- query: parsedQuery,
192
- });
193
- });
194
- if (!server.upgrade(req, {
195
- data: {
196
- handler,
197
- },
198
- })) {
199
- throw new BadRequestError('Upgrading to WebSocket failed.');
200
- }
201
- return undefined;
202
- },
203
- documentation() {
204
- // TODO
205
- return {};
206
- },
207
- };
208
- };
@@ -1,83 +0,0 @@
1
- import type { Log, Schema, Typeof } from '../lib.js';
2
- /**
3
- * Indicates that the HTTP request failed. This can be
4
- * due to a network error or a response that indicates
5
- * failure. In the latter case, the response is part of
6
- * this error.
7
- */
8
- export declare class HttpRequestError extends Error {
9
- readonly response?: HttpResponse;
10
- constructor(message: string, response?: HttpResponse);
11
- }
12
- /**
13
- * The response to an HTTP request.
14
- */
15
- export declare class HttpResponse {
16
- /**
17
- * The HTTP status code.
18
- */
19
- readonly status: number;
20
- /**
21
- * The response headers.
22
- */
23
- readonly headers: Record<string, string>;
24
- /**
25
- * The raw response body.
26
- */
27
- private readonly response;
28
- /**
29
- * Creates a new HTTP response.
30
- * @param status - The status code.
31
- * @param headers - The response headers.
32
- * @param response - The raw response data.
33
- */
34
- constructor(status: number, headers: Record<string, string>, response: Uint8Array);
35
- /**
36
- * @returns The raw response data.
37
- */
38
- buffer(): Buffer;
39
- /**
40
- * @returns The response as a UTF-8 encoded string.
41
- */
42
- text(): string;
43
- /**
44
- * Parses the response as JSON and according to the
45
- * specified schema.
46
- * @param schema - The schema. Default is y.unknown().
47
- * @returns A JavaScript object.
48
- */
49
- json<T extends Schema<unknown>>(schema?: T): Typeof<T>;
50
- }
51
- /**
52
- * A class for performing HTTP requests.
53
- */
54
- export declare class Http {
55
- private log;
56
- constructor(log: Log);
57
- /**
58
- * Performs an HTTP GET request on the given URL.
59
- * @param url - The URL.
60
- * @param headers - The HTTP headers.
61
- * @returns The response.
62
- */
63
- get(url: string, headers?: Record<string, string>): Promise<HttpResponse>;
64
- /**
65
- * Performs an HTTP POST request on the given URL. The body is converted
66
- * to JSON.
67
- * @param url - The URL.
68
- * @param body - The body.
69
- * @param headers - The HTTP headers.
70
- * @returns The response.
71
- */
72
- post(url: string, body: unknown, headers?: Record<string, string>): Promise<HttpResponse>;
73
- /**
74
- * Performs an HTTP request on the given URL. The body is converted to JSON.
75
- * @param method - The HTTP method.
76
- * @param url - The URL.
77
- * @param body - The body.
78
- * @param headers - The HTTP headers.
79
- * @returns The response.
80
- */
81
- request(method: 'GET' | 'POST', url: string, body: unknown, headers?: Record<string, string>): Promise<HttpResponse>;
82
- private getResponse;
83
- }
@@ -1,123 +0,0 @@
1
- import { isUint8Array } from 'node:util/types';
2
- /**
3
- * Indicates that the HTTP request failed. This can be
4
- * due to a network error or a response that indicates
5
- * failure. In the latter case, the response is part of
6
- * this error.
7
- */
8
- export class HttpRequestError extends Error {
9
- constructor(message, response) {
10
- super(message);
11
- this.response = response;
12
- }
13
- }
14
- /**
15
- * The response to an HTTP request.
16
- */
17
- export class HttpResponse {
18
- /**
19
- * Creates a new HTTP response.
20
- * @param status - The status code.
21
- * @param headers - The response headers.
22
- * @param response - The raw response data.
23
- */
24
- constructor(status, headers, response) {
25
- this.status = status;
26
- this.headers = headers;
27
- this.response = Buffer.from(response);
28
- }
29
- /**
30
- * @returns The raw response data.
31
- */
32
- buffer() {
33
- return this.response;
34
- }
35
- /**
36
- * @returns The response as a UTF-8 encoded string.
37
- */
38
- text() {
39
- return this.response.toString('utf8');
40
- }
41
- /**
42
- * Parses the response as JSON and according to the
43
- * specified schema.
44
- * @param schema - The schema. Default is y.unknown().
45
- * @returns A JavaScript object.
46
- */
47
- json(schema) {
48
- const parsed = JSON.parse(this.text());
49
- if (schema) {
50
- return schema.parse(parsed);
51
- }
52
- return parsed;
53
- }
54
- }
55
- /**
56
- * A class for performing HTTP requests.
57
- */
58
- export class Http {
59
- constructor(log) {
60
- this.log = log;
61
- }
62
- /**
63
- * Performs an HTTP GET request on the given URL.
64
- * @param url - The URL.
65
- * @param headers - The HTTP headers.
66
- * @returns The response.
67
- */
68
- get(url, headers) {
69
- return this.request('GET', url, undefined, headers);
70
- }
71
- /**
72
- * Performs an HTTP POST request on the given URL. The body is converted
73
- * to JSON.
74
- * @param url - The URL.
75
- * @param body - The body.
76
- * @param headers - The HTTP headers.
77
- * @returns The response.
78
- */
79
- post(url, body, headers) {
80
- return this.request('POST', url, body, headers);
81
- }
82
- /**
83
- * Performs an HTTP request on the given URL. The body is converted to JSON.
84
- * @param method - The HTTP method.
85
- * @param url - The URL.
86
- * @param body - The body.
87
- * @param headers - The HTTP headers.
88
- * @returns The response.
89
- */
90
- async request(method, url, body, headers) {
91
- const isBuffer = isUint8Array(body);
92
- const requestBody = isBuffer ? body : JSON.stringify(body);
93
- const actualHeaders = isBuffer
94
- ? headers
95
- : { 'content-type': 'application/json', ...headers };
96
- const response = await this.getResponse(method, url, requestBody, actualHeaders);
97
- if (response.status < 200 || response.status >= 300) {
98
- throw new HttpRequestError(`HTTP request returned status code ${response.status}`, response);
99
- }
100
- return response;
101
- }
102
- async getResponse(method, url, body, headers) {
103
- try {
104
- const response = await fetch(url, {
105
- method,
106
- headers,
107
- body,
108
- });
109
- const buffer = Buffer.from(await response.arrayBuffer());
110
- const responseHeaders = {};
111
- response.headers.forEach((value, key) => {
112
- responseHeaders[key] = value;
113
- });
114
- return new HttpResponse(response.status, responseHeaders, buffer);
115
- }
116
- catch (error) {
117
- if (error instanceof Error) {
118
- throw new HttpRequestError(error.message);
119
- }
120
- throw error;
121
- }
122
- }
123
- }