yedra 0.15.6 → 0.16.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,6 +1,6 @@
1
1
  import * as y from './lib.js';
2
2
  export { y };
3
3
  export { Yedra } from './routing/app.js';
4
- export { Get, Post, Put, Delete } from './routing/rest.js';
4
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
5
+ export { Delete, Get, Post, Put } from './routing/rest.js';
5
6
  export { Ws } from './routing/websocket.js';
6
- export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as y from './lib.js';
2
2
  export { y };
3
3
  export { Yedra } from './routing/app.js';
4
- export { Get, Post, Put, Delete } from './routing/rest.js';
4
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
5
+ export { Delete, Get, Post, Put } from './routing/rest.js';
5
6
  export { Ws } from './routing/websocket.js';
6
- export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
package/dist/lib.d.ts CHANGED
@@ -1,22 +1,23 @@
1
- export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
1
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
2
2
  export { Log } from './routing/log.js';
3
3
  export declare const validatePath: (path: string) => void;
4
4
  export { parseEnv } from './routing/env.js';
5
- export { array } from './validation/array.js';
5
+ export { SecurityScheme } from './util/security.js';
6
+ export { BodyType, type Typeof } from './validation/body.js';
6
7
  export { boolean } from './validation/boolean.js';
7
8
  export { date } from './validation/date.js';
9
+ export { either } from './validation/either.js';
8
10
  export { _enum as enum } from './validation/enum.js';
9
- export { _null as null } from './validation/null.js';
10
11
  export { ValidationError } from './validation/error.js';
11
- export { number } from './validation/number.js';
12
12
  export { integer } from './validation/integer.js';
13
- export { object, laxObject } from './validation/object.js';
13
+ export { array } from './validation/modifiable.js';
14
+ export { _null as null } from './validation/null.js';
15
+ export { number } from './validation/number.js';
16
+ export { laxObject, object } from './validation/object.js';
17
+ export { raw } from './validation/raw.js';
14
18
  export { record } from './validation/record.js';
15
19
  export { Schema } from './validation/schema.js';
16
- export { BodyType, type Typeof } from './validation/body.js';
17
- export { raw } from './validation/raw.js';
18
20
  export { stream } from './validation/stream.js';
19
- export { either } from './validation/either.js';
20
21
  export { string } from './validation/string.js';
21
22
  export { union } from './validation/union.js';
22
23
  export { unknown } from './validation/unknown.js';
package/dist/lib.js CHANGED
@@ -1,27 +1,28 @@
1
1
  import { Path } from './routing/path.js';
2
2
  // routing
3
- export { HttpError, BadRequestError, UnauthorizedError, PaymentRequiredError, ForbiddenError, NotFoundError, ConflictError, } from './routing/errors.js';
3
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PaymentRequiredError, UnauthorizedError, } from './routing/errors.js';
4
4
  export { Log } from './routing/log.js';
5
5
  export const validatePath = (path) => {
6
6
  new Path(path);
7
7
  };
8
8
  export { parseEnv } from './routing/env.js';
9
- // validation
10
- export { array } from './validation/array.js';
9
+ export { SecurityScheme } from './util/security.js';
10
+ export { BodyType } from './validation/body.js';
11
11
  export { boolean } from './validation/boolean.js';
12
12
  export { date } from './validation/date.js';
13
+ export { either } from './validation/either.js';
13
14
  export { _enum as enum } from './validation/enum.js';
14
- export { _null as null } from './validation/null.js';
15
15
  export { ValidationError } from './validation/error.js';
16
- export { number } from './validation/number.js';
17
16
  export { integer } from './validation/integer.js';
18
- export { object, laxObject } from './validation/object.js';
17
+ // validation
18
+ export { array } from './validation/modifiable.js';
19
+ export { _null as null } from './validation/null.js';
20
+ export { number } from './validation/number.js';
21
+ export { laxObject, object } from './validation/object.js';
22
+ export { raw } from './validation/raw.js';
19
23
  export { record } from './validation/record.js';
20
24
  export { Schema } from './validation/schema.js';
21
- export { BodyType } from './validation/body.js';
22
- export { raw } from './validation/raw.js';
23
25
  export { stream } from './validation/stream.js';
