yedra 0.20.11 → 0.20.13

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.
Files changed (81) hide show
  1. package/dist/routing/rest.d.ts +2 -0
  2. package/dist/routing/rest.js +2 -0
  3. package/dist/routing/websocket.d.ts +1 -0
  4. package/dist/routing/websocket.js +1 -0
  5. package/dist/src/index.d.ts +9 -0
  6. package/dist/src/index.js +7 -0
  7. package/dist/src/lib.d.ts +10 -0
  8. package/dist/src/lib.js +16 -0
  9. package/dist/src/routing/app.d.ts +165 -0
  10. package/dist/src/routing/app.js +537 -0
  11. package/dist/src/routing/env.d.ts +4 -0
  12. package/dist/src/routing/env.js +13 -0
  13. package/dist/src/routing/errors.d.ts +50 -0
  14. package/dist/src/routing/errors.js +64 -0
  15. package/dist/src/routing/log.d.ts +22 -0
  16. package/dist/src/routing/log.js +30 -0
  17. package/dist/src/routing/path.d.ts +44 -0
  18. package/dist/src/routing/path.js +110 -0
  19. package/dist/src/routing/rest.d.ts +103 -0
  20. package/dist/src/routing/rest.js +181 -0
  21. package/dist/src/routing/websocket.d.ts +60 -0
  22. package/dist/src/routing/websocket.js +132 -0
  23. package/dist/src/schema-lib.d.ts +17 -0
  24. package/dist/src/schema-lib.js +20 -0
  25. package/dist/src/schema.d.ts +2 -0
  26. package/dist/src/schema.js +4 -0
  27. package/dist/src/util/counter.d.ts +7 -0
  28. package/dist/src/util/counter.js +24 -0
  29. package/dist/src/util/docs.d.ts +9 -0
  30. package/dist/src/util/docs.js +57 -0
  31. package/dist/src/util/security.d.ts +14 -0
  32. package/dist/src/util/security.js +6 -0
  33. package/dist/src/util/stream.d.ts +2 -0
  34. package/dist/src/util/stream.js +7 -0
  35. package/dist/src/validation/body.d.ts +46 -0
  36. package/dist/src/validation/body.js +15 -0
  37. package/dist/src/validation/boolean.d.ts +10 -0
  38. package/dist/src/validation/boolean.js +27 -0
  39. package/dist/src/validation/date.d.ts +11 -0
  40. package/dist/src/validation/date.js +29 -0
  41. package/dist/src/validation/doc.d.ts +10 -0
  42. package/dist/src/validation/doc.js +22 -0
  43. package/dist/src/validation/either.d.ts +15 -0
  44. package/dist/src/validation/either.js +38 -0
  45. package/dist/src/validation/enum.d.ts +19 -0
  46. package/dist/src/validation/enum.js +44 -0
  47. package/dist/src/validation/error.d.ts +21 -0
  48. package/dist/src/validation/error.js +32 -0
  49. package/dist/src/validation/integer.d.ts +23 -0
  50. package/dist/src/validation/integer.js +64 -0
  51. package/dist/src/validation/json.d.ts +12 -0
  52. package/dist/src/validation/json.js +33 -0
  53. package/dist/src/validation/lazy.d.ts +48 -0
  54. package/dist/src/validation/lazy.js +78 -0
  55. package/dist/src/validation/modifiable.d.ts +105 -0
  56. package/dist/src/validation/modifiable.js +223 -0
  57. package/dist/src/validation/none.d.ts +10 -0
  58. package/dist/src/validation/none.js +14 -0
  59. package/dist/src/validation/null.d.ts +10 -0
  60. package/dist/src/validation/null.js +21 -0
  61. package/dist/src/validation/number.d.ts +23 -0
  62. package/dist/src/validation/number.js +58 -0
  63. package/dist/src/validation/object.d.ts +38 -0
  64. package/dist/src/validation/object.js +87 -0
  65. package/dist/src/validation/raw.d.ts +13 -0
  66. package/dist/src/validation/raw.js +21 -0
  67. package/dist/src/validation/record.d.ts +16 -0
  68. package/dist/src/validation/record.js +51 -0
  69. package/dist/src/validation/schema.d.ts +35 -0
  70. package/dist/src/validation/schema.js +76 -0
  71. package/dist/src/validation/stream.d.ts +13 -0
  72. package/dist/src/validation/stream.js +26 -0
  73. package/dist/src/validation/string.d.ts +29 -0
  74. package/dist/src/validation/string.js +51 -0
  75. package/dist/src/validation/union.d.ts +16 -0
  76. package/dist/src/validation/union.js +36 -0
  77. package/dist/src/validation/unknown.d.ts +10 -0
  78. package/dist/src/validation/unknown.js +13 -0
  79. package/dist/src/validation/uuid.d.ts +11 -0
  80. package/dist/src/validation/uuid.js +23 -0
  81. package/package.json +3 -4
