yedra 0.13.15 → 0.14.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/README.md +1 -3
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +2 -2
- package/dist/routing/app.d.ts +23 -26
- package/dist/routing/app.js +126 -90
- package/dist/routing/path.js +4 -1
- package/dist/routing/rest.d.ts +23 -2
- package/dist/routing/rest.js +11 -20
- package/dist/util/counter.d.ts +7 -0
- package/dist/util/counter.js +24 -0
- package/dist/util/stream.d.ts +2 -0
- package/dist/util/stream.js +7 -0
- package/dist/validation/array.d.ts +2 -2
- package/dist/validation/array.js +5 -4
- package/dist/validation/body.d.ts +4 -3
- package/dist/validation/boolean.js +1 -1
- package/dist/validation/date.js +1 -1
- package/dist/validation/either.d.ts +2 -1
- package/dist/validation/either.js +2 -2
- package/dist/validation/enum.js +2 -3
- package/dist/validation/error.d.ts +3 -9
- package/dist/validation/error.js +9 -30
- package/dist/validation/integer.js +4 -4
- package/dist/validation/modifiable.d.ts +7 -0
- package/dist/validation/modifiable.js +15 -0
- package/dist/validation/none.d.ts +2 -1
- package/dist/validation/none.js +2 -2
- package/dist/validation/number.js +4 -4
- package/dist/validation/object.d.ts +3 -1
- package/dist/validation/object.js +15 -5
- package/dist/validation/raw.d.ts +4 -3
- package/dist/validation/raw.js +5 -4
- package/dist/validation/record.js +7 -2
- package/dist/validation/schema.d.ts +2 -1
- package/dist/validation/schema.js +9 -4
- package/dist/validation/stream.d.ts +13 -0
- package/dist/validation/stream.js +26 -0
- package/dist/validation/string.js +3 -3
- package/dist/validation/uuid.js +1 -1
- package/package.json +1 -1
- package/dist/validation/intersection.d.ts +0 -15
- package/dist/validation/intersection.js +0 -51
package/README.md
CHANGED
|
@@ -111,9 +111,7 @@ export const loginEndpoint = new Post({
|
|
|
111
111
|
password: y.string(),
|
|
112
112
|
}),
|
|
113
113
|
res: y.object({
|
|
114
|
-
token: y.string().
|
|
115
|
-
description: "The session token.",
|
|
116
|
-
}),
|
|
114
|
+
token: y.string().describe("The session token."),
|
|
117
115
|
}),
|
|
118
116
|
async do(req) {
|
|
119
117
|
if (await isValid(req.body.username, req.body.password)) {
|
package/dist/lib.d.ts
CHANGED
|
@@ -7,14 +7,14 @@ export { boolean } from './validation/boolean.js';
|
|
|
7
7
|
export { date } from './validation/date.js';
|
|
8
8
|
export { _enum as enum } from './validation/enum.js';
|
|
9
9
|
export { ValidationError } from './validation/error.js';
|
|
10
|
-
export { intersection } from './validation/intersection.js';
|
|
11
10
|
export { number } from './validation/number.js';
|
|
12
11
|
export { integer } from './validation/integer.js';
|
|
13
|
-
export { object } from './validation/object.js';
|
|
12
|
+
export { object, laxObject } from './validation/object.js';
|
|
14
13
|
export { record } from './validation/record.js';
|
|
15
14
|
export { Schema } from './validation/schema.js';
|
|
16
15
|
export { BodyType, type Typeof } from './validation/body.js';
|
|
17
16
|
export { raw } from './validation/raw.js';
|
|
17
|
+
export { stream } from './validation/stream.js';
|
|
18
18
|
export { either } from './validation/either.js';
|
|
19
19
|
export { string } from './validation/string.js';
|
|
20
20
|
export { union } from './validation/union.js';
|
package/dist/lib.js
CHANGED
|
@@ -12,14 +12,14 @@ export { boolean } from './validation/boolean.js';
|
|
|
12
12
|
export { date } from './validation/date.js';
|
|
13
13
|
export { _enum as enum } from './validation/enum.js';
|
|
14
14
|
export { ValidationError } from './validation/error.js';
|
|
15
|
-
export { intersection } from './validation/intersection.js';
|
|
16
15
|
export { number } from './validation/number.js';
|
|
17
16
|
export { integer } from './validation/integer.js';
|
|
18
|
-
export { object } from './validation/object.js';
|
|
17
|
+
export { object, laxObject } from './validation/object.js';
|
|
19
18
|
export { record } from './validation/record.js';
|
|
20
19
|
export { Schema } from './validation/schema.js';
|
|
21
20
|
export { BodyType } from './validation/body.js';
|
|
22
21
|
export { raw } from './validation/raw.js';
|
|
22
|
+
export { stream } from './validation/stream.js';
|
|
23
23
|
export { either } from './validation/either.js';
|
|
24
24
|
export { string } from './validation/string.js';
|
|
25
25
|
export { union } from './validation/union.js';
|
package/dist/routing/app.d.ts
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { Counter } from '../util/counter.js';
|
|
3
4
|
import type { SecurityScheme } from '../util/security.js';
|
|
4
5
|
import { RestEndpoint } from './rest.js';
|
|
5
6
|
import { WsEndpoint } from './websocket.js';
|
|
6
7
|
declare class Context {
|
|
7
|
-
private server;
|
|
8
|
-
|
|
8
|
+
private readonly server;
|
|
9
|
+
private readonly wss;
|
|
10
|
+
private readonly counter;
|
|
11
|
+
constructor(server: Server, wss: WebSocketServer, counter: Counter);
|
|
9
12
|
stop(): Promise<void>;
|
|
10
13
|
}
|
|
11
|
-
type MetricsConfig = {
|
|
12
|
-
port: number;
|
|
13
|
-
path: string;
|
|
14
|
-
get?: () => Promise<string>;
|
|
15
|
-
};
|
|
16
14
|
export declare class Yedra {
|
|
17
15
|
private restRoutes;
|
|
18
16
|
private wsRoutes;
|
|
19
|
-
private staticFiles;
|
|
20
17
|
private requestData;
|
|
21
|
-
private readonly metricsEndpoint;
|
|
22
|
-
constructor(options?: {
|
|
23
|
-
metrics: MetricsConfig;
|
|
24
|
-
});
|
|
25
18
|
use(path: string, endpoint: RestEndpoint | WsEndpoint | Yedra): Yedra;
|
|
26
|
-
static(dir: string, fallback?: string): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Handle an HTTP request.
|
|
29
|
-
* @param req - The HTTP request.
|
|
30
|
-
* @returns The HTTP response.
|
|
31
|
-
*/
|
|
32
|
-
fetch(url: URL | string, options?: {
|
|
33
|
-
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
34
|
-
body?: string | Buffer;
|
|
35
|
-
headers?: Record<string, string>;
|
|
36
|
-
}): Promise<Response>;
|
|
37
19
|
/**
|
|
38
20
|
* Generate OpenAPI documentation for the app.
|
|
39
21
|
*/
|
|
@@ -49,12 +31,27 @@ export declare class Yedra {
|
|
|
49
31
|
url: string;
|
|
50
32
|
}[];
|
|
51
33
|
}): object;
|
|
34
|
+
private static loadStatic;
|
|
35
|
+
private performRequest;
|
|
52
36
|
listen(port: number, options?: {
|
|
53
37
|
tls?: {
|
|
54
38
|
key: string;
|
|
55
39
|
cert: string;
|
|
56
40
|
};
|
|
57
|
-
|
|
41
|
+
metrics?: {
|
|
42
|
+
port: number;
|
|
43
|
+
path: string;
|
|
44
|
+
get?: () => Promise<string> | string;
|
|
45
|
+
};
|
|
46
|
+
static?: {
|
|
47
|
+
dir: string;
|
|
48
|
+
fallback?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Prevents all normal output from Yedra. Mostly useful for tests.
|
|
52
|
+
*/
|
|
53
|
+
quiet?: boolean;
|
|
54
|
+
}): Promise<Context>;
|
|
58
55
|
private static errorResponse;
|
|
59
56
|
private matchRestRoute;
|
|
60
57
|
private matchWsRoute;
|
package/dist/routing/app.js
CHANGED
|
@@ -5,27 +5,38 @@ import { extname, join } from 'node:path';
|
|
|
5
5
|
import { URL } from 'node:url';
|
|
6
6
|
import mime from 'mime';
|
|
7
7
|
import { WebSocketServer } from 'ws';
|
|
8
|
+
import { Counter } from '../util/counter.js';
|
|
8
9
|
import { HttpError } from './errors.js';
|
|
9
10
|
import { Path } from './path.js';
|
|
10
11
|
import { RestEndpoint } from './rest.js';
|
|
11
12
|
import { WsEndpoint } from './websocket.js';
|
|
12
13
|
class Context {
|
|
13
|
-
constructor(server) {
|
|
14
|
+
constructor(server, wss, counter) {
|
|
14
15
|
this.server = server;
|
|
16
|
+
this.wss = wss;
|
|
17
|
+
this.counter = counter;
|
|
15
18
|
}
|
|
16
19
|
stop() {
|
|
17
20
|
return new Promise((resolve) => {
|
|
18
|
-
|
|
21
|
+
// don't accept any new connections
|
|
22
|
+
this.wss.close();
|
|
23
|
+
this.server.close();
|
|
24
|
+
for (const client of this.wss.clients) {
|
|
25
|
+
// send shutdown message to all WebSocket clients
|
|
26
|
+
client.close(1000, 'Server Shutdown');
|
|
27
|
+
}
|
|
28
|
+
// wait until all connections are done
|
|
29
|
+
this.counter.wait().then(() => {
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
19
32
|
});
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
export class Yedra {
|
|
23
|
-
constructor(
|
|
36
|
+
constructor() {
|
|
24
37
|
this.restRoutes = [];
|
|
25
38
|
this.wsRoutes = [];
|
|
26
|
-
this.staticFiles = new Map();
|
|
27
39
|
this.requestData = {};
|
|
28
|
-
this.metricsEndpoint = options?.metrics;
|
|
29
40
|
}
|
|
30
41
|
use(path, endpoint) {
|
|
31
42
|
if (endpoint instanceof Yedra) {
|
|
@@ -49,62 +60,94 @@ export class Yedra {
|
|
|
49
60
|
}
|
|
50
61
|
return this;
|
|
51
62
|
}
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Generate OpenAPI documentation for the app.
|
|
65
|
+
*/
|
|
66
|
+
docs(options) {
|
|
67
|
+
const paths = {};
|
|
68
|
+
for (const route of this.restRoutes) {
|
|
69
|
+
const path = route.path.toString();
|
|
70
|
+
const methods = paths[path] ?? {};
|
|
71
|
+
methods[route.endpoint.method.toLowerCase()] =
|
|
72
|
+
route.endpoint.documentation(options.security ?? {});
|
|
73
|
+
paths[path] = methods;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
openapi: '3.0.2',
|
|
77
|
+
info: options.info,
|
|
78
|
+
components: {
|
|
79
|
+
securitySchemes: options.security,
|
|
80
|
+
},
|
|
81
|
+
servers: options.servers,
|
|
82
|
+
paths,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
static async loadStatic(options) {
|
|
86
|
+
if (options === undefined) {
|
|
87
|
+
return new Map();
|
|
88
|
+
}
|
|
89
|
+
const staticFiles = new Map();
|
|
90
|
+
const files = await readdir(options.dir, { recursive: true });
|
|
54
91
|
await Promise.all(files.map(async (file) => {
|
|
55
|
-
const absolute = join(dir, file);
|
|
92
|
+
const absolute = join(options.dir, file);
|
|
56
93
|
if (!(await stat(absolute)).isFile()) {
|
|
57
94
|
return;
|
|
58
95
|
}
|
|
59
96
|
const data = await readFile(absolute);
|
|
60
|
-
|
|
97
|
+
staticFiles.set(`/${file}`, {
|
|
61
98
|
data,
|
|
62
99
|
mime: mime.getType(extname(file)) ?? 'application/octet-stream',
|
|
63
100
|
});
|
|
64
101
|
}));
|
|
65
|
-
if (fallback) {
|
|
66
|
-
const data = await readFile(fallback);
|
|
67
|
-
|
|
102
|
+
if (options.fallback) {
|
|
103
|
+
const data = await readFile(options.fallback);
|
|
104
|
+
staticFiles.set('__fallback', {
|
|
68
105
|
data,
|
|
69
|
-
mime: mime.getType(extname(fallback)) ?? 'application/octet-stream',
|
|
106
|
+
mime: mime.getType(extname(options.fallback)) ?? 'application/octet-stream',
|
|
70
107
|
});
|
|
71
108
|
}
|
|
109
|
+
return staticFiles;
|
|
72
110
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const parsedUrl = typeof url === 'string' ? new URL(url, 'http://localhost') : url;
|
|
80
|
-
const method = options?.method ?? 'GET';
|
|
81
|
-
if (method !== 'GET' &&
|
|
82
|
-
method !== 'POST' &&
|
|
83
|
-
method !== 'PUT' &&
|
|
84
|
-
method !== 'DELETE') {
|
|
85
|
-
return Yedra.errorResponse(405, `Method '${method}' not allowed.`);
|
|
111
|
+
async performRequest(staticFiles, req) {
|
|
112
|
+
if (req.method !== 'GET' &&
|
|
113
|
+
req.method !== 'POST' &&
|
|
114
|
+
req.method !== 'PUT' &&
|
|
115
|
+
req.method !== 'DELETE') {
|
|
116
|
+
return Yedra.errorResponse(405, `Method \`${req.method}\` not allowed.`);
|
|
86
117
|
}
|
|
87
|
-
const match = this.matchRestRoute(
|
|
118
|
+
const match = this.matchRestRoute(req.url.pathname, req.method);
|
|
88
119
|
if (!match.result) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
// no matching route found
|
|
121
|
+
if (req.method === 'GET') {
|
|
122
|
+
// look for a static file
|
|
123
|
+
const staticFile = staticFiles.get(req.url.pathname) ?? staticFiles.get('__fallback');
|
|
93
124
|
if (staticFile !== undefined) {
|
|
94
|
-
return
|
|
125
|
+
return {
|
|
126
|
+
status: 200,
|
|
127
|
+
body: staticFile.data,
|
|
95
128
|
headers: {
|
|
96
129
|
'content-type': staticFile.mime,
|
|
97
130
|
},
|
|
98
|
-
}
|
|
131
|
+
};
|
|
99
132
|
}
|
|
100
133
|
}
|
|
101
134
|
if (match.invalidMethod) {
|
|
102
|
-
|
|
135
|
+
// we found a route, but it did not match the request method
|
|
136
|
+
return Yedra.errorResponse(405, `Method ${req.method} not allowed for path \`${req.url.pathname}\`.`);
|
|
103
137
|
}
|
|
104
|
-
return Yedra.errorResponse(404, `Path
|
|
138
|
+
return Yedra.errorResponse(404, `Path \`${req.url.pathname}\` not found.`);
|
|
105
139
|
}
|
|
106
140
|
try {
|
|
107
|
-
return await match.result.endpoint.handle(
|
|
141
|
+
return await match.result.endpoint.handle({
|
|
142
|
+
url: req.url.pathname,
|
|
143
|
+
body: req.body,
|
|
144
|
+
params: match.result.params,
|
|
145
|
+
query: Object.fromEntries(req.url.searchParams),
|
|
146
|
+
headers: Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
|
|
147
|
+
key,
|
|
148
|
+
Array.isArray(value) ? value.join(',') : (value ?? ''),
|
|
149
|
+
])),
|
|
150
|
+
});
|
|
108
151
|
}
|
|
109
152
|
catch (error) {
|
|
110
153
|
if (error instanceof HttpError) {
|
|
@@ -114,58 +157,45 @@ export class Yedra {
|
|
|
114
157
|
return Yedra.errorResponse(500, 'Internal Server Error.');
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*/
|
|
120
|
-
docs(options) {
|
|
121
|
-
const paths = {};
|
|
122
|
-
for (const route of this.restRoutes) {
|
|
123
|
-
const path = route.path.toString();
|
|
124
|
-
const methods = paths[path] ?? {};
|
|
125
|
-
methods[route.endpoint.method.toLowerCase()] =
|
|
126
|
-
route.endpoint.documentation(options.security ?? {});
|
|
127
|
-
paths[path] = methods;
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
openapi: '3.0.2',
|
|
131
|
-
info: options.info,
|
|
132
|
-
components: {
|
|
133
|
-
securitySchemes: options.security,
|
|
134
|
-
},
|
|
135
|
-
servers: options.servers,
|
|
136
|
-
paths,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
listen(port, options) {
|
|
160
|
+
async listen(port, options) {
|
|
161
|
+
const staticFiles = await Yedra.loadStatic(options?.static);
|
|
140
162
|
const server = options?.tls === undefined
|
|
141
163
|
? createHttpServer()
|
|
142
164
|
: createHttpsServer({
|
|
143
165
|
key: options.tls.key,
|
|
144
166
|
cert: options.tls.cert,
|
|
145
167
|
});
|
|
146
|
-
|
|
168
|
+
const counter = new Counter();
|
|
169
|
+
server.on('request', async (req, res) => {
|
|
170
|
+
counter.increment();
|
|
147
171
|
const url = new URL(req.url, 'http://localhost');
|
|
148
172
|
const begin = Date.now();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
|
|
155
|
-
const response = await this.fetch(url, {
|
|
156
|
-
method: req.method,
|
|
157
|
-
body: req.method === 'POST' || req.method === 'PUT' ? body : undefined,
|
|
158
|
-
headers: Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
|
|
159
|
-
key,
|
|
160
|
-
Array.isArray(value) ? value.join(',') : (value ?? ''),
|
|
161
|
-
])),
|
|
162
|
-
});
|
|
163
|
-
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
164
|
-
res.end(Buffer.from(await response.arrayBuffer()));
|
|
165
|
-
const duration = Date.now() - begin;
|
|
166
|
-
console.log(`${req.method} ${url.pathname} -> ${response.status} (${duration}ms)`);
|
|
167
|
-
this.track(req.method, response.status, duration / 1000);
|
|
173
|
+
const response = await this.performRequest(staticFiles, {
|
|
174
|
+
method: req.method ?? 'GET',
|
|
175
|
+
url,
|
|
176
|
+
body: req,
|
|
177
|
+
headers: req.headers,
|
|
168
178
|
});
|
|
179
|
+
const status = response.status ?? 200;
|
|
180
|
+
res.writeHead(status, response.headers);
|
|
181
|
+
if (response.body instanceof ReadableStream) {
|
|
182
|
+
for await (const chunk of response.body) {
|
|
183
|
+
res.write(chunk);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const buffer = response.body instanceof Uint8Array
|
|
188
|
+
? response.body
|
|
189
|
+
: JSON.stringify(response.body);
|
|
190
|
+
res.write(buffer);
|
|
191
|
+
}
|
|
192
|
+
res.end();
|
|
193
|
+
const duration = Date.now() - begin;
|
|
194
|
+
if (options?.quiet !== true) {
|
|
195
|
+
console.log(`${req.method} ${url.pathname} -> ${status} (${duration}ms)`);
|
|
196
|
+
}
|
|
197
|
+
this.track(req.method, status, duration / 1000);
|
|
198
|
+
counter.decrement();
|
|
169
199
|
});
|
|
170
200
|
const wss = new WebSocketServer({ server });
|
|
171
201
|
wss.on('connection', async (ws, req) => {
|
|
@@ -183,15 +213,18 @@ export class Yedra {
|
|
|
183
213
|
ws.close(4000 + error.status, error.message);
|
|
184
214
|
}
|
|
185
215
|
else {
|
|
186
|
-
|
|
216
|
+
console.error(error);
|
|
217
|
+
ws.close(1011, 'Internal Error');
|
|
187
218
|
}
|
|
188
219
|
}
|
|
189
220
|
});
|
|
190
221
|
server.listen(port, () => {
|
|
191
|
-
|
|
222
|
+
if (options?.quiet !== true) {
|
|
223
|
+
console.log(`yedra listening on http://localhost:${port}`);
|
|
224
|
+
}
|
|
192
225
|
});
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
if (options?.metrics !== undefined) {
|
|
227
|
+
const metricsEndpoint = options.metrics;
|
|
195
228
|
const metricsServer = createHttpServer();
|
|
196
229
|
metricsServer.on('request', async (req, res) => {
|
|
197
230
|
if (req.method === 'GET' && req.url === metricsEndpoint.path) {
|
|
@@ -208,18 +241,21 @@ export class Yedra {
|
|
|
208
241
|
}
|
|
209
242
|
});
|
|
210
243
|
metricsServer.listen(metricsEndpoint.port, () => {
|
|
211
|
-
|
|
244
|
+
if (options.quiet !== true) {
|
|
245
|
+
console.log(`yedra metrics on http://localhost:${metricsEndpoint.port}${metricsEndpoint.path}`);
|
|
246
|
+
}
|
|
212
247
|
});
|
|
213
248
|
}
|
|
214
|
-
return new Context(server);
|
|
249
|
+
return new Context(server, wss, counter);
|
|
215
250
|
}
|
|
216
251
|
static errorResponse(status, errorMessage) {
|
|
217
|
-
return
|
|
218
|
-
status,
|
|
219
|
-
errorMessage,
|
|
220
|
-
}, {
|
|
252
|
+
return {
|
|
221
253
|
status,
|
|
222
|
-
|
|
254
|
+
body: {
|
|
255
|
+
status,
|
|
256
|
+
errorMessage,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
223
259
|
}
|
|
224
260
|
matchRestRoute(url, method) {
|
|
225
261
|
let invalidMethod = false;
|
package/dist/routing/path.js
CHANGED
|
@@ -58,7 +58,10 @@ export class Path {
|
|
|
58
58
|
*/
|
|
59
59
|
match(path) {
|
|
60
60
|
const params = {};
|
|
61
|
-
const actual = path
|
|
61
|
+
const actual = path
|
|
62
|
+
.substring(1)
|
|
63
|
+
.split('/')
|
|
64
|
+
.filter((segment) => segment !== '');
|
|
62
65
|
if (this.expected.length < actual.length && !this.expected.includes('*')) {
|
|
63
66
|
// path cannot be longer than expected, unless it contains a wildcard
|
|
64
67
|
return undefined;
|
package/dist/routing/rest.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
1
2
|
import type { SecurityScheme } from '../util/security.js';
|
|
2
3
|
import type { BodyType, Typeof } from '../validation/body.js';
|
|
3
4
|
import { NoneBody } from '../validation/none.js';
|
|
@@ -33,7 +34,17 @@ type EndpointOptions<Params extends Record<string, Schema<unknown>>, Query exten
|
|
|
33
34
|
};
|
|
34
35
|
export declare abstract class RestEndpoint {
|
|
35
36
|
abstract get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
36
|
-
abstract handle(
|
|
37
|
+
abstract handle(req: {
|
|
38
|
+
url: string;
|
|
39
|
+
body: Readable;
|
|
40
|
+
params: Record<string, string>;
|
|
41
|
+
query: Record<string, string>;
|
|
42
|
+
headers: Record<string, string>;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
status?: number;
|
|
45
|
+
body: unknown;
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
}>;
|
|
37
48
|
abstract documentation(securitySchemes: Record<string, SecurityScheme>): object;
|
|
38
49
|
}
|
|
39
50
|
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 {
|
|
@@ -44,7 +55,17 @@ declare class ConcreteRestEndpoint<Params extends Record<string, Schema<unknown>
|
|
|
44
55
|
private headersSchema;
|
|
45
56
|
constructor(method: 'GET' | 'POST' | 'PUT' | 'DELETE', options: EndpointOptions<Params, Query, Headers, Req, Res>);
|
|
46
57
|
get method(): 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
47
|
-
handle(
|
|
58
|
+
handle(req: {
|
|
59
|
+
url: string;
|
|
60
|
+
body: Readable;
|
|
61
|
+
params: Record<string, string>;
|
|
62
|
+
query: Record<string, string>;
|
|
63
|
+
headers: Record<string, string>;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
status?: number;
|
|
66
|
+
body: unknown;
|
|
67
|
+
headers?: Record<string, string>;
|
|
68
|
+
}>;
|
|
48
69
|
documentation(securitySchemes: Record<string, SecurityScheme>): object;
|
|
49
70
|
}
|
|
50
71
|
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,7 +1,7 @@
|
|
|
1
1
|
import { paramDocs } from '../util/docs.js';
|
|
2
2
|
import { Issue, ValidationError } from '../validation/error.js';
|
|
3
3
|
import { NoneBody, none } from '../validation/none.js';
|
|
4
|
-
import { object } from '../validation/object.js';
|
|
4
|
+
import { laxObject, object } from '../validation/object.js';
|
|
5
5
|
import { BadRequestError } from './errors.js';
|
|
6
6
|
export class RestEndpoint {
|
|
7
7
|
}
|
|
@@ -12,23 +12,24 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
12
12
|
this.options = options;
|
|
13
13
|
this.paramsSchema = object(options.params);
|
|
14
14
|
this.querySchema = object(options.query);
|
|
15
|
-
|
|
15
|
+
// headers need to be lax, since there are lots of them
|
|
16
|
+
this.headersSchema = laxObject(options.headers);
|
|
16
17
|
}
|
|
17
18
|
get method() {
|
|
18
19
|
return this._method;
|
|
19
20
|
}
|
|
20
|
-
async handle(
|
|
21
|
+
async handle(req) {
|
|
21
22
|
let parsedBody;
|
|
22
23
|
let parsedParams;
|
|
23
24
|
let parsedQuery;
|
|
24
25
|
let parsedHeaders;
|
|
25
26
|
const issues = [];
|
|
26
27
|
try {
|
|
27
|
-
parsedBody = this.options.req.deserialize(
|
|
28
|
+
parsedBody = await this.options.req.deserialize(req.body, req.headers['content-type'] ?? 'application/octet-stream');
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
if (error instanceof SyntaxError) {
|
|
31
|
-
issues.push(new Issue(
|
|
32
|
+
issues.push(new Issue(['body'], error.message));
|
|
32
33
|
}
|
|
33
34
|
else if (error instanceof ValidationError) {
|
|
34
35
|
issues.push(...error.withPrefix('body'));
|
|
@@ -38,7 +39,7 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
try {
|
|
41
|
-
parsedParams = this.paramsSchema.parse(params);
|
|
42
|
+
parsedParams = this.paramsSchema.parse(req.params);
|
|
42
43
|
}
|
|
43
44
|
catch (error) {
|
|
44
45
|
if (error instanceof ValidationError) {
|
|
@@ -49,7 +50,7 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
try {
|
|
52
|
-
parsedQuery = this.querySchema.parse(query);
|
|
53
|
+
parsedQuery = this.querySchema.parse(req.query);
|
|
53
54
|
}
|
|
54
55
|
catch (error) {
|
|
55
56
|
if (error instanceof ValidationError) {
|
|
@@ -60,7 +61,7 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
try {
|
|
63
|
-
parsedHeaders = this.headersSchema.parse(headers);
|
|
64
|
+
parsedHeaders = this.headersSchema.parse(req.headers);
|
|
64
65
|
}
|
|
65
66
|
catch (error) {
|
|
66
67
|
if (error instanceof ValidationError) {
|
|
@@ -74,8 +75,8 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
74
75
|
const error = new ValidationError(issues);
|
|
75
76
|
throw new BadRequestError(error.format());
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
url,
|
|
78
|
+
return await this.options.do({
|
|
79
|
+
url: req.url,
|
|
79
80
|
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
80
81
|
params: parsedParams,
|
|
81
82
|
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
@@ -85,16 +86,6 @@ class ConcreteRestEndpoint extends RestEndpoint {
|
|
|
85
86
|
// biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
|
|
86
87
|
body: parsedBody,
|
|
87
88
|
});
|
|
88
|
-
if (response.body instanceof Uint8Array) {
|
|
89
|
-
return new Response(response.body, {
|
|
90
|
-
status: response.status,
|
|
91
|
-
headers: response.headers,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return Response.json(response.body, {
|
|
95
|
-
status: response.status,
|
|
96
|
-
headers: response.headers,
|
|
97
|
-
});
|
|
98
89
|
}
|
|
99
90
|
documentation(securitySchemes) {
|
|
100
91
|
const parameters = [
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class Counter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.count = 0;
|
|
4
|
+
}
|
|
5
|
+
wait() {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
if (this.count === 0) {
|
|
8
|
+
resolve();
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
this.resolve = resolve;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
increment() {
|
|
16
|
+
this.count += 1;
|
|
17
|
+
}
|
|
18
|
+
decrement() {
|
|
19
|
+
this.count -= 1;
|
|
20
|
+
if (this.count === 0 && this.resolve !== undefined) {
|
|
21
|
+
this.resolve();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Typeof } from './body.js';
|
|
2
2
|
import { ModifiableSchema } from './modifiable.js';
|
|
3
3
|
import type { Schema } from './schema.js';
|
|
4
|
-
declare class ArraySchema<ItemSchema extends Schema<unknown>> extends ModifiableSchema<Typeof<ItemSchema>[]> {
|
|
4
|
+
export declare class ArraySchema<ItemSchema extends Schema<unknown>> extends ModifiableSchema<Typeof<ItemSchema>[]> {
|
|
5
5
|
private readonly itemSchema;
|
|
6
6
|
private readonly minItems?;
|
|
7
7
|
private readonly maxItems?;
|
|
@@ -28,6 +28,6 @@ declare class ArraySchema<ItemSchema extends Schema<unknown>> extends Modifiable
|
|
|
28
28
|
/**
|
|
29
29
|
* A schema matching arrays of the provided item type.
|
|
30
30
|
* @param itemSchema - The schema for array items.
|
|
31
|
+
* @deprecated Use the .array() method instead.
|
|
31
32
|
*/
|
|
32
33
|
export declare const array: <ItemSchema extends Schema<unknown>>(itemSchema: ItemSchema) => ArraySchema<ItemSchema>;
|
|
33
|
-
export {};
|