24
- export { either } from './validation/either.js';
25
26
  export { string } from './validation/string.js';
26
27
  export { union } from './validation/union.js';
27
28
  export { unknown } from './validation/unknown.js';
@@ -1,7 +1,6 @@
1
1
  import type { IncomingMessage, Server, ServerResponse } from 'node:http';
2
2
  import { WebSocketServer } from 'ws';
3
3
  import { Counter } from '../util/counter.js';
4
- import type { SecurityScheme } from '../util/security.js';
5
4
  import { RestEndpoint } from './rest.js';
6
5
  import { WsEndpoint } from './websocket.js';
7
6
  declare class Context {
@@ -23,27 +22,35 @@ type ServeConfig = {
23
22
  dir: string;
24
23
  fallback?: string | ServeFallback;
25
24
  };
25
+ type DocsData = {
26
+ /**
27
+ * The title of your API.
28
+ */
29
+ title: string;
30
+ /**
31
+ * The description of your API.
32
+ */
33
+ description: string;
34
+ /**
35
+ * The current version of your API.
36
+ */
37
+ version: string;
38
+ /**
39
+ * The list of servers your API is reachable under.
40
+ */
41
+ servers?: {
42
+ description: string;
43
+ url: string;
44
+ }[];
45
+ };
26
46
  type ConnectMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
27
47
  export declare class Yedra {
28
48
  private restRoutes;
29
49
  private wsRoutes;
30
50
  private requestData;
51
+ private generatedDocs;
31
52
  use(path: string, endpoint: RestEndpoint | WsEndpoint | Yedra): Yedra;
32
- /**
33
- * Generate OpenAPI documentation for the app.
34
- */
35
- docs(options: {
36
- info: {
37
- title: string;
38
- description: string;
39
- version: string;
40
- };
41
- security?: Record<string, SecurityScheme>;
42
- servers: {
43
- description: string;
44
- url: string;
45
- }[];
46
- }): object;
53
+ private generateDocs;
47
54
  private static loadServe;
48
55
  private performRequest;
49
56
  private middlewareNext;
@@ -57,6 +64,11 @@ export declare class Yedra {
57
64
  path: string;
58
65
  get?: () => Promise<string> | string;
59
66
  };
67
+ /**
68
+ * Configuration for the `/openapi.json` endpoint, which generates
69
+ * OpenAPI documentation.
70
+ */
71
+ docs?: DocsData;
60
72
  serve?: ServeConfig;
61
73
  /**
62
74
  * Prevents all normal output from Yedra. Mostly useful for tests.
@@ -1,4 +1,4 @@
1
- import { readFile, readdir, stat } from 'node:fs/promises';
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import { createServer as createHttpServer } from 'node:http';
3
3
  import { createServer as createHttpsServer } from 'node:https';
4
4
  import { extname, join } from 'node:path';
@@ -43,7 +43,10 @@ export class Yedra {
43
43
  if (endpoint instanceof Yedra) {
44
44
  for (const route of endpoint.restRoutes) {
45
45
  const newPath = route.path.withPrefix(path);
46
- this.restRoutes.push({ path: newPath, endpoint: route.endpoint });
46
+ this.restRoutes.push({
47
+ path: newPath,
48
+ endpoint: route.endpoint,
49
+ });
47
50
  }
48
51
  for (const route of endpoint.wsRoutes) {
49
52
  const newPath = route.path.withPrefix(path);
@@ -61,10 +64,9 @@ export class Yedra {
61
64
  }
62
65
  return this;
63
66
  }
64
- /**
65
- * Generate OpenAPI documentation for the app.
66
- */
67
- docs(options) {
67
+ generateDocs(options) {
68
+ // this set will be filled with the security schemes from all endpoints
69
+ const securitySchemes = new Set();
68
70
  const paths = {};
69
71
  for (const route of this.restRoutes) {
70
72
  if (route.endpoint.isHidden()) {
@@ -74,18 +76,25 @@ export class Yedra {
74
76
  const path = route.path.toString();
75
77
  const methods = paths[path] ?? {};
76
78
  methods[route.endpoint.method.toLowerCase()] =
77
- route.endpoint.documentation(path, options.security ?? {});
79
+ route.endpoint.documentation(path, securitySchemes);
78
80
  paths[path] = methods;
79
81
  }
80
- return {
82
+ this.generatedDocs = JSON.stringify({
81
83
  openapi: '3.0.2',
82
- info: options.info,
84
+ info: {
85
+ title: options?.title ?? 'Yedra API',
86
+ description: options?.description ??
87
+ 'This is an OpenAPI documentation generated automatically by Yedra.',
88
+ version: options?.version ?? '0.1.0',
89
+ },
83
90
  components: {
84
- securitySchemes: options.security,
91
+ securitySchemes: Object.fromEntries(securitySchemes
92
+ .values()
93
+ .map((scheme) => [scheme.name, scheme.scheme])),
85
94
  },
86
- servers: options.servers,
95
+ servers: options?.servers ?? [],
87
96
  paths,
88
- };
97
+ });
89
98
  }
90
99
  static async loadServe(config) {
91
100
  if (config === undefined) {
@@ -137,6 +146,19 @@ export class Yedra {
137
146
  req.method !== 'DELETE') {
138
147
  return Yedra.errorResponse(405, `Method \`${req.method}\` not allowed.`);
139
148
  }
149
+ if (req.method === 'GET' && req.url.pathname === '/openapi.json') {
150
+ if (this.generatedDocs === undefined) {
151
+ console.error('Docs were not generated correctly.');
152
+ return Yedra.errorResponse(500, 'Internal Server Error');
153
+ }
154
+ return {
155
+ status: 200,
156
+ body: Buffer.from(this.generatedDocs ?? '{}', 'utf-8'),
157
+ headers: {
158
+ 'content-type': 'application/json',
159
+ },
160
+ };
161
+ }
140
162
  const match = this.matchRestRoute(req.url.pathname, req.method);
141
163
  if (!match.result) {
142
164
  // no matching route found
@@ -155,7 +177,9 @@ export class Yedra {
155
177
  }
156
178
  if (serveData.fallback !== undefined) {
157
179
  try {
158
- const response = await serveData.fallback({ href: req.url.href });
180
+ const response = await serveData.fallback({
181
+ href: req.url.href,
182
+ });
159
183
  return {
160
184
  status: response.status ?? 200,
161
185
  body: isUint8Array(response.body)
@@ -211,6 +235,7 @@ export class Yedra {
211
235
  }
212
236
  async listen(port, options) {
213
237
  const serveData = await Yedra.loadServe(options?.serve);
238
+ this.generateDocs(options?.docs);
214
239
  const server = options?.tls === undefined
215
240
  ? createHttpServer()
216
241
  : createHttpsServer({
@@ -320,7 +345,7 @@ export class Yedra {
320
345
  }
321
346
  matchRestRoute(url, method) {
322
347
  let invalidMethod = false;
323
- let result = undefined;
348
+ let result;
324
349
  for (const route of this.restRoutes) {
325
350
  const match = route.path.match(url);
326
351
  if (match === undefined) {
@@ -340,7 +365,7 @@ export class Yedra {
340
365
  return { invalidMethod, result };
341
366
  }
342
367
  matchWsRoute(url) {
343
- let result = undefined;
368
+ let result;
344
369
  for (const route of this.wsRoutes) {
345
370
  const match = route.path.match(url);
346
371
  if (match === undefined) {
@@ -19,7 +19,7 @@ export class Path {
19
19
  .substring(1)
20
20
  .split('/')
21
21
  .filter((segment) => segment !== '');
22
- const invalidSegment = this.expected.find((part) => part.match(/^((:?[A-Za-z0-9\-\.]+\??)|\*)$/) === null);
22
+ const invalidSegment = this.expected.find((part) => part.match(/^((:?[A-Za-z0-9\-.]+\??)|\*)$/) === null);
23
23
  if (invalidSegment) {
24
24
  throw new Error(`API path ${path} is invalid: Segment ${invalidSegment} does not match regex /^((:?[A-Za-z0-9-\.]+\\??)|\\*)$/.`);
25
25
  }
@@ -25,10 +25,9 @@ type EndpointOptions<Params extends Record<string, Schema<unknown>>, Query exten
25
25
  summary: string;
26
26
  description?: string;
27
27
  /**
28
- * List of security definitions that apply to this endpoint.
29
- * Security definitions need to be included in the `app.docs()` call.
28
+ * List of security schemes that apply to this endpoint.
30
29
  */
31
- security?: string[];
30
+ security?: SecurityScheme[];
32
31
  /**
33
32
  * Whether this endpoint should be excluded from the documentation.
34
33
  * Default is false.
@@ -55,8 +54,13 @@ export declare abstract class RestEndpoint {
55
54
  headers?: Record<string, string>;
56
55
  }>;
57
56
  abstract isHidden(): boolean;
58
- abstract documentation(path: string, securitySchemes: Record<string, SecurityScheme>): object;
57
+ abstract documentation(path: string, securitySchemes: Set<SecurityScheme>): object;
59
58
  }
59
+ /**
60
+ * This class implements all REST endpoints in yedra. Its parent class,
61
+ * `RestEndpoint`, is not abstract because there are multiple implementations,
62
+ * but so that we can hide all the generic parameters of this class.
63
+ */
60
64
  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 {
61
65
  private _method;
62
66
  private options;
@@ -77,7 +81,7 @@ declare class ConcreteRestEndpoint<Params extends Record<string, Schema<unknown>
77
81
  headers?: Record<string, string>;
78
82
  }>;
79
83
  isHidden(): boolean;
80
- documentation(path: string, securitySchemes: Record<string, SecurityScheme>): object;
84
+ documentation(path: string, securitySchemes: Set<SecurityScheme>): object;
81
85
  }
82
86
  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> {
83
87
  constructor(options: Omit<EndpointOptions<Params, Query, Headers, NoneBody, Res>, 'req'>);
@@ -5,6 +5,11 @@ import { laxObject, object } from '../validation/object.js';
5
5
  import { BadRequestError } from './errors.js';
6
6
  export class RestEndpoint {
7
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
+ */
8
13
  class ConcreteRestEndpoint extends RestEndpoint {
9
14
  constructor(method, options) {
10
15
  super();
@@ -92,19 +97,22 @@ class ConcreteRestEndpoint extends RestEndpoint {
92
97
  return this.options.hidden ?? false;
93
98
  }
94
99
  documentation(path, securitySchemes) {
100
+ const security = this.options.security ?? [];
101
+ for (const scheme of security) {
102
+ // add all our security schemes to the global list of schemes
103
+ securitySchemes.add(scheme);
104
+ }
95
105
  const parameters = [
96
- ...paramDocs(this.options.params, 'path', this.options.security ?? [], securitySchemes),
97
- ...paramDocs(this.options.query, 'query', this.options.security ?? [], securitySchemes),
98
- ...paramDocs(this.options.headers, 'header', this.options.security ?? [], securitySchemes),
106
+ ...paramDocs(this.options.params, 'path', security),
107
+ ...paramDocs(this.options.query, 'query', security),
108
+ ...paramDocs(this.options.headers, 'header', security),
99
109
  ];
100
110
  return {
101
111
  tags: [this.options.category],
102
112
  summary: this.options.summary,
103
113
  description: this.options.description,
104
114
  operationId: `${path.substring(1).replaceAll('/', '_')}_${this.method.toLowerCase()}`,
105
- security: this.options.security !== undefined
106
- ? this.options.security.map((security) => ({ [security]: [] }))
107
- : [],
115
+ security: security.map((security) => ({ [security.name]: [] })),
108
116
  parameters,
109
117
  requestBody: this.options.req instanceof NoneBody
110
118
  ? undefined
@@ -89,8 +89,8 @@ export class Ws extends WsEndpoint {
89
89
  }
90
90
  documentation() {
91
91
  const parameters = [
92
- ...paramDocs(this.options.params, 'path', [], {}),
93
- ...paramDocs(this.options.query, 'query', [], {}),
92
+ ...paramDocs(this.options.params, 'path', []),
93
+ ...paramDocs(this.options.query, 'query', []),
94
94
  ];
95
95
  return {
96
96
  tags: [this.options.category],
@@ -6,4 +6,4 @@ import type { SecurityScheme } from './security.js';
6
6
  * @param position - The position of the parameter, e.g. `path`, `query`, `header`. This is passed directly to OpenAPI.
7
7
  * @returns A list of parameter documentations in OpenAPI format.
8
8
  */
9
- export declare const paramDocs: <Params extends Record<string, Schema<unknown>>>(params: Params, position: "path" | "query" | "header", security: string[], securitySchemes: Record<string, SecurityScheme>) => object[];
9
+ export declare const paramDocs: <Params extends Record<string, Schema<unknown>>>(params: Params, position: "path" | "query" | "header", security: SecurityScheme[]) => object[];
package/dist/util/docs.js CHANGED
@@ -4,10 +4,10 @@
4
4
  * @param position - The position of the parameter, e.g. `path`, `query`, `header`. This is passed directly to OpenAPI.
5
5
  * @returns A list of parameter documentations in OpenAPI format.
6
6
  */
7
- export const paramDocs = (params, position, security, securitySchemes) => {
7
+ export const paramDocs = (params, position, security) => {
8
8
  const result = [];
9
9
  for (const name in params) {
10
- if (isAuthToken(name, position, security, securitySchemes)) {
10
+ if (isAuthToken(name, position, security)) {
11
11
  // don't include auth tokens in the documentation, they're already
12
12
  // handled by the separate authentication feature of OpenAPI
13
13
  continue;
@@ -32,16 +32,13 @@ export const paramDocs = (params, position, security, securitySchemes) => {
32
32
  * @param securitySchemes - The security scheme definitions.
33
33
  * @returns Whether the parameter is an auth token.
34
34
  */
35
- const isAuthToken = (paramName, position, security, securitySchemes) => {
35
+ const isAuthToken = (paramName, position, securitySchemes) => {
36
36
  if (position === 'path') {
37
- // parameters cannot be auth tokens
37
+ // path parameters cannot be auth tokens
38
38
  return false;
39
39
  }
40
- for (const [securityName, securityScheme] of Object.entries(securitySchemes)) {
41
- if (!security.includes(securityName)) {
42
- continue;
43
- }
44
- if (securityScheme.type === 'http') {
40
+ for (const scheme of securitySchemes) {
41
+ if (scheme.scheme.type === 'http') {
45
42
  if (paramName.toLowerCase() === 'authorization' &&
46
43
  position === 'header') {
47
44
  // http auth token has to be in authorization header
@@ -49,8 +46,9 @@ const isAuthToken = (paramName, position, security, securitySchemes) => {
49
46
  }
50
47
  }
51
48
  else {
52
- if (securityScheme.in === position && securityScheme.name === paramName) {
53
- // api key has to have the correct name and be in the correct position
49
+ if (scheme.scheme.in === position &&
50
+ paramName.toLowerCase() === scheme.scheme.name.toLowerCase()) {
51
+ // API key has to have the correct name and be in the correct position
54
52
  return true;
55
53
  }
56
54
  }
@@ -1,4 +1,9 @@
1
- export type SecurityScheme = {
1
+ export declare class SecurityScheme {
2
+ name: string;
3
+ scheme: SecuritySchemeData;
4
+ constructor(name: string, scheme: SecuritySchemeData);
5
+ }
6
+ type SecuritySchemeData = {
2
7
  type: 'http';
3
8
  scheme: 'basic' | 'bearer';
4
9
  } | {
@@ -6,3 +11,4 @@ export type SecurityScheme = {
6
11
  in: 'header' | 'query' | 'cookie';
7
12
  name: string;
8
13
  };
14
+ export {};
@@ -1 +1,6 @@
1
- export {};
1
+ export class SecurityScheme {
2
+ constructor(name, scheme) {
3
+ this.name = name;
4
+ this.scheme = scheme;
5
+ }
6
+ }
@@ -1,4 +1,4 @@
1
- import type { ArraySchema } from './array.js';
1
+ import type { Typeof } from './body.js';
2
2
  import { DocSchema } from './doc.js';
3
3
  import { Schema } from './schema.js';
4
4
  export declare abstract class ModifiableSchema<T> extends Schema<T> {
@@ -27,4 +27,34 @@ declare class OptionalSchema<T> extends Schema<T | undefined> {
27
27
  documentation(): object;
28
28
  isOptional(): boolean;
29
29
  }
30
+ export declare class ArraySchema<ItemSchema extends Schema<unknown>> extends ModifiableSchema<Typeof<ItemSchema>[]> {
31
+ private readonly itemSchema;
32
+ private readonly minItems?;
33
+ private readonly maxItems?;
34
+ constructor(itemSchema: ItemSchema, minItems?: number, maxItems?: number);
35
+ /**
36
+ * Set the minimum number of items for arrays.
37
+ * @param items - The minimum number of items.
38
+ */
39
+ min(items: number): ArraySchema<ItemSchema>;
40
+ /**
41
+ * Set the maximum number of items for arrays.
42
+ * @param items - The maximum number of items.
43
+ */
44
+ max(items: number): ArraySchema<ItemSchema>;
45
+ /**
46
+ * Set the exact number of items for arrays.
47
+ * This is equivalent to calling both min and max.
48
+ * @param items - The number of items.
49
+ */
50
+ length(items: number): ArraySchema<ItemSchema>;
51
+ parse(obj: unknown): Typeof<ItemSchema>[];
52
+ documentation(): object;
53
+ }
54
+ /**
55
+ * A schema matching arrays of the provided item type.
56
+ * @param itemSchema - The schema for array items.
57
+ * @deprecated Use the .array() method instead.
58
+ */
59
+ export declare const array: <ItemSchema extends Schema<unknown>>(itemSchema: ItemSchema) => ArraySchema<ItemSchema>;
30
60
  export {};
@@ -1,4 +1,5 @@
1
1
  import { DocSchema } from './doc.js';
2
+ import { Issue, ValidationError } from './error.js';
2
3
  import { Schema } from './schema.js';
3
4
  export class ModifiableSchema extends Schema {
4
5
  /**
@@ -11,7 +12,7 @@ export class ModifiableSchema extends Schema {
11
12
  return new DocSchema(this, description, example);
12
13
  }
13
14
  array() {
14
- return new ArraySchemaModule.ArraySchema(this);
15
+ return new ArraySchema(this);
15
16
  }
16
17
  }
17
18
  class OptionalSchema extends Schema {
@@ -31,7 +32,7 @@ class OptionalSchema extends Schema {
31
32
  return new DocSchema(this, description, example);
32
33
  }
33
34
  array() {
34
- return new ArraySchemaModule.ArraySchema(this);
35
+ return new ArraySchema(this);
35
36
  }
36
37
  parse(obj) {
37
38
  if (obj === undefined || obj === null) {
@@ -46,4 +47,83 @@ class OptionalSchema extends Schema {
46
47
  return true;
47
48
  }
48
49
  }
49
- const ArraySchemaModule = await import('./array.js');
50
+ export class ArraySchema extends ModifiableSchema {
51
+ constructor(itemSchema, minItems, maxItems) {
52
+ super();
53
+ this.itemSchema = itemSchema;
54
+ this.minItems = minItems;
55
+ this.maxItems = maxItems;
56
+ }
57
+ /**
58
+ * Set the minimum number of items for arrays.
59
+ * @param items - The minimum number of items.
60
+ */
61
+ min(items) {
62
+ return new ArraySchema(this.itemSchema, items, this.maxItems);
63
+ }
64
+ /**
65
+ * Set the maximum number of items for arrays.
66
+ * @param items - The maximum number of items.
67
+ */
68
+ max(items) {
69
+ return new ArraySchema(this.itemSchema, this.minItems, items);
70
+ }
71
+ /**
72
+ * Set the exact number of items for arrays.
73
+ * This is equivalent to calling both min and max.
74
+ * @param items - The number of items.
75
+ */
76
+ length(items) {
77
+ return new ArraySchema(this.itemSchema, items, items);
78
+ }
79
+ parse(obj) {
80
+ if (!Array.isArray(obj)) {
81
+ throw new ValidationError([
82
+ new Issue([], `Expected array but got ${typeof obj}`),
83
+ ]);
84
+ }
85
+ if (this.minItems && obj.length < this.minItems) {
86
+ throw new ValidationError([
87
+ new Issue([], `Must have at least ${this.minItems} items, but has ${obj.length}`),
88
+ ]);
89
+ }
90
+ if (this.maxItems && obj.length > this.maxItems) {
91
+ throw new ValidationError([
92
+ new Issue([], `Must have at most ${this.maxItems} items, but has ${obj.length}`),
93
+ ]);
94
+ }
95
+ const elems = [];
96
+ const issues = [];
97
+ for (let i = 0; i < obj.length; ++i) {
98
+ try {
99
+ elems.push(this.itemSchema.parse(obj[i]));
100
+ }
101
+ catch (error) {
102
+ if (error instanceof ValidationError) {
103
+ issues.push(...error.withPrefix(i.toString()));
104
+ }
105
+ else {
106
+ throw error;
107
+ }
108
+ }
109
+ }
110
+ if (issues.length > 0) {
111
+ throw new ValidationError(issues);
112
+ }
113
+ return elems;
114
+ }
115
+ documentation() {
116
+ return {
117
+ type: 'array',
118
+ items: this.itemSchema.documentation(),
119
+ minItems: this.minItems,
120
+ maxItems: this.maxItems,
121
+ };
122
+ }
123
+ }
124
+ /**
125
+ * A schema matching arrays of the provided item type.
126
+ * @param itemSchema - The schema for array items.
127
+ * @deprecated Use the .array() method instead.
128
+ */
129
+ export const array = (itemSchema) => new ArraySchema(itemSchema);
package/package.json CHANGED
@@ -1,30 +1,41 @@
1
1
  {
2
- "name": "yedra",
3
- "version": "0.15.6",
4
- "repository": "github:0codekit/yedra",
5
- "main": "dist/index.js",
6
- "devDependencies": {
7
- "@biomejs/biome": "^1.9.4",
8
- "@types/bun": "^1.2.16",
9
- "@types/node": "^24.0.3",
10
- "@types/uuid": "^10.0.0",
11
- "@types/ws": "^8.18.1",
12
- "typescript": "^5.8.3"
13
- },
14
- "bugs": "https://github.com/0codekit/yedra/issues",
15
- "contributors": ["Justus Zorn <jzorn@wemakefuture.com>"],
16
- "description": "A TypeScript web framework with OpenAPI generation.",
17
- "keywords": ["typescript", "web", "http", "schema", "validation", "openapi"],
18
- "license": "MIT",
19
- "files": ["./dist/**"],
20
- "scripts": {
21
- "check": "biome check src/*"
22
- },
23
- "types": "dist/index.d.ts",
24
- "type": "module",
25
- "dependencies": {
26
- "mime": "^4.0.7",
27
- "uuid": "^11.1.0",
28
- "ws": "^8.18.2"
29
- }
2
+ "name": "yedra",
3
+ "version": "0.16.0",
4
+ "repository": "github:0codekit/yedra",
5
+ "main": "dist/index.js",
6
+ "devDependencies": {
7
+ "@biomejs/biome": "^2.0.6",
8
+ "@types/bun": "^1.2.17",
9
+ "@types/node": "^24.0.7",
10
+ "@types/uuid": "^10.0.0",
11
+ "@types/ws": "^8.18.1",
12
+ "typescript": "^5.8.3"
13
+ },
14
+ "bugs": "https://github.com/0codekit/yedra/issues",
15
+ "contributors": [
16
+ "Justus Zorn <jzorn@wemakefuture.com>"
17
+ ],
18
+ "description": "A TypeScript web framework with OpenAPI generation.",
19
+ "keywords": [
20
+ "typescript",
21
+ "web",
22
+ "http",
23
+ "schema",
24
+ "validation",
25
+ "openapi"
26
+ ],
27
+ "license": "MIT",
28
+ "files": [
29
+ "./dist/**"
30
+ ],
31
+ "scripts": {
32
+ "check": "biome check src/*"
33
+ },
34
+ "types": "dist/index.d.ts",
35
+ "type": "module",
36
+ "dependencies": {
37
+ "mime": "^4.0.7",
38
+ "uuid": "^11.1.0",
39
+ "ws": "^8.18.3"
40
+ }
30
41
  }
@@ -1,33 +0,0 @@
1
- import type { Typeof } from './body.js';
2
- import { ModifiableSchema } from './modifiable.js';
3
- import type { Schema } from './schema.js';
4
- export declare class ArraySchema<ItemSchema extends Schema<unknown>> extends ModifiableSchema<Typeof<ItemSchema>[]> {
5
- private readonly itemSchema;
6
- private readonly minItems?;
7
- private readonly maxItems?;
8
- constructor(itemSchema: ItemSchema, minItems?: number, maxItems?: number);
9
- /**
10
- * Set the minimum number of items for arrays.
11
- * @param items - The minimum number of items.
12
- */
13
- min(items: number): ArraySchema<ItemSchema>;
14
- /**
15
- * Set the maximum number of items for arrays.
16
- * @param items - The maximum number of items.
17
- */
18
- max(items: number): ArraySchema<ItemSchema>;
19
- /**
20
- * Set the exact number of items for arrays.
21
- * This is equivalent to calling both min and max.
22
- * @param items - The number of items.
23
- */
24
- length(items: number): ArraySchema<ItemSchema>;
25
- parse(obj: unknown): Typeof<ItemSchema>[];
26
- documentation(): object;
27
- }
28
- /**
29
- * A schema matching arrays of the provided item type.
30
- * @param itemSchema - The schema for array items.
31
- * @deprecated Use the .array() method instead.
32
- */
33
- export declare const array: <ItemSchema extends Schema<unknown>>(itemSchema: ItemSchema) => ArraySchema<ItemSchema>;
@@ -1,82 +0,0 @@
1
- import { Issue, ValidationError } from './error.js';
2
- import { ModifiableSchema } from './modifiable.js';
3
- export class ArraySchema extends ModifiableSchema {
4
- constructor(itemSchema, minItems, maxItems) {
5
- super();
6
- this.itemSchema = itemSchema;
7
- this.minItems = minItems;
8
- this.maxItems = maxItems;
9
- }
10
- /**
11
- * Set the minimum number of items for arrays.
12
- * @param items - The minimum number of items.
13
- */
14
- min(items) {
15
- return new ArraySchema(this.itemSchema, items, this.maxItems);
16
- }
17
- /**
18
- * Set the maximum number of items for arrays.
19
- * @param items - The maximum number of items.
20
- */
21
- max(items) {
22
- return new ArraySchema(this.itemSchema, this.minItems, items);
23
- }
24
- /**
25
- * Set the exact number of items for arrays.
26
- * This is equivalent to calling both min and max.
27
- * @param items - The number of items.
28
- */
29
- length(items) {
30
- return new ArraySchema(this.itemSchema, items, items);
31
- }
32
- parse(obj) {
33
- if (!Array.isArray(obj)) {
34
- throw new ValidationError([
35
- new Issue([], `Expected array but got ${typeof obj}`),
36
- ]);
37
- }
38
- if (this.minItems && obj.length < this.minItems) {
39
- throw new ValidationError([
40
- new Issue([], `Must have at least ${this.minItems} items, but has ${obj.length}`),
41
- ]);
42
- }
43
- if (this.maxItems && obj.length > this.maxItems) {
44
- throw new ValidationError([
45
- new Issue([], `Must have at most ${this.maxItems} items, but has ${obj.length}`),
46
- ]);
47
- }
48
- const elems = [];
49
- const issues = [];
50
- for (let i = 0; i < obj.length; ++i) {
51
- try {
52
- elems.push(this.itemSchema.parse(obj[i]));
53
- }
54
- catch (error) {
55
- if (error instanceof ValidationError) {
56
- issues.push(...error.withPrefix(i.toString()));
57
- }
58
- else {
59
- throw error;
60
- }
61
- }
62
- }
63
- if (issues.length > 0) {
64
- throw new ValidationError(issues);
65
- }
66
- return elems;
67
- }
68
- documentation() {
69
- return {
70
- type: 'array',
71
- items: this.itemSchema.documentation(),
72
- minItems: this.minItems,
73
- maxItems: this.maxItems,
74
- };
75
- }
76
- }
77
- /**
78
- * A schema matching arrays of the provided item type.
79
- * @param itemSchema - The schema for array items.
80
- * @deprecated Use the .array() method instead.
81
- */
82
- export const array = (itemSchema) => new ArraySchema(itemSchema);