@@ -0,0 +1,30 @@
1
+ export class Log {
2
+ /**
3
+ * Output a debug message.
4
+ * @param args - The message.
5
+ */
6
+ debug(...args) {
7
+ console.debug(...args);
8
+ }
9
+ /**
10
+ * Output an informational message.
11
+ * @param args - The message.
12
+ */
13
+ info(...args) {
14
+ console.info(...args);
15
+ }
16
+ /**
17
+ * Output a warning message.
18
+ * @param args - The message.
19
+ */
20
+ warning(...args) {
21
+ console.warn(...args);
22
+ }
23
+ /**
24
+ * Output an error message.
25
+ * @param args - The message.
26
+ */
27
+ error(...args) {
28
+ console.error(...args);
29
+ }
30
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Represents an HTTP API path. Provides methods for concatenating paths and
3
+ * extracting path parameters from strings.
4
+ */
5
+ export declare class Path {
6
+ private readonly expected;
7
+ /**
8
+ * Creates a new path. The path must start with `/`. Every part of the path
9
+ * must match /^(:?[a-z0-9]+\??)|\*$/. It can either be a specific word, e.g.
10
+ * `template`, or a parameter which starts with `:`, e.g. `:id`. If the
11
+ * parameter should be optional, add a `?`, e.g. `:id?`. The last part of the
12
+ * path can also be `*`, which matches any number of arbitrary segments.
13
+ * @param path - The path parameter.
14
+ */
15
+ constructor(path: string);
16
+ /**
17
+ * Prepends this path with another path.
18
+ * @param prefix - The prefix to prepend.
19
+ * @returns The prefixed path.
20
+ */
21
+ withPrefix(prefix: string): Path;
22
+ /**
23
+ * Returns the path as a string. The segments are joined with `/`, and the
24
+ * result is can be converted to a `Path` again.
25
+ * @returns The path as a string.
26
+ */
27
+ toString(): string;
28
+ /**
29
+ * Match this API path to a string. If the string matches the path, this
30
+ * returns a record mapping the path parameters (e.g. `:id`) to the values
31
+ * found in the actual string. Otherwise, it returns
32
+ * undefined.
33
+ * @param path - The string path to match.
34
+ */
35
+ match(path: string): {
36
+ params: Record<string, string>;
37
+ score: number;
38
+ } | undefined;
39
+ /**
40
+ * Remove `:` and `?` from a parameter path segment.
41
+ * @param param - The path segment.
42
+ */
43
+ private static normalizeParam;
44
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Represents an HTTP API path. Provides methods for concatenating paths and
3
+ * extracting path parameters from strings.
4
+ */
5
+ export class Path {
6
+ /**
7
+ * Creates a new path. The path must start with `/`. Every part of the path
8
+ * must match /^(:?[a-z0-9]+\??)|\*$/. It can either be a specific word, e.g.
9
+ * `template`, or a parameter which starts with `:`, e.g. `:id`. If the
10
+ * parameter should be optional, add a `?`, e.g. `:id?`. The last part of the
11
+ * path can also be `*`, which matches any number of arbitrary segments.
12
+ * @param path - The path parameter.
13
+ */
14
+ constructor(path) {
15
+ if (!path.startsWith('/')) {
16
+ throw new Error(`API path ${path} is invalid: Must start with '/'.`);
17
+ }
18
+ this.expected = path
19
+ .substring(1)
20
+ .split('/')
21
+ .filter((segment) => segment !== '');
22
+ const invalidSegment = this.expected.find((part) => part.match(/^((:?[A-Za-z0-9\-.]+\??)|\*)$/) === null);
23
+ if (invalidSegment) {
24
+ throw new Error(`API path ${path} is invalid: Segment ${invalidSegment} does not match regex /^((:?[A-Za-z0-9-.]+\\??)|\\*)$/.`);
25
+ }
26
+ const wildcard = this.expected.indexOf('*');
27
+ if (wildcard !== -1 && wildcard !== this.expected.length - 1) {
28
+ throw new Error(`API path ${path} is invalid: * must be the last path segment.`);
29
+ }
30
+ const firstOptional = this.expected.findIndex((part) => part.endsWith('?'));
31
+ if (firstOptional !== -1 &&
32
+ !this.expected.slice(firstOptional).every((part) => part.endsWith('?'))) {
33
+ throw new Error(`API path ${path} is invalid: Optional segment cannot be followed by non-optional segment.`);
34
+ }
35
+ }
36
+ /**
37
+ * Prepends this path with another path.
38
+ * @param prefix - The prefix to prepend.
39
+ * @returns The prefixed path.
40
+ */
41
+ withPrefix(prefix) {
42
+ return new Path(`${prefix}/${this.expected.join('/')}`);
43
+ }
44
+ /**
45
+ * Returns the path as a string. The segments are joined with `/`, and the
46
+ * result is can be converted to a `Path` again.
47
+ * @returns The path as a string.
48
+ */
49
+ toString() {
50
+ return `/${this.expected.map((segment) => (segment.startsWith(':') ? `{${segment.substring(1)}}` : segment)).join('/')}`;
51
+ }
52
+ /**
53
+ * Match this API path to a string. If the string matches the path, this
54
+ * returns a record mapping the path parameters (e.g. `:id`) to the values
55
+ * found in the actual string. Otherwise, it returns
56
+ * undefined.
57
+ * @param path - The string path to match.
58
+ */
59
+ match(path) {
60
+ const params = {};
61
+ const actual = path
62
+ .substring(1)
63
+ .split('/')
64
+ .filter((segment) => segment !== '');
65
+ if (this.expected.length < actual.length && !this.expected.includes('*')) {
66
+ // path cannot be longer than expected, unless it contains a wildcard
67
+ return undefined;
68
+ }
69
+ for (let i = 0; i < actual.length; ++i) {
70
+ if (this.expected[i] === '*') {
71
+ // wildcard, parsing successful
72
+ return { params, score: Number.POSITIVE_INFINITY };
73
+ }
74
+ if (this.expected[i].startsWith(':')) {
75
+ // parameter, accept anything
76
+ params[Path.normalizeParam(this.expected[i])] = actual[i];
77
+ }
78
+ else if (Path.normalizeParam(this.expected[i]) !== actual[i].toLowerCase()) {
79
+ // not a parameter, and the values don't match
80
+ return undefined;
81
+ }
82
+ }
83
+ if (actual.length < this.expected.length &&
84
+ !this.expected[actual.length].endsWith('?') &&
85
+ this.expected[actual.length] !== '*') {
86
+ // path is incomplete
87
+ return undefined;
88
+ }
89
+ return {
90
+ params,
91
+ score: this.expected.includes('*')
92
+ ? Number.POSITIVE_INFINITY
93
+ : Object.keys(params).length,
94
+ };
95
+ }
96
+ /**
97
+ * Remove `:` and `?` from a parameter path segment.
98
+ * @param param - The path segment.
99
+ */
100
+ static normalizeParam(param) {
101
+ let result = param;
102
+ if (result.startsWith(':')) {
103
+ result = result.substring(1);
104
+ }
105
+ if (result.endsWith('?')) {
106
+ result = result.substring(0, result.length - 1);
107
+ }
108
+ return result;
109
+ }
110
+ }
@@ -0,0 +1,103 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { SecurityScheme } from '../util/security.js';
3
+ import type { BodyType, Typeof, TypeofAccepts } from '../validation/body.js';
4
+ import { NoneBody } from '../validation/none.js';
5
+ import { type ObjectSchema } from '../validation/object.js';
6
+ import type { Schema } from '../validation/schema.js';
7
+ type ReqObject<Params, Query, Headers, Body> = {
8
+ url: string;
9
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
10
+ params: Params;
11
+ query: Query;
12
+ headers: Headers;
13
+ rawHeaders: Record<string, string>;
14
+ body: Body;
15
+ };
16
+ type ResObject<Body> = Promise<{
17
+ status?: number;
18
+ body: Body;
19
+ headers?: Record<string, string | undefined>;
20
+ }> | {
21
+ status?: number;
22
+ body: Body;
23
+ headers?: Record<string, string | undefined>;
24
+ };
25
+ type EndpointOptions<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Req extends BodyType<unknown, unknown>, Res extends BodyType<unknown, unknown>> = {
26
+ category: string;
27
+ summary: string;
28
+ description?: string;
29
+ /**
30
+ * List of security schemes that apply to this endpoint.
31
+ */
32
+ security?: SecurityScheme[];
33
+ /**
34
+ * Whether this endpoint should be excluded from the documentation.
35
+ * Default is false.
36
+ */
37
+ hidden?: boolean;
38
+ params: Params;
39
+ query: Query;
40
+ headers: Headers;
41
+ req: Req;
42
+ res: Res;
43
+ do: (req: ReqObject<Typeof<ObjectSchema<Params>>, Typeof<ObjectSchema<Query>>, Typeof<ObjectSchema<Headers>>, Typeof<Req>>) => ResObject<TypeofAccepts<Res>>;
44
+ };
45
+ export declare abstract class RestEndpoint {
46
+ abstract get method(): 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
47
+ abstract handle(req: {
48
+ url: string;
49
+ body: Readable;
50
+ params: Record<string, string>;
51
+ query: Record<string, string>;
52
+ headers: Record<string, string>;
53
+ }): Promise<{
54
+ status?: number;
55
+ body: unknown;
56
+ headers?: Record<string, string | undefined>;
57
+ }>;
58
+ abstract isHidden(): boolean;
59
+ abstract documentation(path: string, securitySchemes: Set<SecurityScheme>): object;
60
+ }
61
+ /**
62
+ * This class implements all REST endpoints in yedra. Its parent class,
63
+ * `RestEndpoint`, is not abstract because there are multiple implementations,
64
+ * but so that we can hide all the generic parameters of this class.
65
+ */
66
+ 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, unknown>, Res extends BodyType<unknown, unknown>> extends RestEndpoint {
67
+ private _method;
68
+ private options;
69
+ private paramsSchema;
70
+ private querySchema;
71
+ private headersSchema;
72
+ constructor(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', options: EndpointOptions<Params, Query, Headers, Req, Res>);
73
+ get method(): 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
74
+ handle(req: {
75
+ url: string;
76
+ body: Readable;
77
+ params: Record<string, string>;
78
+ query: Record<string, string>;
79
+ headers: Record<string, string>;
80
+ }): Promise<{
81
+ status?: number;
82
+ body: unknown;
83
+ headers?: Record<string, string | undefined>;
84
+ }>;
85
+ isHidden(): boolean;
86
+ documentation(path: string, securitySchemes: Set<SecurityScheme>): object;
87
+ }
88
+ 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, unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, NoneBody, Res> {
89
+ constructor(options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, 'req'>);
90
+ }
91
+ 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, unknown>, Res extends BodyType<unknown, unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, Req, Res> {
92
+ constructor(options: EndpointOptions<Params, Query, Headers, Req, Res>);
93
+ }
94
+ 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, unknown>, Res extends BodyType<unknown, unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, Req, Res> {
95
+ constructor(options: EndpointOptions<Params, Query, Headers, Req, Res>);
96
+ }
97
+ export declare class Patch<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>, Req extends BodyType<unknown, unknown>, Res extends BodyType<unknown, unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, Req, Res> {
98
+ constructor(options: EndpointOptions<Params, Query, Headers, Req, Res>);
99
+ }
100
+ 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, unknown>> extends ConcreteRestEndpoint<Params, Query, Headers, NoneBody, Res> {
101
+ constructor(options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, 'req'>);
102
+ }
103
+ export {};
@@ -0,0 +1,181 @@
1
+ import { paramDocs } from '../util/docs.js';
2
+ import { Issue, ValidationError } from '../validation/error.js';
3
+ import { NoneBody, none } from '../validation/none.js';
4
+ import { laxObject, object } from '../validation/object.js';
5
+ import { BadRequestError } from './errors.js';
6
+ export class RestEndpoint {
7
+ }
8
+ /**
9
+ * This class implements all REST endpoints in yedra. Its parent class,
10
+ * `RestEndpoint`, is not abstract because there are multiple implementations,
11
+ * but so that we can hide all the generic parameters of this class.
12
+ */
13
+ class ConcreteRestEndpoint extends RestEndpoint {
14
+ constructor(method, options) {
15
+ super();
16
+ this._method = method;
17
+ this.options = options;
18
+ this.paramsSchema = object(options.params);
19
+ // queries can sometimes include other elements for application-unrelated reasons
20
+ this.querySchema = laxObject(options.query);
21
+ // headers need to be lax, since there are lots of them
22
+ this.headersSchema = laxObject(options.headers);
23
+ }
24
+ get method() {
25
+ return this._method;
26
+ }
27
+ async handle(req) {
28
+ let parsedBody;
29
+ let parsedParams;
30
+ let parsedQuery;
31
+ let parsedHeaders;
32
+ const issues = [];
33
+ try {
34
+ const result = await this.options.req.deserialize(req.body, req.headers['content-type'] ?? 'application/octet-stream');
35
+ parsedBody = result;
36
+ }
37
+ catch (error) {
38
+ if (error instanceof SyntaxError) {
39
+ issues.push(new Issue(['body'], error.message));
40
+ }
41
+ else if (error instanceof ValidationError) {
42
+ issues.push(...error.withPrefix('body'));
43
+ }
44
+ else {
45
+ throw error;
46
+ }
47
+ }
48
+ try {
49
+ parsedParams = this.paramsSchema.parse(req.params);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof ValidationError) {
53
+ issues.push(...error.withPrefix('params'));
54
+ }
55
+ else {
56
+ throw error;
57
+ }
58
+ }
59
+ try {
60
+ parsedQuery = this.querySchema.parse(req.query);
61
+ }
62
+ catch (error) {
63
+ if (error instanceof ValidationError) {
64
+ issues.push(...error.withPrefix('query'));
65
+ }
66
+ else {
67
+ throw error;
68
+ }
69
+ }
70
+ try {
71
+ parsedHeaders = this.headersSchema.parse(req.headers);
72
+ }
73
+ catch (error) {
74
+ if (error instanceof ValidationError) {
75
+ issues.push(...error.withPrefix('headers'));
76
+ }
77
+ else {
78
+ throw error;
79
+ }
80
+ }
81
+ if (issues.length > 0) {
82
+ const error = new ValidationError(issues);
83
+ throw new BadRequestError(error.format());
84
+ }
85
+ return await this.options.do({
86
+ url: req.url,
87
+ method: this._method,
88
+ // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
89
+ params: parsedParams,
90
+ // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
91
+ query: parsedQuery,
92
+ // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
93
+ headers: parsedHeaders,
94
+ rawHeaders: req.headers,
95
+ // biome-ignore lint/style/noNonNullAssertion: this is required to convince TypeScript that this is initialized
96
+ body: parsedBody,
97
+ });
98
+ }
99
+ isHidden() {
100
+ return this.options.hidden ?? false;
101
+ }
102
+ documentation(path, securitySchemes) {
103
+ const security = this.options.security ?? [];
104
+ for (const scheme of security) {
105
+ // add all our security schemes to the global list of schemes
106
+ securitySchemes.add(scheme);
107
+ }
108
+ const parameters = [
109
+ ...paramDocs(this.options.params, 'path', security),
110
+ ...paramDocs(this.options.query, 'query', security),
111
+ ...paramDocs(this.options.headers, 'header', security),
112
+ ];
113
+ return {
114
+ tags: [this.options.category],
115
+ summary: this.options.summary,
116
+ description: this.options.description,
117
+ operationId: `${path.substring(1).replaceAll('/', '_')}_${this.method.toLowerCase()}`,
118
+ security: security.map((security) => ({ [security.name]: [] })),
119
+ parameters,
120
+ requestBody: this.options.req instanceof NoneBody
121
+ ? undefined
122
+ : {
123
+ required: true,
124
+ content: this.options.req.bodyDocs(),
125
+ },
126
+ responses: {
127
+ '200': {
128
+ description: 'Success',
129
+ content: this.options.res.bodyDocs(),
130
+ },
131
+ '400': {
132
+ description: 'Bad Request',
133
+ content: {
134
+ 'application/json': {
135
+ schema: {
136
+ type: 'object',
137
+ properties: {
138
+ status: {
139
+ type: 'number',
140
+ },
141
+ errorMessage: {
142
+ type: 'string',
143
+ },
144
+ code: {
145
+ type: 'string',
146
+ },
147
+ },
148
+ required: ['status', 'errorMessage'],
149
+ },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ };
155
+ }
156
+ }
157
+ export class Get extends ConcreteRestEndpoint {
158
+ constructor(options) {
159
+ super('GET', { req: none(), ...options });
160
+ }
161
+ }
162
+ export class Post extends ConcreteRestEndpoint {
163
+ constructor(options) {
164
+ super('POST', options);
165
+ }
166
+ }
167
+ export class Put extends ConcreteRestEndpoint {
168
+ constructor(options) {
169
+ super('PUT', options);
170
+ }
171
+ }
172
+ export class Patch extends ConcreteRestEndpoint {
173
+ constructor(options) {
174
+ super('PATCH', options);
175
+ }
176
+ }
177
+ export class Delete extends ConcreteRestEndpoint {
178
+ constructor(options) {
179
+ super('DELETE', { req: none(), ...options });
180
+ }
181
+ }
@@ -0,0 +1,60 @@
1
+ import type { URL } from 'node:url';
2
+ import type { WebSocket as NodeWebSocket } from 'ws';
3
+ import type { Typeof } from '../validation/body.js';
4
+ import { type ObjectSchema } from '../validation/object.js';
5
+ import type { Schema } from '../validation/schema.js';
6
+ type MessageCb = (message: Buffer) => Promise<void> | void;
7
+ type CloseCb = (code: number | undefined, reason: string | undefined) => Promise<void> | void;
8
+ declare class YedraWebSocket {
9
+ private ws;
10
+ private messageQueue;
11
+ private messageHandlers;
12
+ private closeHandlers;
13
+ constructor(ws: NodeWebSocket);
14
+ set onmessage(cb: MessageCb);
15
+ set onclose(cb: CloseCb);
16
+ /**
17
+ * Send binary data over the WebSocket connection;
18
+ * @param message - The message that will be sent.
19
+ */
20
+ send(message: Uint8Array<ArrayBufferLike> | string): void;
21
+ /**
22
+ * Closes the WebSocket.
23
+ * @param code - The close code. Must be one of:
24
+ * - 1000: normal closure
25
+ * - 1009: message too big
26
+ * - 1011: server encountered error
27
+ * - 1012: server restarting
28
+ * - 1013: server too busy or rate-limiting
29
+ * - 4000-4999: reserved for applications
30
+ * @param reason - The reason the WebSocket was closed.
31
+ */
32
+ close(code?: number, reason?: string): void;
33
+ }
34
+ export type { YedraWebSocket };
35
+ type WebSocketOptions<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>> = {
36
+ category: string;
37
+ summary: string;
38
+ description?: string;
39
+ params: Params;
40
+ query: Query;
41
+ headers: Headers;
42
+ do: (ws: YedraWebSocket, req: {
43
+ url: string;
44
+ params: Typeof<ObjectSchema<Params>>;
45
+ query: Typeof<ObjectSchema<Query>>;
46
+ headers: Typeof<ObjectSchema<Headers>>;
47
+ }) => Promise<void> | void;
48
+ };
49
+ export declare abstract class WsEndpoint {
50
+ abstract handle(url: URL, params: Record<string, string>, headers: Record<string, string>, ws: NodeWebSocket): Promise<void>;
51
+ }
52
+ export declare class Ws<Params extends Record<string, Schema<unknown>>, Query extends Record<string, Schema<unknown>>, Headers extends Record<string, Schema<unknown>>> extends WsEndpoint {
53
+ private options;
54
+ private paramsSchema;
55
+ private querySchema;
56
+ private headersSchema;
57
+ constructor(options: WebSocketOptions<Params, Query, Headers>);
58
+ handle(url: URL, params: Record<string, string>, headers: Record<string, string>, ws: NodeWebSocket): Promise<void>;
59
+ documentation(): object;
60
+ }
@@ -0,0 +1,132 @@
1
+ import { paramDocs } from '../util/docs.js';
2
+ import { ValidationError } from '../validation/error.js';
3
+ import { laxObject, object } from '../validation/object.js';
4
+ import { BadRequestError } from './errors.js';
5
+ class YedraWebSocket {
6
+ constructor(ws) {
7
+ this.messageQueue = [];
8
+ this.messageHandlers = [];
9
+ this.closeHandlers = [];
10
+ this.ws = ws;
11
+ ws.on('message', (data) => {
12
+ for (const cb of this.messageHandlers) {
13
+ cb(data);
14
+ }
15
+ if (this.messageHandlers.length === 0) {
16
+ // there are no message handlers registered yet,
17
+ // add the message to the queue
18
+ this.messageQueue.push(data);
19
+ }
20
+ });
21
+ ws.on('close', (code, reason) => {
22
+ for (const cb of this.closeHandlers) {
23
+ cb(code, reason);
24
+ }
25
+ });
26
+ }
27
+ set onmessage(cb) {
28
+ this.messageHandlers.push(cb);
29
+ // if there are queued messages, process them now
30
+ const messages = this.messageQueue;
31
+ this.messageQueue = [];
32
+ for (const message of messages) {
33
+ cb(message);
34
+ }
35
+ }
36
+ set onclose(cb) {
37
+ this.closeHandlers.push(cb);
38
+ }
39
+ /**
40
+ * Send binary data over the WebSocket connection;
41
+ * @param message - The message that will be sent.
42
+ */
43
+ send(message) {
44
+ this.ws.send(message);
45
+ }
46
+ /**
47
+ * Closes the WebSocket.
48
+ * @param code - The close code. Must be one of:
49
+ * - 1000: normal closure
50
+ * - 1009: message too big
51
+ * - 1011: server encountered error
52
+ * - 1012: server restarting
53
+ * - 1013: server too busy or rate-limiting
54
+ * - 4000-4999: reserved for applications
55
+ * @param reason - The reason the WebSocket was closed.
56
+ */
57
+ close(code, reason) {
58
+ this.ws.close(code, reason);
59
+ }
60
+ }
61
+ export class WsEndpoint {
62
+ }
63
+ export class Ws extends WsEndpoint {
64
+ constructor(options) {
65
+ super();
66
+ this.options = options;
67
+ this.paramsSchema = object(options.params);
68
+ this.querySchema = object(options.query);
69
+ this.headersSchema = laxObject(options.headers);
70
+ }
71
+ async handle(url, params, headers, ws) {
72
+ let parsedParams;
73
+ let parsedQuery;
74
+ let parsedHeaders;
75
+ try {
76
+ parsedParams = this.paramsSchema.parse(params);
77
+ parsedQuery = this.querySchema.parse(Object.fromEntries(url.searchParams));
78
+ parsedHeaders = this.headersSchema.parse(headers);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof ValidationError) {
82
+ throw new BadRequestError(error.format());
83
+ }
84
+ throw error;
85
+ }
86
+ await this.options.do(new YedraWebSocket(ws), {
87
+ url: url.pathname,
88
+ params: parsedParams,
89
+ query: parsedQuery,
90
+ headers: parsedHeaders,
91
+ });
92
+ return undefined;
93
+ }
94
+ documentation() {
95
+ const parameters = [
96
+ ...paramDocs(this.options.params, 'path', []),
97
+ ...paramDocs(this.options.query, 'query', []),
98
+ ...paramDocs(this.options.headers, 'header', []),
99
+ ];
100
+ return {
101
+ tags: [this.options.category],
102
+ summary: this.options.summary,
103
+ description: this.options.description,
104
+ parameters,
105
+ responses: {
106
+ '101': {
107
+ description: 'Switching to WebSocket',
108
+ },
109
+ '400': {
110
+ description: 'Upgrading to WebSocket failed',
111
+ content: {
112
+ 'application/json': {
113
+ schema: {
114
+ type: 'object',
115
+ properties: {
116
+ status: {
117
+ type: 'number',
118
+ example: 400,
119
+ },
120
+ errorMessage: {
121
+ type: 'string',
122
+ example: 'Upgrading to WebSocket failed.',
123
+ },
124
+ },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ };
131
+ }
132
+ }
@@ -0,0 +1,17 @@
1
+ export { BodyType, type Typeof } from './validation/body.js';
2
+ export { boolean } from './validation/boolean.js';
3
+ export { date } from './validation/date.js';
4
+ export { _enum as enum } from './validation/enum.js';
5
+ export { ValidationError } from './validation/error.js';
6
+ export { integer } from './validation/integer.js';
7
+ export { lazy, LazySchema } from './validation/lazy.js';
8
+ export { array } from './validation/modifiable.js';
9
+ export { _null as null } from './validation/null.js';
10
+ export { number } from './validation/number.js';
11
+ export { laxObject, object } from './validation/object.js';
12
+ export { record } from './validation/record.js';
13
+ export { Schema } from './validation/schema.js';
14
+ export { string } from './validation/string.js';
15
+ export { union } from './validation/union.js';
16
+ export { unknown } from './validation/unknown.js';
17
+ export { uuid } from './validation/uuid.js';