snap-on-openapi 1.0.6 → 1.0.9
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/OpenApi.js +7 -6
- package/dist/services/DescriptionChecker/DescriptionChecker.js +1 -1
- package/dist/services/ExpressWrapper/ExpressWrapper.d.ts +2 -1
- package/dist/services/ExpressWrapper/ExpressWrapper.js +54 -44
- package/dist/services/RoutingFactory/RoutingFactory.d.ts +1 -1
- package/dist/services/SchemaGenerator/SchemaGenerator.d.ts +2 -0
- package/dist/services/SchemaGenerator/SchemaGenerator.js +48 -25
- package/dist/services/TanstackStartWrapper/TanstackStartWrapper.js +1 -0
- package/dist/services/ValidationUtils/ValidationUtils.d.ts +4 -3
- package/dist/services/ValidationUtils/ValidationUtils.js +4 -2
- package/dist/services/ValidationUtils/transformers/stringDateTimeTransformer.d.ts +3 -0
- package/dist/services/ValidationUtils/transformers/stringDateTimeTransformer.js +20 -0
- package/dist/services/ValidationUtils/transformers/stringDateTimeTransformer.test copy.d.ts +1 -0
- package/dist/services/ValidationUtils/transformers/stringDateTimeTransformer.test copy.js +13 -0
- package/dist/services/ValidationUtils/transformers/stringDateTransformer.d.ts +1 -1
- package/dist/services/ValidationUtils/transformers/stringDateTransformer.js +17 -14
- package/dist/types/AnyRoute.d.ts +1 -1
- package/dist/types/Route.d.ts +10 -8
- package/dist/types/config/Config.d.ts +9 -0
- package/package.json +1 -1
package/dist/OpenApi.js
CHANGED
|
@@ -43,7 +43,7 @@ export class OpenApi {
|
|
|
43
43
|
this.basePath = config.basePath;
|
|
44
44
|
const info = {
|
|
45
45
|
title: config.apiName ?? 'My API',
|
|
46
|
-
version: '
|
|
46
|
+
version: config.apiVersion ?? '1.0.0',
|
|
47
47
|
};
|
|
48
48
|
this.schemaGenerator = new SchemaGenerator(this.logger.derrive('SchemaGenerator'), info, this.config, this.routes, this.servers);
|
|
49
49
|
this.wrappers = {
|
|
@@ -106,11 +106,11 @@ export class OpenApi {
|
|
|
106
106
|
}
|
|
107
107
|
getRouteForPath(path, method) {
|
|
108
108
|
const fittingRoutes = [];
|
|
109
|
+
const pathParts = path.split('/').filter((x) => x !== '');
|
|
109
110
|
outer: for (const route of this.routes) {
|
|
110
111
|
if (route.method === method) {
|
|
111
112
|
fittingRoutes.push(route);
|
|
112
113
|
const routeParts = route.path.split('/').filter((x) => x !== '');
|
|
113
|
-
const pathParts = path.split('/').filter((x) => x !== '');
|
|
114
114
|
if (routeParts.length !== pathParts.length) {
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
@@ -130,7 +130,8 @@ export class OpenApi {
|
|
|
130
130
|
async processRootRoute(originalReq) {
|
|
131
131
|
try {
|
|
132
132
|
const url = new URL(originalReq.url);
|
|
133
|
-
const
|
|
133
|
+
const basePath = this.getBasePath() === '/' ? '' : this.getBasePath();
|
|
134
|
+
const urlPath = url.pathname.replace(basePath, '');
|
|
134
135
|
const route = this.getRouteForPath(urlPath, originalReq.method);
|
|
135
136
|
if (!route) {
|
|
136
137
|
this.logger.info(`Route for ${originalReq.method}:${urlPath} not found`);
|
|
@@ -177,7 +178,7 @@ export class OpenApi {
|
|
|
177
178
|
};
|
|
178
179
|
this.logger.info(`Calling route ${route.path}`);
|
|
179
180
|
this.logger.info(`${req.method}: ${req.path}`, {
|
|
180
|
-
|
|
181
|
+
path: req.params,
|
|
181
182
|
query: req.query,
|
|
182
183
|
body: req.body,
|
|
183
184
|
});
|
|
@@ -194,7 +195,7 @@ export class OpenApi {
|
|
|
194
195
|
let response;
|
|
195
196
|
const containsBody = route.method !== Method.GET;
|
|
196
197
|
if (containsBody && route.validators.body) {
|
|
197
|
-
const body = route.validators.body.
|
|
198
|
+
const body = route.validators.body.safeParse(req.body);
|
|
198
199
|
if (!body.success) {
|
|
199
200
|
throw new ValidationError(body.error, ValidationLocation.Body);
|
|
200
201
|
}
|
|
@@ -237,7 +238,7 @@ export class OpenApi {
|
|
|
237
238
|
}
|
|
238
239
|
const finalResponse = route.validators.responseHeaders ? response : { body: response, headers: {} };
|
|
239
240
|
const finalResponseValidator = z.object({
|
|
240
|
-
body: route.validators.response,
|
|
241
|
+
body: route.validators.response ?? z.undefined(),
|
|
241
242
|
headers: route.validators.responseHeaders?.strict() ?? z.object({}),
|
|
242
243
|
});
|
|
243
244
|
const validated = finalResponseValidator.safeParse(finalResponse);
|
|
@@ -16,7 +16,7 @@ export class DescriptionChecker {
|
|
|
16
16
|
if (!route.description || route.description.length < minimalLength) {
|
|
17
17
|
throw new Error(`Description for ${route.path} is missing or too small`);
|
|
18
18
|
}
|
|
19
|
-
this.checkValidatorDescriptions(route, 'responseValidator', 'responseValidator', route.validators.response);
|
|
19
|
+
this.checkValidatorDescriptions(route, 'responseValidator', 'responseValidator', route.validators.response ?? z.undefined());
|
|
20
20
|
this.checkValidatorDescriptions(route, 'pathValidator', 'pathValidator', route.validators.path ?? z.object({}), false);
|
|
21
21
|
this.checkValidatorDescriptions(route, 'queryValidator', 'queryValidator', route.validators.query ?? z.object({}), false);
|
|
22
22
|
this.checkValidatorDescriptions(route, 'bodyValidator', 'bodyValidator', route.validators.body ?? z.object({}), false);
|
|
@@ -9,9 +9,10 @@ export declare class ExpressWrapper<TRouteTypes extends string, TErrorCodes exte
|
|
|
9
9
|
protected developmentUtils: DevelopmentUtils;
|
|
10
10
|
protected schemaRoute: RoutePath;
|
|
11
11
|
constructor(openApi: OpenApi<TRouteTypes, TErrorCodes, TConfig>);
|
|
12
|
-
requestBodyToString(req: ExpressRequest): Promise<string | undefined>;
|
|
13
12
|
createStoplightRoute(route: RoutePath, expressApp: ExpressApp): void;
|
|
14
13
|
createSwaggerRoute(route: RoutePath, expressApp: ExpressApp): void;
|
|
15
14
|
createSchemaRoute(route: RoutePath, expressApp: ExpressApp): void;
|
|
16
15
|
createOpenApiRootRoute(expressApp: ExpressApp): void;
|
|
16
|
+
protected covertExpressRequestToRequest(req: ExpressRequest): Promise<Request>;
|
|
17
|
+
protected requestBodyToString(req: ExpressRequest): Promise<string | undefined>;
|
|
17
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { format } from 'url';
|
|
2
1
|
import { DevelopmentUtils } from '../DevelopmentUtils/DevelopmentUtils.js';
|
|
2
|
+
import { format, parse } from 'url';
|
|
3
3
|
export class ExpressWrapper {
|
|
4
4
|
service;
|
|
5
5
|
developmentUtils;
|
|
@@ -8,25 +8,6 @@ export class ExpressWrapper {
|
|
|
8
8
|
this.service = openApi;
|
|
9
9
|
this.developmentUtils = new DevelopmentUtils();
|
|
10
10
|
}
|
|
11
|
-
async requestBodyToString(req) {
|
|
12
|
-
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const chunks = [];
|
|
17
|
-
req.on('data', (chunk) => {
|
|
18
|
-
chunks.push(chunk);
|
|
19
|
-
});
|
|
20
|
-
req.on('end', () => {
|
|
21
|
-
const bodyBuffer = Buffer.concat(chunks);
|
|
22
|
-
const bodyString = bodyBuffer.toString();
|
|
23
|
-
resolve(bodyString);
|
|
24
|
-
});
|
|
25
|
-
req.on('error', (error) => {
|
|
26
|
-
reject(error);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
11
|
createStoplightRoute(route, expressApp) {
|
|
31
12
|
const handler = async (req, res) => {
|
|
32
13
|
const body = this.developmentUtils.getStoplightHtml(this.schemaRoute);
|
|
@@ -52,31 +33,15 @@ export class ExpressWrapper {
|
|
|
52
33
|
}
|
|
53
34
|
createOpenApiRootRoute(expressApp) {
|
|
54
35
|
const route = this.service.getBasePath();
|
|
55
|
-
const headerToStr = (header) => {
|
|
56
|
-
if (Array.isArray(header)) {
|
|
57
|
-
return header.join(',');
|
|
58
|
-
}
|
|
59
|
-
return header;
|
|
60
|
-
};
|
|
61
36
|
const handler = async (req, res) => {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pathname: req.originalUrl,
|
|
71
|
-
});
|
|
72
|
-
const body = await this.requestBodyToString(req);
|
|
73
|
-
const openApiRequest = new Request(url, {
|
|
74
|
-
headers: headers,
|
|
75
|
-
method: req.method,
|
|
76
|
-
body: body,
|
|
77
|
-
});
|
|
78
|
-
const result = await this.service.processRootRoute(openApiRequest);
|
|
79
|
-
res.status(result.status).header('Content-Type', 'application/json').json(result.body);
|
|
37
|
+
const request = await this.covertExpressRequestToRequest(req);
|
|
38
|
+
const result = await this.service.processRootRoute(request);
|
|
39
|
+
res.status(result.status);
|
|
40
|
+
res.header('Content-Type', 'application/json');
|
|
41
|
+
for (const header of Object.entries(result.headers)) {
|
|
42
|
+
res.header(header[0], header[1]);
|
|
43
|
+
}
|
|
44
|
+
res.json(result.body);
|
|
80
45
|
};
|
|
81
46
|
const regex = new RegExp(`${route}.*`);
|
|
82
47
|
expressApp.get(regex, handler);
|
|
@@ -85,4 +50,49 @@ export class ExpressWrapper {
|
|
|
85
50
|
expressApp.delete(regex, handler);
|
|
86
51
|
expressApp.put(regex, handler);
|
|
87
52
|
}
|
|
53
|
+
async covertExpressRequestToRequest(req) {
|
|
54
|
+
const headerToStr = (header) => {
|
|
55
|
+
if (Array.isArray(header)) {
|
|
56
|
+
return header.join(',');
|
|
57
|
+
}
|
|
58
|
+
return header;
|
|
59
|
+
};
|
|
60
|
+
const emptyHeaders = {};
|
|
61
|
+
const headers = Object.entries(req.headers).reduce((acc, val) => ({
|
|
62
|
+
...acc,
|
|
63
|
+
...(typeof val[1] !== 'undefined' ? { [val[0]]: headerToStr(val[1]) } : {}),
|
|
64
|
+
}), emptyHeaders);
|
|
65
|
+
const body = await this.requestBodyToString(req);
|
|
66
|
+
const parsedUrl = parse(req.originalUrl, true);
|
|
67
|
+
const url = format({
|
|
68
|
+
...parsedUrl,
|
|
69
|
+
host: req.host,
|
|
70
|
+
protocol: req.protocol,
|
|
71
|
+
});
|
|
72
|
+
const openApiRequest = new Request(url, {
|
|
73
|
+
headers: headers,
|
|
74
|
+
method: req.method,
|
|
75
|
+
body: body,
|
|
76
|
+
});
|
|
77
|
+
return openApiRequest;
|
|
78
|
+
}
|
|
79
|
+
async requestBodyToString(req) {
|
|
80
|
+
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
req.on('data', (chunk) => {
|
|
86
|
+
chunks.push(chunk);
|
|
87
|
+
});
|
|
88
|
+
req.on('end', () => {
|
|
89
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
90
|
+
const bodyString = bodyBuffer.toString();
|
|
91
|
+
resolve(bodyString);
|
|
92
|
+
});
|
|
93
|
+
req.on('error', (error) => {
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
88
98
|
}
|
|
@@ -6,5 +6,5 @@ import { RouteExtraProps } from '../../types/config/RouteExtraProps.js';
|
|
|
6
6
|
export declare class RoutingFactory<TRouteTypes extends string, TErrorCodes extends string, TConfig extends AnyConfig<TRouteTypes, TErrorCodes>> {
|
|
7
7
|
protected map: TConfig;
|
|
8
8
|
constructor(map: TConfig);
|
|
9
|
-
createRoute<TType extends TRouteTypes, TMethod extends Method, TResponseValidator extends ZodFirstPartySchemaTypes, TQueryValidator extends ZodObject<ZodRawShape> | undefined = undefined, TPathValidator extends ZodObject<ZodRawShape> | undefined = undefined, TBodyValidator extends
|
|
9
|
+
createRoute<TType extends TRouteTypes, TMethod extends Method, TResponseValidator extends ZodFirstPartySchemaTypes | undefined = undefined, TQueryValidator extends ZodObject<ZodRawShape> | undefined = undefined, TPathValidator extends ZodObject<ZodRawShape> | undefined = undefined, TBodyValidator extends ZodFirstPartySchemaTypes | undefined = undefined, TResponseHeadersValidator extends ZodObject<ZodRawShape> | undefined = undefined>(params: Route<TType, Awaited<ReturnType<TConfig['routes'][TType]['contextFactory']>>, TResponseValidator, TPathValidator, TQueryValidator, TBodyValidator, TResponseHeadersValidator, TMethod> & (RouteExtraProps<TConfig['routes'][TType]['extraProps']>)): Route<TType, Awaited<ReturnType<TConfig['routes'][TType]['contextFactory']>>, TResponseValidator, TPathValidator, TQueryValidator, TBodyValidator, TResponseHeadersValidator>;
|
|
10
10
|
}
|
|
@@ -13,6 +13,8 @@ export declare class SchemaGenerator<TRouteTypes extends string, TErrorCodes ext
|
|
|
13
13
|
protected routeSpec: TConfig;
|
|
14
14
|
constructor(logger: Logger, info: Info, spec: TConfig, routes: AnyRoute<TRouteTypes>[], servers: Server[]);
|
|
15
15
|
getYaml(): string;
|
|
16
|
+
getJson(): string;
|
|
17
|
+
saveJson(path: string): void;
|
|
16
18
|
saveYaml(path: string): void;
|
|
17
19
|
protected createDocument(): ReturnType<typeof createDocument>;
|
|
18
20
|
protected createOperation(route: AnyRoute<TRouteTypes>): ZodOpenApiOperationObject;
|
|
@@ -19,17 +19,29 @@ export class SchemaGenerator {
|
|
|
19
19
|
}
|
|
20
20
|
getYaml() {
|
|
21
21
|
const document = this.createDocument();
|
|
22
|
+
this.logger.info('Generating YAML for Open API');
|
|
22
23
|
const yaml = stringify(document, { aliasDuplicateObjects: false });
|
|
23
24
|
return yaml;
|
|
24
25
|
}
|
|
26
|
+
getJson() {
|
|
27
|
+
const document = this.createDocument();
|
|
28
|
+
this.logger.info('Generating JSON for Open API');
|
|
29
|
+
const json = JSON.stringify(document, null, 2);
|
|
30
|
+
return json;
|
|
31
|
+
}
|
|
32
|
+
saveJson(path) {
|
|
33
|
+
const json = this.getJson();
|
|
34
|
+
this.logger.info(`Saving JSON to ${path}`);
|
|
35
|
+
writeFileSync(path, json);
|
|
36
|
+
}
|
|
25
37
|
saveYaml(path) {
|
|
26
|
-
this.logger.info('Generating YAML for Open API');
|
|
27
38
|
const yaml = this.getYaml();
|
|
39
|
+
this.logger.info(`Saving YAML to ${path}`);
|
|
28
40
|
writeFileSync(path, yaml);
|
|
29
41
|
}
|
|
30
42
|
createDocument() {
|
|
31
43
|
const openApi = {
|
|
32
|
-
openapi: '3.1.0',
|
|
44
|
+
openapi: this.routeSpec.generator?.openApiVersion ?? '3.1.0',
|
|
33
45
|
info: this.info,
|
|
34
46
|
components: {
|
|
35
47
|
securitySchemes: {
|
|
@@ -68,15 +80,20 @@ export class SchemaGenerator {
|
|
|
68
80
|
query: route.validators.query,
|
|
69
81
|
path: route.validators.path,
|
|
70
82
|
};
|
|
83
|
+
const mediaTypes = this.routeSpec.generator?.responseMediaTypes ?? ['application/json'];
|
|
84
|
+
const content = mediaTypes.reduce((acc, mediaType) => ({
|
|
85
|
+
...acc,
|
|
86
|
+
[mediaType]: { schema: route.validators.response },
|
|
87
|
+
}), {});
|
|
71
88
|
const operation = {
|
|
72
89
|
requestParams: requestParams,
|
|
73
90
|
description: route.description,
|
|
91
|
+
tags: route.tags,
|
|
92
|
+
operationId: route.operationId,
|
|
74
93
|
responses: {
|
|
75
94
|
200: {
|
|
76
|
-
description: 'Good Response',
|
|
77
|
-
content:
|
|
78
|
-
'application/json': { schema: route.validators.response },
|
|
79
|
-
},
|
|
95
|
+
description: this.routeSpec.generator?.goodResponseDescription ?? 'Good Response',
|
|
96
|
+
content: route.validators.response ? content : undefined,
|
|
80
97
|
},
|
|
81
98
|
},
|
|
82
99
|
};
|
|
@@ -94,22 +111,25 @@ export class SchemaGenerator {
|
|
|
94
111
|
errors.push(errorConfig);
|
|
95
112
|
httpStatusMap.set(errorConfig.status, errors);
|
|
96
113
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
const shouldGenerateErrors = this.routeSpec.generator?.generateErrors ?? true;
|
|
115
|
+
if (shouldGenerateErrors) {
|
|
116
|
+
for (const [code, errors] of httpStatusMap.entries()) {
|
|
117
|
+
const error = errors[0];
|
|
118
|
+
if (!error) {
|
|
119
|
+
throw new Error(`No errors found for code '${code}'`);
|
|
120
|
+
}
|
|
121
|
+
const description = errors.map((x) => x.description).join(' or ');
|
|
122
|
+
const validators = errors.map((x) => x.responseValidator);
|
|
123
|
+
const schema = errors.length === 1 ? error.responseValidator : z.union(validators).openapi({ unionOneOf: true });
|
|
124
|
+
operation.responses[error.status] = {
|
|
125
|
+
description: description,
|
|
126
|
+
content: {
|
|
127
|
+
'application/json': {
|
|
128
|
+
schema: schema,
|
|
129
|
+
},
|
|
110
130
|
},
|
|
111
|
-
}
|
|
112
|
-
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
if (this.routeSpec.routes[route.type].authorization) {
|
|
115
135
|
operation.security = [
|
|
@@ -118,11 +138,14 @@ export class SchemaGenerator {
|
|
|
118
138
|
},
|
|
119
139
|
];
|
|
120
140
|
}
|
|
121
|
-
if (route.method !== Method.GET) {
|
|
141
|
+
if (route.method !== Method.GET && route.validators.body) {
|
|
142
|
+
const mediaTypes = this.routeSpec.generator?.requestMediaTypes ?? ['application/json'];
|
|
143
|
+
const content = mediaTypes.reduce((acc, mediaType) => ({
|
|
144
|
+
...acc,
|
|
145
|
+
[mediaType]: { schema: route.validators.body },
|
|
146
|
+
}), {});
|
|
122
147
|
operation.requestBody = {
|
|
123
|
-
content
|
|
124
|
-
'application/json': { schema: route.validators.body },
|
|
125
|
-
},
|
|
148
|
+
content,
|
|
126
149
|
};
|
|
127
150
|
}
|
|
128
151
|
return operation;
|
|
@@ -2,7 +2,8 @@ import 'zod-openapi/extend';
|
|
|
2
2
|
import { ZodObject, ZodRawShape, ZodType } from 'zod';
|
|
3
3
|
export declare class ValidationUtils {
|
|
4
4
|
readonly strings: {
|
|
5
|
-
datetime: import("zod").ZodEffects<import("zod").ZodEffects<import("zod").ZodString, string, string>, Date, string>;
|
|
5
|
+
datetime: import("zod").ZodUnion<[import("zod").ZodDate, import("zod").ZodEffects<import("zod").ZodEffects<import("zod").ZodString, string, string>, Date, string>]>;
|
|
6
|
+
date: import("zod").ZodUnion<[import("zod").ZodDate, import("zod").ZodEffects<import("zod").ZodEffects<import("zod").ZodString, string, string>, Date, string>]>;
|
|
6
7
|
number: import("zod").ZodEffects<import("zod").ZodEffects<import("zod").ZodString, string, string>, number, string>;
|
|
7
8
|
boolean: import("zod").ZodEffects<import("zod").ZodEnum<["true", "false"]>, boolean, "true" | "false">;
|
|
8
9
|
};
|
|
@@ -39,18 +40,18 @@ export declare class ValidationUtils {
|
|
|
39
40
|
count: number;
|
|
40
41
|
}>;
|
|
41
42
|
}, "strip", import("zod").ZodTypeAny, {
|
|
43
|
+
items: T["_output"][];
|
|
42
44
|
info: {
|
|
43
45
|
page: number;
|
|
44
46
|
pageSize: number;
|
|
45
47
|
count: number;
|
|
46
48
|
};
|
|
47
|
-
items: T["_output"][];
|
|
48
49
|
}, {
|
|
50
|
+
items: T["_input"][];
|
|
49
51
|
info: {
|
|
50
52
|
page: number;
|
|
51
53
|
pageSize: number;
|
|
52
54
|
count: number;
|
|
53
55
|
};
|
|
54
|
-
items: T["_input"][];
|
|
55
56
|
}>;
|
|
56
57
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import 'zod-openapi/extend';
|
|
2
2
|
import { array, number, object } from 'zod';
|
|
3
|
-
import { stringDateTransformer } from './transformers/stringDateTransformer.js';
|
|
4
3
|
import { stringNumberTransformer } from './transformers/stringNumberTransfromer.js';
|
|
5
4
|
import { stringBooleanTransformer } from './transformers/stringBooleanTransformer.js';
|
|
5
|
+
import { stringDateTimeTransformer } from './transformers/stringDateTimeTransformer.js';
|
|
6
|
+
import { stringDateTransformer } from './transformers/stringDateTransformer.js';
|
|
6
7
|
export class ValidationUtils {
|
|
7
8
|
strings = {
|
|
8
|
-
datetime:
|
|
9
|
+
datetime: stringDateTimeTransformer,
|
|
10
|
+
date: stringDateTransformer,
|
|
9
11
|
number: stringNumberTransformer,
|
|
10
12
|
boolean: stringBooleanTransformer,
|
|
11
13
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'zod-openapi/extend';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
export const stringDateTimeTransformer = z.union([
|
|
4
|
+
z.date(),
|
|
5
|
+
z.string().refine((input) => {
|
|
6
|
+
try {
|
|
7
|
+
const output = new Date(Date.parse(input));
|
|
8
|
+
const outputStr = output.toISOString().replace('T', ' ');
|
|
9
|
+
const inputStr = input.replace('T', ' ');
|
|
10
|
+
console.log(inputStr, outputStr);
|
|
11
|
+
return inputStr === outputStr;
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}, 'Not a valid date string')
|
|
17
|
+
.transform((x) => {
|
|
18
|
+
return new Date(Date.parse(x));
|
|
19
|
+
}),
|
|
20
|
+
]).openapi({ type: 'string', format: 'date-time' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { stringDateTimeTransformer } from './stringDateTimeTransformer.js';
|
|
3
|
+
describe('stringDateTimeTransformer', () => {
|
|
4
|
+
test('Can parse ISO datetime', async () => {
|
|
5
|
+
const result = stringDateTimeTransformer.safeParse('2025-07-03T08:33:32.442Z');
|
|
6
|
+
expect(result.success).toBe(true);
|
|
7
|
+
expect(result.data?.toISOString()).toBe('2025-07-03T08:33:32.442Z');
|
|
8
|
+
});
|
|
9
|
+
test("Doesn't throw on bad input", async () => {
|
|
10
|
+
const result = stringDateTimeTransformer.safeParse('jibberish');
|
|
11
|
+
expect(result.success).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import 'zod-openapi/extend';
|
|
2
2
|
import z from 'zod';
|
|
3
|
-
export declare const stringDateTransformer: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, Date, string>;
|
|
3
|
+
export declare const stringDateTransformer: z.ZodUnion<[z.ZodDate, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, Date, string>]>;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import 'zod-openapi/extend';
|
|
2
2
|
import z from 'zod';
|
|
3
|
-
export const stringDateTransformer = z.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
export const stringDateTransformer = z.union([
|
|
4
|
+
z.date(),
|
|
5
|
+
z.string().refine((input) => {
|
|
6
|
+
try {
|
|
7
|
+
const output = new Date(Date.parse(input));
|
|
8
|
+
const outputStr = output.toISOString().split('T')[0];
|
|
9
|
+
const inputStr = input.replace('T', ' ');
|
|
10
|
+
return inputStr === outputStr;
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}, 'Not a valid date string')
|
|
16
|
+
.transform((x) => {
|
|
17
|
+
return new Date(Date.parse(x));
|
|
18
|
+
}),
|
|
19
|
+
]).openapi({ type: 'string', format: 'date-time' });
|
package/dist/types/AnyRoute.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ZodObject, ZodRawShape, ZodFirstPartySchemaTypes } from 'zod';
|
|
2
2
|
import { Route } from './Route.js';
|
|
3
|
-
export type AnyRoute<TRouteType extends string> = Route<TRouteType, any, ZodFirstPartySchemaTypes, ZodObject<ZodRawShape> | undefined, ZodObject<ZodRawShape> | undefined,
|
|
3
|
+
export type AnyRoute<TRouteType extends string> = Route<TRouteType, any, ZodFirstPartySchemaTypes | undefined, ZodObject<ZodRawShape> | undefined, ZodObject<ZodRawShape> | undefined, ZodFirstPartySchemaTypes | undefined, ZodObject<ZodRawShape> | undefined>;
|
package/dist/types/Route.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { z, ZodFirstPartySchemaTypes, ZodObject, ZodRawShape } from 'zod';
|
|
2
2
|
import { Method } from '../enums/Methods.js';
|
|
3
3
|
import { RoutePath } from './RoutePath.js';
|
|
4
|
-
type BodyHandlerResponse<T extends ZodFirstPartySchemaTypes> = Promise<z.infer<T
|
|
5
|
-
type FullHandlerRespnse<T extends ZodFirstPartySchemaTypes, THeaders extends ZodObject<ZodRawShape
|
|
6
|
-
body:
|
|
7
|
-
headers: z.infer<THeaders
|
|
4
|
+
type BodyHandlerResponse<T extends ZodFirstPartySchemaTypes | undefined = undefined> = Promise<T extends undefined ? undefined : z.infer<Exclude<T, undefined>>>;
|
|
5
|
+
type FullHandlerRespnse<T extends ZodFirstPartySchemaTypes | undefined, THeaders extends ZodObject<ZodRawShape> | undefined> = Promise<{
|
|
6
|
+
body: Awaited<BodyHandlerResponse<T>>;
|
|
7
|
+
headers: z.infer<Exclude<THeaders, undefined>>;
|
|
8
8
|
}>;
|
|
9
|
-
type HandlerResponse<T extends ZodFirstPartySchemaTypes, TH extends ZodObject<ZodRawShape> | undefined> = TH extends undefined ? BodyHandlerResponse<T> : FullHandlerRespnse<T, Exclude<TH, undefined>>;
|
|
10
|
-
export interface Route<TType extends string, TContext extends object, TResponseValidator extends ZodFirstPartySchemaTypes, TPathValidator extends ZodObject<ZodRawShape> | undefined, TQueryValidator extends ZodObject<ZodRawShape> | undefined, TBodyValidator extends
|
|
9
|
+
type HandlerResponse<T extends ZodFirstPartySchemaTypes | undefined, TH extends ZodObject<ZodRawShape> | undefined> = TH extends undefined ? BodyHandlerResponse<T> : FullHandlerRespnse<T, Exclude<TH, undefined>>;
|
|
10
|
+
export interface Route<TType extends string, TContext extends object, TResponseValidator extends ZodFirstPartySchemaTypes | undefined, TPathValidator extends ZodObject<ZodRawShape> | undefined, TQueryValidator extends ZodObject<ZodRawShape> | undefined, TBodyValidator extends ZodFirstPartySchemaTypes | undefined, TResponseHeadersValidator extends ZodObject<ZodRawShape> | undefined, TMethod extends Method = Method> {
|
|
11
|
+
tags?: string[];
|
|
12
|
+
operationId?: string;
|
|
11
13
|
type: TType;
|
|
12
14
|
method: TMethod;
|
|
13
15
|
path: RoutePath;
|
|
@@ -16,12 +18,12 @@ export interface Route<TType extends string, TContext extends object, TResponseV
|
|
|
16
18
|
query?: TQueryValidator;
|
|
17
19
|
path?: TPathValidator;
|
|
18
20
|
body?: TMethod extends 'GET' ? never : TBodyValidator;
|
|
19
|
-
response
|
|
21
|
+
response?: TResponseValidator;
|
|
20
22
|
responseHeaders?: TResponseHeadersValidator;
|
|
21
23
|
};
|
|
22
24
|
handler: (context: {
|
|
23
25
|
params: {
|
|
24
|
-
body: TBodyValidator extends
|
|
26
|
+
body: TBodyValidator extends ZodFirstPartySchemaTypes ? z.infer<TBodyValidator> : object;
|
|
25
27
|
query: TQueryValidator extends ZodObject<ZodRawShape> ? z.infer<TQueryValidator> : object;
|
|
26
28
|
path: TPathValidator extends ZodObject<ZodRawShape> ? z.infer<TPathValidator> : object;
|
|
27
29
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ZodOpenApiVersion } from 'zod-openapi';
|
|
1
2
|
import { Logger } from '../../services/Logger/Logger.js';
|
|
2
3
|
import { LogLevel } from '../../services/Logger/types/LogLevel.js';
|
|
3
4
|
import { AnyRoute } from '../AnyRoute.js';
|
|
@@ -12,10 +13,18 @@ export type Config<TRouteTypes extends string, TErrorCodes extends string, TErro
|
|
|
12
13
|
logger?: Logger;
|
|
13
14
|
basePath: RoutePath;
|
|
14
15
|
routes: TRouteConfigMap;
|
|
16
|
+
generator?: {
|
|
17
|
+
openApiVersion?: ZodOpenApiVersion;
|
|
18
|
+
goodResponseDescription?: string;
|
|
19
|
+
generateErrors?: boolean;
|
|
20
|
+
requestMediaTypes?: string[];
|
|
21
|
+
responseMediaTypes?: string[];
|
|
22
|
+
};
|
|
15
23
|
errors: TErrorConfigMap;
|
|
16
24
|
defaultError: ErrorResponse<TErrorCodes, TErrorConfigMap>;
|
|
17
25
|
skipDescriptionsCheck?: boolean;
|
|
18
26
|
apiName?: string;
|
|
27
|
+
apiVersion?: string;
|
|
19
28
|
servers?: Server[];
|
|
20
29
|
logLevel?: LogLevel;
|
|
21
30
|
handleError?: (e: unknown, req: Request) => ErrorResponse<TErrorCodes, TErrorConfigMap>;
|