ts-openapi-express 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jsonPointer.d.ts +8 -0
- package/dist/jsonPointer.js +39 -0
- package/dist/middleware/json.d.ts +5 -0
- package/dist/middleware/json.js +28 -0
- package/dist/middleware/validation.d.ts +11 -0
- package/dist/middleware/validation.js +40 -0
- package/dist/openapi-types/http.d.ts +77 -0
- package/dist/openapi-types/http.js +1 -0
- package/dist/openapi-types/index.d.ts +2 -0
- package/dist/openapi-types/index.js +2 -0
- package/dist/openapi-types/types.d.ts +59 -0
- package/dist/openapi-types/types.js +1 -0
- package/dist/openapiExpress.d.ts +13 -0
- package/dist/openapiExpress.js +37 -0
- package/dist/openapiRoutes.d.ts +4 -0
- package/dist/openapiRoutes.js +91 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +16 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jacob Shirley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { error as openapiValidatorErrors } from 'express-openapi-validator';
|
|
2
|
+
export type OpenapiValidatorError = (typeof openapiValidatorErrors)[keyof typeof openapiValidatorErrors];
|
|
3
|
+
declare const isExpressOpenAPIValidatorError: (err: any) => err is Error & {
|
|
4
|
+
status: number;
|
|
5
|
+
};
|
|
6
|
+
declare class HttpError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
constructor(message: string, status: number);
|
|
9
|
+
type(): string;
|
|
10
|
+
}
|
|
11
|
+
declare class UnauthorizedError extends HttpError {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
declare class RequestBodyTooLargeError extends HttpError {
|
|
15
|
+
constructor(maxSizeBytes: number);
|
|
16
|
+
}
|
|
17
|
+
declare class InvalidJsonError extends HttpError {
|
|
18
|
+
readonly jsonString: string;
|
|
19
|
+
constructor(jsonString: string);
|
|
20
|
+
}
|
|
21
|
+
export { isExpressOpenAPIValidatorError, HttpError, RequestBodyTooLargeError, InvalidJsonError, UnauthorizedError, };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { error as openapiValidatorErrors } from 'express-openapi-validator';
|
|
2
|
+
const isExpressOpenAPIValidatorError = (err) => {
|
|
3
|
+
for (const key in openapiValidatorErrors) {
|
|
4
|
+
if (err instanceof
|
|
5
|
+
openapiValidatorErrors[key]) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
};
|
|
11
|
+
class HttpError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(message, status) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
type() {
|
|
18
|
+
return this.constructor.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class UnauthorizedError extends HttpError {
|
|
22
|
+
constructor() {
|
|
23
|
+
super('Unauthorized', 401);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class RequestBodyTooLargeError extends HttpError {
|
|
27
|
+
constructor(maxSizeBytes) {
|
|
28
|
+
super('Request body too large. Max size: ' + maxSizeBytes + ' bytes', 413);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class InvalidJsonError extends HttpError {
|
|
32
|
+
jsonString;
|
|
33
|
+
constructor(jsonString) {
|
|
34
|
+
super(`Invalid JSON: ${jsonString}`, 400);
|
|
35
|
+
this.jsonString = jsonString;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export { isExpressOpenAPIValidatorError, HttpError, RequestBodyTooLargeError, InvalidJsonError, UnauthorizedError, };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare function resolveLocal<T>(object: object, ref: string): T;
|
|
2
|
+
declare function resolvePointer<T>(object: T, options?: {
|
|
3
|
+
resolveLocalRefs?: boolean;
|
|
4
|
+
}, parent?: any, currentPath?: string, element?: string): T;
|
|
5
|
+
declare function resolveFile<T = any>(path: string, options?: {
|
|
6
|
+
resolveLocalRefs?: boolean;
|
|
7
|
+
}, element?: string): T;
|
|
8
|
+
export { resolveLocal, resolveFile, resolvePointer };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseDocument } from 'yaml';
|
|
2
|
+
import { get } from 'lodash-es';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
function resolveLocal(object, ref) {
|
|
6
|
+
const [p, element] = ref.split('#');
|
|
7
|
+
return get(object, element.split('/').filter(Boolean));
|
|
8
|
+
}
|
|
9
|
+
function resolvePointer(object, options, parent = object, currentPath, element) {
|
|
10
|
+
const resolveLocal = options?.resolveLocalRefs ?? true;
|
|
11
|
+
const data = element
|
|
12
|
+
? get(object, element.split('/').filter(Boolean))
|
|
13
|
+
: object;
|
|
14
|
+
for (const key in data) {
|
|
15
|
+
const value = data[key];
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
data[key] = resolvePointer(value, options, parent, currentPath, '');
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (key !== '$ref')
|
|
21
|
+
continue;
|
|
22
|
+
const [p, element] = (value + '').split('#');
|
|
23
|
+
if (p === '' && !resolveLocal)
|
|
24
|
+
continue;
|
|
25
|
+
return !currentPath
|
|
26
|
+
? get(parent, element.split('/').filter(Boolean))
|
|
27
|
+
: resolveFile(path.join(p ? path.dirname(currentPath) : currentPath, p), options, element);
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
function resolveFile(path, options, element = '') {
|
|
32
|
+
const src = fs.readFileSync(path, 'utf-8');
|
|
33
|
+
const object = path.endsWith('.yaml')
|
|
34
|
+
? parseDocument(src).toJSON()
|
|
35
|
+
: JSON.parse(src);
|
|
36
|
+
const result = resolvePointer(object, options, object, path, element);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
export { resolveLocal, resolveFile, resolvePointer };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { InvalidJsonError, RequestBodyTooLargeError } from '../errors.js';
|
|
2
|
+
const DEFAULT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
function json(options) {
|
|
4
|
+
const limit = options?.limit ?? DEFAULT_LIMIT_BYTES;
|
|
5
|
+
return async function json(req, res, next) {
|
|
6
|
+
const contentType = req.headers['content-type'];
|
|
7
|
+
if (contentType?.trim()?.includes('application/json')) {
|
|
8
|
+
let body = '';
|
|
9
|
+
for await (const chunk of req) {
|
|
10
|
+
body += chunk.toString();
|
|
11
|
+
if (body.length > limit) {
|
|
12
|
+
next(new RequestBodyTooLargeError(limit));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
req.bodyAsString = body;
|
|
17
|
+
try {
|
|
18
|
+
req.body = JSON.parse(body);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
next(new InvalidJsonError(body));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export { json };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OpenapiSpec } from '../openapi-types/index.js';
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
declare function makeQueryWritable(req: Request, res: Response, next: NextFunction): void;
|
|
4
|
+
declare function requestResponseValidatorMiddleware(options: {
|
|
5
|
+
spec: OpenapiSpec;
|
|
6
|
+
validation?: {
|
|
7
|
+
requests?: boolean;
|
|
8
|
+
responses?: boolean;
|
|
9
|
+
} | false;
|
|
10
|
+
}): (import("express-openapi-validator/dist/framework/types.js").OpenApiRequestHandler[] | typeof makeQueryWritable)[];
|
|
11
|
+
export { requestResponseValidatorMiddleware };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { middleware as openapiExpressValidatorMiddleware } from 'express-openapi-validator';
|
|
2
|
+
// TODO: remove this when express-openapi-validator supports Express 5.x. See: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
3
|
+
// This is caused by a breaking change in Express 5.x, where query is immutable by default
|
|
4
|
+
function makeQueryWritable(req, res, next) {
|
|
5
|
+
if (req.query)
|
|
6
|
+
Object.defineProperty(req, 'query', {
|
|
7
|
+
writable: true,
|
|
8
|
+
value: { ...req.query },
|
|
9
|
+
});
|
|
10
|
+
next();
|
|
11
|
+
}
|
|
12
|
+
function makeQueryReadOnly(req, res, next) {
|
|
13
|
+
if (req.query)
|
|
14
|
+
Object.defineProperty(req, 'query', {
|
|
15
|
+
writable: false,
|
|
16
|
+
value: { ...req.query },
|
|
17
|
+
});
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
function requestResponseValidatorMiddleware(options) {
|
|
21
|
+
const { spec, validation } = options;
|
|
22
|
+
const middleware = openapiExpressValidatorMiddleware({
|
|
23
|
+
apiSpec: spec,
|
|
24
|
+
validateApiSpec: true,
|
|
25
|
+
ignoreUndocumented: true,
|
|
26
|
+
validateSecurity: false,
|
|
27
|
+
validateRequests: validation === false
|
|
28
|
+
? false
|
|
29
|
+
: validation?.requests === false
|
|
30
|
+
? false
|
|
31
|
+
: true,
|
|
32
|
+
validateResponses: validation === false
|
|
33
|
+
? false
|
|
34
|
+
: validation?.responses === false
|
|
35
|
+
? false
|
|
36
|
+
: true,
|
|
37
|
+
});
|
|
38
|
+
return [makeQueryWritable, middleware, makeQueryReadOnly];
|
|
39
|
+
}
|
|
40
|
+
export { requestResponseValidatorMiddleware };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
interface IncomingHttpHeaders {
|
|
2
|
+
accept?: string | undefined;
|
|
3
|
+
'accept-language'?: string | undefined;
|
|
4
|
+
'accept-patch'?: string | undefined;
|
|
5
|
+
'accept-ranges'?: string | undefined;
|
|
6
|
+
'access-control-allow-credentials'?: string | undefined;
|
|
7
|
+
'access-control-allow-headers'?: string | undefined;
|
|
8
|
+
'access-control-allow-methods'?: string | undefined;
|
|
9
|
+
'access-control-allow-origin'?: string | undefined;
|
|
10
|
+
'access-control-expose-headers'?: string | undefined;
|
|
11
|
+
'access-control-max-age'?: string | undefined;
|
|
12
|
+
'access-control-request-headers'?: string | undefined;
|
|
13
|
+
'access-control-request-method'?: string | undefined;
|
|
14
|
+
age?: string | undefined;
|
|
15
|
+
allow?: string | undefined;
|
|
16
|
+
'alt-svc'?: string | undefined;
|
|
17
|
+
authorization?: string | undefined;
|
|
18
|
+
'cache-control'?: string | undefined;
|
|
19
|
+
connection?: string | undefined;
|
|
20
|
+
'content-disposition'?: string | undefined;
|
|
21
|
+
'content-encoding'?: string | undefined;
|
|
22
|
+
'content-language'?: string | undefined;
|
|
23
|
+
'content-length'?: string | undefined;
|
|
24
|
+
'content-location'?: string | undefined;
|
|
25
|
+
'content-range'?: string | undefined;
|
|
26
|
+
'content-type'?: string | undefined;
|
|
27
|
+
cookie?: string | undefined;
|
|
28
|
+
date?: string | undefined;
|
|
29
|
+
etag?: string | undefined;
|
|
30
|
+
expect?: string | undefined;
|
|
31
|
+
expires?: string | undefined;
|
|
32
|
+
forwarded?: string | undefined;
|
|
33
|
+
from?: string | undefined;
|
|
34
|
+
host?: string | undefined;
|
|
35
|
+
'if-match'?: string | undefined;
|
|
36
|
+
'if-modified-since'?: string | undefined;
|
|
37
|
+
'if-none-match'?: string | undefined;
|
|
38
|
+
'if-unmodified-since'?: string | undefined;
|
|
39
|
+
'last-modified'?: string | undefined;
|
|
40
|
+
location?: string | undefined;
|
|
41
|
+
origin?: string | undefined;
|
|
42
|
+
pragma?: string | undefined;
|
|
43
|
+
'proxy-authenticate'?: string | undefined;
|
|
44
|
+
'proxy-authorization'?: string | undefined;
|
|
45
|
+
'public-key-pins'?: string | undefined;
|
|
46
|
+
range?: string | undefined;
|
|
47
|
+
referer?: string | undefined;
|
|
48
|
+
'retry-after'?: string | undefined;
|
|
49
|
+
'sec-websocket-accept'?: string | undefined;
|
|
50
|
+
'sec-websocket-extensions'?: string | undefined;
|
|
51
|
+
'sec-websocket-key'?: string | undefined;
|
|
52
|
+
'sec-websocket-protocol'?: string | undefined;
|
|
53
|
+
'sec-websocket-version'?: string | undefined;
|
|
54
|
+
'set-cookie'?: string[] | undefined;
|
|
55
|
+
'strict-transport-security'?: string | undefined;
|
|
56
|
+
tk?: string | undefined;
|
|
57
|
+
trailer?: string | undefined;
|
|
58
|
+
'transfer-encoding'?: string | undefined;
|
|
59
|
+
upgrade?: string | undefined;
|
|
60
|
+
'user-agent'?: string | undefined;
|
|
61
|
+
vary?: string | undefined;
|
|
62
|
+
via?: string | undefined;
|
|
63
|
+
warning?: string | undefined;
|
|
64
|
+
'www-authenticate'?: string | undefined;
|
|
65
|
+
}
|
|
66
|
+
type StandardRequestHeaders = {
|
|
67
|
+
[K in keyof IncomingHttpHeaders as string extends K ? never : number extends K ? never : K]: IncomingHttpHeaders[K];
|
|
68
|
+
};
|
|
69
|
+
type StandardRequestHeader = Lowercase<keyof StandardRequestHeaders>;
|
|
70
|
+
type StandardResponseHeader = Lowercase<'Accept-Ranges' | 'Age' | 'Allow' | 'Cache-Control' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-MD5' | 'Content-Range' | 'Content-Security-Policy' | 'Content-Type' | 'Date' | 'ETag' | 'Expires' | 'Last-Modified' | 'Link' | 'Location' | 'P3P' | 'Pragma' | 'Proxy-Authenticate' | 'Refresh' | 'Retry-After' | 'Server' | 'Set-Cookie' | 'Strict-Transport-Security' | 'Trailer' | 'Transfer-Encoding' | 'Upgrade' | 'User-Agent' | 'Vary' | 'Via' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-Frame-Options' | 'X-Powered-By' | 'X-XSS-Protection'>;
|
|
71
|
+
type StandardHeaderMap = {
|
|
72
|
+
[x in StandardResponseHeader]?: string;
|
|
73
|
+
};
|
|
74
|
+
type HeadersMap = {
|
|
75
|
+
[x in `x-${string}` | `X-${string}` | StandardRequestHeader]?: string;
|
|
76
|
+
};
|
|
77
|
+
export type { StandardRequestHeader, StandardResponseHeader, StandardHeaderMap, HeadersMap, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
type LowercaseKeys<T> = {
|
|
3
|
+
[K in keyof T as K extends string ? Lowercase<K> : K]: T[K];
|
|
4
|
+
};
|
|
5
|
+
type HttpMethod = 'post' | 'get' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'trace' | 'connect';
|
|
6
|
+
type OpenapiSpec = OpenAPIV3.Document;
|
|
7
|
+
type GetQueryParameters<T> = T extends {
|
|
8
|
+
parameters: {
|
|
9
|
+
query?: any;
|
|
10
|
+
};
|
|
11
|
+
} ? T['parameters']['query'] : never;
|
|
12
|
+
type GetPathParameters<T> = T extends {
|
|
13
|
+
parameters: {
|
|
14
|
+
path: any;
|
|
15
|
+
};
|
|
16
|
+
} ? T['parameters']['path'] : never;
|
|
17
|
+
type GetRequestHeaders<T> = T extends {
|
|
18
|
+
parameters: {
|
|
19
|
+
header?: any;
|
|
20
|
+
};
|
|
21
|
+
} ? [T['parameters']['header']] extends [never | undefined] ? never : LowercaseKeys<T['parameters']['header']> : never;
|
|
22
|
+
type GetRequestBody<T> = T extends {
|
|
23
|
+
requestBody?: {
|
|
24
|
+
content?: any;
|
|
25
|
+
};
|
|
26
|
+
} ? undefined extends T['requestBody'] ? NonNullable<T['requestBody']>['content'][keyof NonNullable<T['requestBody']>['content']] | undefined : NonNullable<T['requestBody']>['content'][keyof NonNullable<T['requestBody']>['content']] : never;
|
|
27
|
+
type GetStatusCodes<T> = T extends {
|
|
28
|
+
responses: {
|
|
29
|
+
[x: number]: any;
|
|
30
|
+
};
|
|
31
|
+
} ? Extract<keyof T['responses'], number> : never;
|
|
32
|
+
type GetResponseBody<T, Code extends number = number> = T extends {
|
|
33
|
+
responses: {
|
|
34
|
+
[x in Code]: {
|
|
35
|
+
content?: {
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
} ? T['responses'][Code]['content'][keyof T['responses'][Code]['content']] : never;
|
|
41
|
+
type GetJsonBody<T, Code extends number = number> = T extends {
|
|
42
|
+
responses: {
|
|
43
|
+
[x in Code]: {
|
|
44
|
+
content?: {
|
|
45
|
+
'application/json': any;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
} ? Extract<T['responses'][Code extends never ? Extract<keyof T['responses'], number> : Code]['content'], object>['application/json'] : never;
|
|
50
|
+
type GetResponseHeaders<T, Code extends number = GetStatusCodes<T>> = T extends {
|
|
51
|
+
responses: {
|
|
52
|
+
[code in Code]: {
|
|
53
|
+
headers: {
|
|
54
|
+
[headerName: string]: any;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
} ? T['responses'][Code]['headers'] : never;
|
|
59
|
+
export type { HttpMethod, OpenapiSpec, GetJsonBody, GetStatusCodes, GetPathParameters, GetRequestBody, GetQueryParameters, GetRequestHeaders, GetResponseHeaders, GetResponseBody, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import { ExpressMiddleware, OpenapiApplication, Routes } from './types.js';
|
|
3
|
+
declare function openapiExpress<Spec>(options: {
|
|
4
|
+
specPath: string;
|
|
5
|
+
routes: Routes<Spec>;
|
|
6
|
+
middleware?: ExpressMiddleware[];
|
|
7
|
+
validateRequest?: boolean;
|
|
8
|
+
validateResponse?: boolean;
|
|
9
|
+
decodeJsonBody?: boolean;
|
|
10
|
+
app?: Application;
|
|
11
|
+
disableXPoweredBy?: boolean;
|
|
12
|
+
}): OpenapiApplication<Spec>;
|
|
13
|
+
export { openapiExpress };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { json } from './middleware/json.js';
|
|
3
|
+
import { requestResponseValidatorMiddleware } from './middleware/validation.js';
|
|
4
|
+
import { resolveFile } from './jsonPointer.js';
|
|
5
|
+
import { openapiRoutes } from './openapiRoutes.js';
|
|
6
|
+
function openapiExpress(options) {
|
|
7
|
+
const { specPath, routes, decodeJsonBody = true, disableXPoweredBy = true, validateRequest = true, validateResponse = true, middleware = [], } = options;
|
|
8
|
+
const spec = resolveFile(specPath, { resolveLocalRefs: false });
|
|
9
|
+
const app = options.app || express();
|
|
10
|
+
if (disableXPoweredBy)
|
|
11
|
+
app.disable('x-powered-by');
|
|
12
|
+
app.use('/openapi.json', (req, res) => {
|
|
13
|
+
res.json(spec);
|
|
14
|
+
});
|
|
15
|
+
if (decodeJsonBody) {
|
|
16
|
+
app.use(json());
|
|
17
|
+
}
|
|
18
|
+
if (validateRequest || validateResponse)
|
|
19
|
+
app.use(...requestResponseValidatorMiddleware({
|
|
20
|
+
spec,
|
|
21
|
+
validation: {
|
|
22
|
+
requests: validateRequest,
|
|
23
|
+
responses: validateResponse,
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
for (const m of middleware) {
|
|
27
|
+
app.use(m);
|
|
28
|
+
}
|
|
29
|
+
openapiRoutes(routes, app);
|
|
30
|
+
return Object.assign(app, {
|
|
31
|
+
version: spec.info.version,
|
|
32
|
+
openApiPath: specPath,
|
|
33
|
+
spec,
|
|
34
|
+
routes: routes,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export { openapiExpress };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { openapiPathToExpressPath } from './utils.js';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
function assertHttpMethod(method) {
|
|
5
|
+
if (![
|
|
6
|
+
'get',
|
|
7
|
+
'post',
|
|
8
|
+
'put',
|
|
9
|
+
'patch',
|
|
10
|
+
'delete',
|
|
11
|
+
'trace',
|
|
12
|
+
'options',
|
|
13
|
+
'connect',
|
|
14
|
+
].includes(method)) {
|
|
15
|
+
throw new Error(`Invalid HTTP method: ${method}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function routeResponse(route) {
|
|
19
|
+
return async (request, res) => {
|
|
20
|
+
const result = await route.handler(request);
|
|
21
|
+
const responseCodes = Object.keys(result).map(Number);
|
|
22
|
+
if (responseCodes.length === 0) {
|
|
23
|
+
throw new Error('No response code defined');
|
|
24
|
+
}
|
|
25
|
+
if (responseCodes.length > 1) {
|
|
26
|
+
throw new Error('Multiple response codes defined');
|
|
27
|
+
}
|
|
28
|
+
const responseCode = responseCodes[0];
|
|
29
|
+
if (!result[responseCode]) {
|
|
30
|
+
throw new Error(`No response defined for status code ${responseCode}`);
|
|
31
|
+
}
|
|
32
|
+
for (const header in result[responseCode].headers) {
|
|
33
|
+
res.set(String(header), String(result[responseCode].headers[header]));
|
|
34
|
+
}
|
|
35
|
+
res.status(responseCode);
|
|
36
|
+
const body = 'body' in result[responseCode]
|
|
37
|
+
? result[responseCode].body
|
|
38
|
+
: undefined;
|
|
39
|
+
if (body === undefined) {
|
|
40
|
+
res.end();
|
|
41
|
+
}
|
|
42
|
+
else if (body instanceof Readable) {
|
|
43
|
+
body.pipe(res);
|
|
44
|
+
}
|
|
45
|
+
else if (body instanceof ReadableStream) {
|
|
46
|
+
const reader = body.getReader();
|
|
47
|
+
const read = async () => {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
if (done) {
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.write(value.toString());
|
|
54
|
+
await read();
|
|
55
|
+
};
|
|
56
|
+
await read();
|
|
57
|
+
}
|
|
58
|
+
else if (typeof body === 'string') {
|
|
59
|
+
res.send(body);
|
|
60
|
+
}
|
|
61
|
+
else if (typeof body === 'object' &&
|
|
62
|
+
body !== null &&
|
|
63
|
+
(Symbol.asyncIterator in body || Symbol.iterator in body)) {
|
|
64
|
+
if (Array.isArray(body)) {
|
|
65
|
+
res.json(body);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for await (const chunk of body) {
|
|
69
|
+
res.write(chunk.toString());
|
|
70
|
+
}
|
|
71
|
+
res.end();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
res.json(typeof body === 'object' && body && 'toJSON' in body
|
|
75
|
+
? body.toJSON()
|
|
76
|
+
: body);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function openapiRoutes(routes, app = express()) {
|
|
81
|
+
for (const path of Object.keys(routes)) {
|
|
82
|
+
for (const method in routes[path]) {
|
|
83
|
+
assertHttpMethod(method);
|
|
84
|
+
const route = routes[path][method];
|
|
85
|
+
const expressPath = openapiPathToExpressPath(String(path));
|
|
86
|
+
app[method](expressPath, routeResponse(route));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return app;
|
|
90
|
+
}
|
|
91
|
+
export { openapiRoutes };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Application, NextFunction, Request as ExpressRequest, Response as ExpressResponse } from 'express';
|
|
2
|
+
import { GetRequestBody, GetRequestHeaders, GetResponseBody, GetPathParameters, GetQueryParameters, GetResponseHeaders, GetStatusCodes, HttpMethod, OpenapiSpec, StandardHeaderMap, StandardRequestHeader } from './openapi-types/index.js';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
type OpenapiRequest<T> = Omit<ExpressRequest, 'query' | 'params' | 'body' | 'header' | 'headers'> & {
|
|
5
|
+
query: GetQueryParameters<T>;
|
|
6
|
+
params: GetPathParameters<T>;
|
|
7
|
+
body: GetRequestBody<T>;
|
|
8
|
+
header: <const THeader extends keyof GetRequestHeaders<T>>(header: StandardRequestHeader | THeader) => GetRequestHeaders<T>[THeader];
|
|
9
|
+
headers: StandardHeaderMap & GetRequestHeaders<T>;
|
|
10
|
+
};
|
|
11
|
+
type OpenapiResponse<T, Code extends GetStatusCodes<T> = GetStatusCodes<T>> = {
|
|
12
|
+
[code in NoInfer<Code>]?: {
|
|
13
|
+
headers: GetResponseHeaders<T, code> & StandardHeaderMap;
|
|
14
|
+
} & (GetResponseBody<T, code> extends [undefined] ? {} : {
|
|
15
|
+
body: GetResponseBody<T, code> | {
|
|
16
|
+
toJSON(): GetResponseBody<T, code>;
|
|
17
|
+
} | Readable | ReadableStream<{
|
|
18
|
+
toString(): string;
|
|
19
|
+
}> | Iterable<{
|
|
20
|
+
toString(): string;
|
|
21
|
+
}> | AsyncIterable<{
|
|
22
|
+
toString(): string;
|
|
23
|
+
}>;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
type HandlerResult<Schema> = Promise<OpenapiResponse<Schema>> | OpenapiResponse<Schema>;
|
|
27
|
+
type Route<Schema> = {
|
|
28
|
+
handler: (request: OpenapiRequest<Schema>) => HandlerResult<Schema>;
|
|
29
|
+
middleware?: ((request: OpenapiRequest<Schema>) => HandlerResult<Schema>)[];
|
|
30
|
+
};
|
|
31
|
+
type Routes<Paths> = {
|
|
32
|
+
[Path in keyof Paths]: {
|
|
33
|
+
[Method in Extract<keyof Paths[Path], HttpMethod> as [
|
|
34
|
+
undefined
|
|
35
|
+
] extends [Paths[Path][Method]] ? never : Method]: Route<Paths[Path][Method]>;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type OpenapiApplication<T> = Application & {
|
|
39
|
+
version: string;
|
|
40
|
+
openApiPath: string;
|
|
41
|
+
routes: Routes<T>;
|
|
42
|
+
spec: OpenapiSpec;
|
|
43
|
+
};
|
|
44
|
+
type ExpressMiddleware = (request: ExpressRequest, response: ExpressResponse, next: NextFunction) => void;
|
|
45
|
+
type ExpressMiddlewareWithError = (error: any, request: ExpressRequest, response: ExpressResponse, next: NextFunction) => void;
|
|
46
|
+
type ErrorResponse = {
|
|
47
|
+
message: string;
|
|
48
|
+
detail: string;
|
|
49
|
+
path: string;
|
|
50
|
+
errorCode: string;
|
|
51
|
+
};
|
|
52
|
+
declare global {
|
|
53
|
+
namespace Express {
|
|
54
|
+
interface Request {
|
|
55
|
+
bodyAsString: string;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export type { OpenapiApplication, OpenapiRequest, OpenapiResponse, OpenapiSpec, Route, Routes, HandlerResult, ExpressMiddleware, ExpressMiddlewareWithError, ErrorResponse, ExpressRequest, ExpressResponse, };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OpenapiRequest } from './types.js';
|
|
2
|
+
declare const validateRequest: <T>(req: any) => req is OpenapiRequest<T>;
|
|
3
|
+
declare const openapiPathToExpressPath: (openapiPath: string) => string;
|
|
4
|
+
declare const redirect: (url: string) => {
|
|
5
|
+
302: {
|
|
6
|
+
headers: {};
|
|
7
|
+
body: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export { validateRequest, openapiPathToExpressPath, redirect };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const validateRequest = (req) => {
|
|
2
|
+
//TODO: validate the request
|
|
3
|
+
return true;
|
|
4
|
+
};
|
|
5
|
+
const openapiPathToExpressPath = (openapiPath) => {
|
|
6
|
+
return openapiPath.replace(/\{/gm, ':').replace(/\}/gm, '');
|
|
7
|
+
};
|
|
8
|
+
const redirect = (url) => {
|
|
9
|
+
return {
|
|
10
|
+
302: {
|
|
11
|
+
headers: {},
|
|
12
|
+
body: `<head><meta http-equiv="Refresh" content="0; URL=${url}" /></head>`,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export { validateRequest, openapiPathToExpressPath, redirect };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-openapi-express",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"directories": {
|
|
8
|
+
"test": "test"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"openapi",
|
|
12
|
+
"express",
|
|
13
|
+
"typescript",
|
|
14
|
+
"rest-api",
|
|
15
|
+
"api",
|
|
16
|
+
"validation",
|
|
17
|
+
"type-safe",
|
|
18
|
+
"schema-validation",
|
|
19
|
+
"openapi-typescript",
|
|
20
|
+
"request-validation",
|
|
21
|
+
"response-validation",
|
|
22
|
+
"nodejs"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"express": "^5.0.0",
|
|
28
|
+
"express-openapi-validator": "^5.3.9",
|
|
29
|
+
"lodash-es": "^4.17.23",
|
|
30
|
+
"yaml": "^2.8.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "^5.0.0",
|
|
34
|
+
"@types/lodash-es": "^4.17.12",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@types/supertest": "^6.0.2",
|
|
37
|
+
"@types/swagger-ui-express": "^4.1.6",
|
|
38
|
+
"openapi-types": "^12.1.0",
|
|
39
|
+
"openapi-typescript": "^7.4.1",
|
|
40
|
+
"supertest": "^7.0.0",
|
|
41
|
+
"typescript": "^5.0.4",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "pnpm test:unit",
|
|
50
|
+
"test:unit": "pnpm test:compile:spec && vitest run",
|
|
51
|
+
"test:compile:spec": "openapi-typescript test/unit/openapi.test.yaml --output test/unit/test-schema.ts",
|
|
52
|
+
"compile": "tsc -p tsconfig.prod.json",
|
|
53
|
+
"watch": "tsc -p tsconfig.prod.json --watch"
|
|
54
|
+
}
|
|
55
|
+
}
|