ts-typed-api 0.1.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/.eslintrc.json +16 -0
- package/LICENSE +201 -0
- package/README.md +181 -0
- package/dist/apiClient.d.ts +147 -0
- package/dist/apiClient.js +206 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.js +215 -0
- package/dist/definition.d.ts +121 -0
- package/dist/definition.js +79 -0
- package/dist/example-server/client.d.ts +2 -0
- package/dist/example-server/client.js +19 -0
- package/dist/example-server/definitions.d.ts +157 -0
- package/dist/example-server/definitions.js +35 -0
- package/dist/example-server/server.d.ts +1 -0
- package/dist/example-server/server.js +66 -0
- package/dist/handler.d.ts +16 -0
- package/dist/handler.js +185 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +13 -0
- package/dist/object-handlers.d.ts +16 -0
- package/dist/object-handlers.js +39 -0
- package/dist/openapiGenerator.d.ts +2 -0
- package/dist/openapiGenerator.js +119 -0
- package/dist/router/definition.d.ts +118 -0
- package/dist/router/definition.js +80 -0
- package/dist/router/router.d.ts +29 -0
- package/dist/router/router.js +168 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.js +20 -0
- package/dist/src/router/client.d.ts +151 -0
- package/dist/src/router/client.js +215 -0
- package/dist/src/router/definition.d.ts +121 -0
- package/dist/src/router/definition.js +79 -0
- package/dist/src/router/handler.d.ts +16 -0
- package/dist/src/router/handler.js +170 -0
- package/dist/src/router/index.d.ts +5 -0
- package/dist/src/router/index.js +22 -0
- package/dist/src/router/object-handlers.d.ts +16 -0
- package/dist/src/router/object-handlers.js +39 -0
- package/dist/src/router/router.d.ts +18 -0
- package/dist/src/router/router.js +20 -0
- package/examples/advanced/client.ts +226 -0
- package/examples/advanced/definitions.ts +156 -0
- package/examples/advanced/server.ts +298 -0
- package/examples/simple/client.ts +24 -0
- package/examples/simple/definitions.ts +57 -0
- package/examples/simple/server.ts +67 -0
- package/package.json +57 -0
- package/src/client.ts +368 -0
- package/src/definition.ts +231 -0
- package/src/handler.ts +220 -0
- package/src/index.ts +4 -0
- package/src/object-handlers.ts +84 -0
- package/src/router.ts +93 -0
- package/tsconfig.json +17 -0
package/dist/handler.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRouteHandlers = registerRouteHandlers;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// Register route handlers with Express, now generic over TDef
|
|
6
|
+
function registerRouteHandlers(app, apiDefinition, // Pass the actual API definition object
|
|
7
|
+
routeHandlers, // Use the generic handler type
|
|
8
|
+
middlewares) {
|
|
9
|
+
routeHandlers.forEach((specificHandlerIterationItem) => {
|
|
10
|
+
const { domain, routeKey, handler } = specificHandlerIterationItem; // Use 'as any' for simplicity in destructuring union
|
|
11
|
+
const currentDomain = domain;
|
|
12
|
+
const currentRouteKey = routeKey;
|
|
13
|
+
// Use the passed apiDefinition object
|
|
14
|
+
const routeDefinition = apiDefinition.endpoints[currentDomain][currentRouteKey];
|
|
15
|
+
if (!routeDefinition) {
|
|
16
|
+
console.error(`Route definition not found for domain "${String(currentDomain)}" and routeKey "${String(currentRouteKey)}"`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const { path, method } = routeDefinition;
|
|
20
|
+
// Apply prefix from API definition if it exists
|
|
21
|
+
const fullPath = apiDefinition.prefix
|
|
22
|
+
? `${apiDefinition.prefix.startsWith('/') ? apiDefinition.prefix : `/${apiDefinition.prefix}`}${path}`.replace(/\/+/g, '/')
|
|
23
|
+
: path;
|
|
24
|
+
const expressMiddleware = async (expressReq, expressRes) => {
|
|
25
|
+
try {
|
|
26
|
+
// Ensure TDef is correctly used for type inference if this section needs it.
|
|
27
|
+
// Currently, parsedParams,Query,Body are based on runtime routeDefinition.
|
|
28
|
+
const parsedParams = ('params' in routeDefinition && routeDefinition.params)
|
|
29
|
+
? routeDefinition.params.parse(expressReq.params)
|
|
30
|
+
: expressReq.params;
|
|
31
|
+
const parsedQuery = ('query' in routeDefinition && routeDefinition.query)
|
|
32
|
+
? routeDefinition.query.parse(expressReq.query)
|
|
33
|
+
: expressReq.query;
|
|
34
|
+
const parsedBody = (method === 'POST' || method === 'PUT') && ('body' in routeDefinition && routeDefinition.body)
|
|
35
|
+
? routeDefinition.body.parse(expressReq.body)
|
|
36
|
+
: expressReq.body;
|
|
37
|
+
// Construct TypedRequest using TDef, currentDomain, currentRouteKey
|
|
38
|
+
const finalTypedReq = {
|
|
39
|
+
...expressReq,
|
|
40
|
+
params: parsedParams,
|
|
41
|
+
query: parsedQuery,
|
|
42
|
+
body: parsedBody,
|
|
43
|
+
headers: expressReq.headers,
|
|
44
|
+
cookies: expressReq.cookies,
|
|
45
|
+
ip: expressReq.ip,
|
|
46
|
+
ips: expressReq.ips,
|
|
47
|
+
hostname: expressReq.hostname,
|
|
48
|
+
protocol: expressReq.protocol,
|
|
49
|
+
secure: expressReq.secure,
|
|
50
|
+
xhr: expressReq.xhr,
|
|
51
|
+
fresh: expressReq.fresh,
|
|
52
|
+
stale: expressReq.stale,
|
|
53
|
+
subdomains: expressReq.subdomains,
|
|
54
|
+
path: expressReq.path,
|
|
55
|
+
originalUrl: expressReq.originalUrl,
|
|
56
|
+
baseUrl: expressReq.baseUrl,
|
|
57
|
+
url: expressReq.url,
|
|
58
|
+
};
|
|
59
|
+
// Augment expressRes with the .respond method, using TDef
|
|
60
|
+
const typedExpressRes = expressRes;
|
|
61
|
+
typedExpressRes.respond = (status, dataForResponse) => {
|
|
62
|
+
// Use the passed apiDefinition object
|
|
63
|
+
const routeSchemaForHandler = apiDefinition.endpoints[currentDomain][currentRouteKey];
|
|
64
|
+
const responseSchemaForStatus = routeSchemaForHandler.responses[status];
|
|
65
|
+
if (!responseSchemaForStatus) {
|
|
66
|
+
console.error(`No response schema defined for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}`);
|
|
67
|
+
typedExpressRes.status(500).json({
|
|
68
|
+
// data: null, // data field might not be part of error schema for 500 if not using unified
|
|
69
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let responseBodyToValidate; // This will be the object { data: ..., error: ... }
|
|
74
|
+
if (status === 422) {
|
|
75
|
+
// For 422, dataForResponse is expected to be the UnifiedError array or null
|
|
76
|
+
// The schema for 422 is errorUnifiedResponseSchema, expecting { error: UnifiedError }
|
|
77
|
+
responseBodyToValidate = {
|
|
78
|
+
data: null, // data is null for 422
|
|
79
|
+
error: dataForResponse // dataForResponse should be UnifiedError for 422
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// For other statuses, dataForResponse is the actual data payload for the 'data' field
|
|
84
|
+
// The schema is createSuccessUnifiedResponseSchema, expecting { data: ActualData }
|
|
85
|
+
responseBodyToValidate = {
|
|
86
|
+
data: dataForResponse,
|
|
87
|
+
error: null // error is null for success
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Validate the constructed responseBodyToValidate against the full schema for that status
|
|
91
|
+
const validationResult = responseSchemaForStatus.safeParse(responseBodyToValidate);
|
|
92
|
+
if (validationResult.success) {
|
|
93
|
+
typedExpressRes.status(status).json(validationResult.data);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error(`FATAL: Constructed response body failed Zod validation for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}. This indicates an issue with respond logic or schemas.`, validationResult.error.errors);
|
|
97
|
+
console.error("Response body was:", responseBodyToValidate);
|
|
98
|
+
typedExpressRes.status(500).json({
|
|
99
|
+
// data: null,
|
|
100
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const specificHandlerFn = handler;
|
|
105
|
+
await specificHandlerFn(finalTypedReq, typedExpressRes);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
109
|
+
const mappedErrors = error.errors.map(err => {
|
|
110
|
+
let errorType = 'general';
|
|
111
|
+
const pathZero = String(err.path[0]); // Ensure pathZero is a string
|
|
112
|
+
if (pathZero === 'params')
|
|
113
|
+
errorType = 'param'; // Corrected: 'params' from path maps to 'param' type
|
|
114
|
+
else if (pathZero === 'query')
|
|
115
|
+
errorType = 'query';
|
|
116
|
+
else if (pathZero === 'body')
|
|
117
|
+
errorType = 'body';
|
|
118
|
+
return {
|
|
119
|
+
field: err.path.join('.') || 'request',
|
|
120
|
+
message: err.message,
|
|
121
|
+
type: errorType,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const errorResponseBody = { data: null, error: mappedErrors };
|
|
125
|
+
const schema422 = routeDefinition.responses[422];
|
|
126
|
+
if (schema422) {
|
|
127
|
+
const validationResult = schema422.safeParse(errorResponseBody);
|
|
128
|
+
if (validationResult.success) {
|
|
129
|
+
expressRes.status(422).json(validationResult.data);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.error("FATAL: Constructed 422 error response failed its own schema validation.", validationResult.error.errors);
|
|
133
|
+
expressRes.status(500).json({ error: [{ field: "general", type: "general", message: "Internal server error constructing validation error response." }] });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.error("Error: 422 schema not found for route, sending raw Zod errors.");
|
|
138
|
+
expressRes.status(422).json({ error: mappedErrors }); // Fallback
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (error instanceof Error) {
|
|
142
|
+
console.error(`Error in ${method} ${path}:`, error.message, error.stack);
|
|
143
|
+
expressRes.status(500).json({ error: [{ field: "general", type: "general", message: 'Internal server error' }] });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.error(`Unknown error in ${method} ${path}:`, error);
|
|
147
|
+
expressRes.status(500).json({ error: [{ field: "general", type: "general", message: 'An unknown error occurred' }] });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Create middleware wrappers that include endpoint information
|
|
152
|
+
const middlewareWrappers = [];
|
|
153
|
+
if (middlewares && middlewares.length > 0) {
|
|
154
|
+
middlewares.forEach(middleware => {
|
|
155
|
+
const wrappedMiddleware = async (req, res, next) => {
|
|
156
|
+
try {
|
|
157
|
+
await middleware(req, res, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
next(error);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
middlewareWrappers.push(wrappedMiddleware);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Register route with middlewares
|
|
167
|
+
const allHandlers = [...middlewareWrappers, expressMiddleware];
|
|
168
|
+
switch (method.toUpperCase()) {
|
|
169
|
+
case 'GET':
|
|
170
|
+
app.get(fullPath, ...allHandlers);
|
|
171
|
+
break;
|
|
172
|
+
case 'POST':
|
|
173
|
+
app.post(fullPath, ...allHandlers);
|
|
174
|
+
break;
|
|
175
|
+
case 'PUT':
|
|
176
|
+
app.put(fullPath, ...allHandlers);
|
|
177
|
+
break;
|
|
178
|
+
case 'DELETE':
|
|
179
|
+
app.delete(fullPath, ...allHandlers);
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
console.warn(`Unsupported HTTP method: ${method} for path ${fullPath}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return client_1.ApiClient; } });
|
|
6
|
+
Object.defineProperty(exports, "FetchHttpClientAdapter", { enumerable: true, get: function () { return client_1.FetchHttpClientAdapter; } });
|
|
7
|
+
var definition_1 = require("./definition");
|
|
8
|
+
Object.defineProperty(exports, "CreateApiDefinition", { enumerable: true, get: function () { return definition_1.CreateApiDefinition; } });
|
|
9
|
+
Object.defineProperty(exports, "CreateResponses", { enumerable: true, get: function () { return definition_1.CreateResponses; } });
|
|
10
|
+
var object_handlers_1 = require("./object-handlers");
|
|
11
|
+
Object.defineProperty(exports, "RegisterHandlers", { enumerable: true, get: function () { return object_handlers_1.RegisterHandlers; } });
|
|
12
|
+
var zod_1 = require("zod");
|
|
13
|
+
Object.defineProperty(exports, "ZodSchema", { enumerable: true, get: function () { return zod_1.z; } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { ApiDefinitionSchema } from "./definition";
|
|
3
|
+
import { TypedRequest, TypedResponse } from "./router";
|
|
4
|
+
export type EndpointMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction, endpointInfo: {
|
|
5
|
+
domain: string;
|
|
6
|
+
routeKey: string;
|
|
7
|
+
}) => void | Promise<void>;
|
|
8
|
+
type HandlerFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain]> = (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
9
|
+
export type ObjectHandlers<TDef extends ApiDefinitionSchema> = {
|
|
10
|
+
[TDomain in keyof TDef['endpoints']]: {
|
|
11
|
+
[TRouteKey in keyof TDef['endpoints'][TDomain]]: HandlerFunction<TDef, TDomain, TRouteKey>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function RegisterHandlers<TDef extends ApiDefinitionSchema>(app: express.Express, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef>, middlewares?: EndpointMiddleware[]): void;
|
|
15
|
+
export declare function makeObjectHandlerRegistrar<TDef extends ApiDefinitionSchema>(apiDefinition: TDef): (app: express.Express, objectHandlers: ObjectHandlers<TDef>, middlewares?: EndpointMiddleware[]) => void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RegisterHandlers = RegisterHandlers;
|
|
4
|
+
exports.makeObjectHandlerRegistrar = makeObjectHandlerRegistrar;
|
|
5
|
+
const handler_1 = require("./handler");
|
|
6
|
+
// Transform object-based handlers to array format
|
|
7
|
+
function transformObjectHandlersToArray(objectHandlers) {
|
|
8
|
+
const handlerArray = [];
|
|
9
|
+
// Iterate through domains
|
|
10
|
+
for (const domain in objectHandlers) {
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(objectHandlers, domain)) {
|
|
12
|
+
const domainHandlers = objectHandlers[domain];
|
|
13
|
+
// Iterate through routes in this domain
|
|
14
|
+
for (const routeKey in domainHandlers) {
|
|
15
|
+
if (Object.prototype.hasOwnProperty.call(domainHandlers, routeKey)) {
|
|
16
|
+
const handler = domainHandlers[routeKey];
|
|
17
|
+
// Create the handler object in the format expected by registerRouteHandlers
|
|
18
|
+
handlerArray.push({
|
|
19
|
+
domain,
|
|
20
|
+
routeKey,
|
|
21
|
+
handler
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return handlerArray;
|
|
28
|
+
}
|
|
29
|
+
// Main utility function that registers object-based handlers
|
|
30
|
+
function RegisterHandlers(app, apiDefinition, objectHandlers, middlewares) {
|
|
31
|
+
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
|
32
|
+
(0, handler_1.registerRouteHandlers)(app, apiDefinition, handlerArray, middlewares);
|
|
33
|
+
}
|
|
34
|
+
// Factory function to create a typed handler registrar for a specific API definition
|
|
35
|
+
function makeObjectHandlerRegistrar(apiDefinition) {
|
|
36
|
+
return function (app, objectHandlers, middlewares) {
|
|
37
|
+
RegisterHandlers(app, apiDefinition, objectHandlers, middlewares);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateOpenApiSpec = void 0;
|
|
4
|
+
const zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
// Extend Zod with OpenAPI capabilities
|
|
7
|
+
(0, zod_to_openapi_1.extendZodWithOpenApi)(zod_1.z);
|
|
8
|
+
function generateOpenApiSpec(definition) {
|
|
9
|
+
const registry = new zod_to_openapi_1.OpenAPIRegistry();
|
|
10
|
+
// Helper to convert Zod schema to OpenAPI schema component
|
|
11
|
+
function registerSchema(name, schema) {
|
|
12
|
+
try {
|
|
13
|
+
return registry.register(name, schema); // Cast to any to handle complex Zod types
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.warn(`Could not register schema ${name}: ${error.message}`);
|
|
17
|
+
// Fallback or simplified schema if registration fails
|
|
18
|
+
return registry.register(name, zod_1.z.object({}).openapi({ description: `Schema for ${name} (fallback due to registration error)` }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Function to convert Zod schema to OpenAPI Parameter Object or Request Body Object
|
|
22
|
+
function zodSchemaToOpenApiParameter(schema, inType) {
|
|
23
|
+
if (!schema || !(schema instanceof zod_1.z.ZodObject))
|
|
24
|
+
return []; // Ensure schema is a ZodObject
|
|
25
|
+
const shape = schema.shape;
|
|
26
|
+
return Object.entries(shape).map(([key, val]) => ({
|
|
27
|
+
name: key,
|
|
28
|
+
in: inType,
|
|
29
|
+
required: !val.isOptional(),
|
|
30
|
+
schema: registerSchema(`${inType}_${key}_${Date.now()}`, val),
|
|
31
|
+
description: val.description,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
function zodSchemaToOpenApiRequestBody(schema) {
|
|
35
|
+
if (!schema)
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
required: true,
|
|
39
|
+
content: {
|
|
40
|
+
'application/json': {
|
|
41
|
+
schema: registerSchema(`RequestBody_${Date.now()}`, schema), // Unique name
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Iterate over the API definition to register routes
|
|
47
|
+
Object.keys(definition).forEach(domainNameKey => {
|
|
48
|
+
// domainNameKey is a string, representing the domain like 'users', 'products'
|
|
49
|
+
const domain = definition[domainNameKey];
|
|
50
|
+
Object.keys(domain).forEach(routeNameKey => {
|
|
51
|
+
// routeNameKey is a string, representing the route name like 'getUser', 'createProduct'
|
|
52
|
+
const route = domain[routeNameKey];
|
|
53
|
+
const pathItem = {}; // Define PathItemObject structure
|
|
54
|
+
const parameters = [];
|
|
55
|
+
if (route.params) {
|
|
56
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.params, 'path'));
|
|
57
|
+
}
|
|
58
|
+
if (route.query) {
|
|
59
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.query, 'query'));
|
|
60
|
+
}
|
|
61
|
+
const requestBody = zodSchemaToOpenApiRequestBody(route.body);
|
|
62
|
+
const responses = {};
|
|
63
|
+
for (const statusCode in route.responses) {
|
|
64
|
+
const responseSchema = route.responses[parseInt(statusCode)];
|
|
65
|
+
if (responseSchema) {
|
|
66
|
+
responses[statusCode] = {
|
|
67
|
+
description: `Response for status code ${statusCode}`,
|
|
68
|
+
content: {
|
|
69
|
+
'application/json': {
|
|
70
|
+
schema: registerSchema(`Response_${statusCode}_${routeNameKey}_${domainNameKey}`, responseSchema),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Add 422 response if not already defined, as it's a default in createResponses
|
|
77
|
+
// Assuming route.responses[422] would exist if it's a standard part of the definition
|
|
78
|
+
if (!responses['422'] && route.responses && route.responses[422]) {
|
|
79
|
+
responses['422'] = {
|
|
80
|
+
description: 'Validation Error',
|
|
81
|
+
content: {
|
|
82
|
+
'application/json': {
|
|
83
|
+
schema: registerSchema(`Response_422_${routeNameKey}_${domainNameKey}`, route.responses[422]),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const operation = {
|
|
89
|
+
summary: `${domainNameKey} - ${routeNameKey}`,
|
|
90
|
+
tags: [domainNameKey],
|
|
91
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
92
|
+
requestBody: requestBody,
|
|
93
|
+
responses: responses,
|
|
94
|
+
};
|
|
95
|
+
// Register the route with the registry
|
|
96
|
+
// The path needs to be transformed from Express-style (:param) to OpenAPI-style ({param})
|
|
97
|
+
const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
|
|
98
|
+
registry.registerPath({
|
|
99
|
+
method: route.method.toLowerCase(),
|
|
100
|
+
path: openApiPath,
|
|
101
|
+
...operation,
|
|
102
|
+
// Add description or other OpenAPI fields if available in RouteSchema
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
// Generate the OpenAPI document
|
|
107
|
+
const generator = new zod_to_openapi_1.OpenApiGeneratorV3(registry.definitions);
|
|
108
|
+
const openApiDocument = generator.generateDocument({
|
|
109
|
+
openapi: '3.0.0',
|
|
110
|
+
info: {
|
|
111
|
+
title: 'My API',
|
|
112
|
+
version: '1.0.0',
|
|
113
|
+
description: 'Automatically generated OpenAPI specification',
|
|
114
|
+
},
|
|
115
|
+
servers: [{ url: '/api' }], // Adjust as needed
|
|
116
|
+
});
|
|
117
|
+
return openApiDocument;
|
|
118
|
+
}
|
|
119
|
+
exports.generateOpenApiSpec = generateOpenApiSpec;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z, ZodTypeAny, ZodType } from 'zod';
|
|
2
|
+
export declare class TsTypeMarker<T> {
|
|
3
|
+
readonly _isTsTypeMarker = true;
|
|
4
|
+
readonly _type: T;
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
export declare function CustomResponse<T>(): TsTypeMarker<T>;
|
|
8
|
+
type InputSchemaOrMarker = ZodTypeAny | TsTypeMarker<any>;
|
|
9
|
+
declare const unifiedErrorSchema: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
10
|
+
field: z.ZodString;
|
|
11
|
+
type: z.ZodEnum<["body", "query", "param", "general"]>;
|
|
12
|
+
message: z.ZodString;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
field: string;
|
|
15
|
+
type: "param" | "body" | "query" | "general";
|
|
16
|
+
message: string;
|
|
17
|
+
}, {
|
|
18
|
+
field: string;
|
|
19
|
+
type: "param" | "body" | "query" | "general";
|
|
20
|
+
message: string;
|
|
21
|
+
}>, "many">>;
|
|
22
|
+
export type UnifiedError = z.infer<typeof unifiedErrorSchema>;
|
|
23
|
+
declare const errorUnifiedResponseSchema: z.ZodObject<{
|
|
24
|
+
error: z.ZodEffects<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
25
|
+
field: z.ZodString;
|
|
26
|
+
type: z.ZodEnum<["body", "query", "param", "general"]>;
|
|
27
|
+
message: z.ZodString;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
field: string;
|
|
30
|
+
type: "param" | "body" | "query" | "general";
|
|
31
|
+
message: string;
|
|
32
|
+
}, {
|
|
33
|
+
field: string;
|
|
34
|
+
type: "param" | "body" | "query" | "general";
|
|
35
|
+
message: string;
|
|
36
|
+
}>, "many">>, {
|
|
37
|
+
field: string;
|
|
38
|
+
type: "param" | "body" | "query" | "general";
|
|
39
|
+
message: string;
|
|
40
|
+
}[] | null, {
|
|
41
|
+
field: string;
|
|
42
|
+
type: "param" | "body" | "query" | "general";
|
|
43
|
+
message: string;
|
|
44
|
+
}[] | null>;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
error: {
|
|
47
|
+
field: string;
|
|
48
|
+
type: "param" | "body" | "query" | "general";
|
|
49
|
+
message: string;
|
|
50
|
+
}[] | null;
|
|
51
|
+
}, {
|
|
52
|
+
error: {
|
|
53
|
+
field: string;
|
|
54
|
+
type: "param" | "body" | "query" | "general";
|
|
55
|
+
message: string;
|
|
56
|
+
}[] | null;
|
|
57
|
+
}>;
|
|
58
|
+
export declare const HttpSuccessCodes: readonly [200, 201, 202, 204];
|
|
59
|
+
export declare const HttpClientErrorCodes: readonly [400, 401, 403, 404, 409];
|
|
60
|
+
export declare const HttpServerErrorCodes: readonly [500];
|
|
61
|
+
export type HttpSuccessStatusCode = typeof HttpSuccessCodes[number];
|
|
62
|
+
export type HttpClientErrorStatusCode = typeof HttpClientErrorCodes[number];
|
|
63
|
+
export type HttpServerErrorStatusCode = typeof HttpServerErrorCodes[number];
|
|
64
|
+
export type AllowedInputStatusCode = HttpSuccessStatusCode | HttpClientErrorStatusCode | HttpServerErrorStatusCode;
|
|
65
|
+
export type AllowedResponseStatusCode = AllowedInputStatusCode | 422;
|
|
66
|
+
type CreateResponsesReturnType<InputSchemas extends Partial<Record<AllowedInputStatusCode, InputSchemaOrMarker>>> = {
|
|
67
|
+
[KStatus in keyof InputSchemas]: InputSchemas[KStatus] extends TsTypeMarker<infer T> ? z.ZodObject<{
|
|
68
|
+
data: ZodType<T, z.ZodTypeDef, T>;
|
|
69
|
+
}> : InputSchemas[KStatus] extends ZodTypeAny ? z.ZodObject<{
|
|
70
|
+
data: InputSchemas[KStatus];
|
|
71
|
+
}> : never;
|
|
72
|
+
} & {
|
|
73
|
+
422: typeof errorUnifiedResponseSchema;
|
|
74
|
+
};
|
|
75
|
+
export declare function createResponses<TInputMap extends Partial<Record<AllowedInputStatusCode, InputSchemaOrMarker>>>(schemas: TInputMap): CreateResponsesReturnType<TInputMap>;
|
|
76
|
+
export interface RouteSchema {
|
|
77
|
+
path: string;
|
|
78
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
|
|
79
|
+
params?: ZodTypeAny;
|
|
80
|
+
query?: ZodTypeAny;
|
|
81
|
+
body?: ZodTypeAny;
|
|
82
|
+
responses: Record<number, ZodTypeAny>;
|
|
83
|
+
}
|
|
84
|
+
export type ApiDefinitionSchema = Record<string, Record<string, RouteSchema>>;
|
|
85
|
+
export declare function createApiDefinition<T extends ApiDefinitionSchema>(definition: T): T;
|
|
86
|
+
export type ApiRouteKey<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef> = keyof TDef[TDomain];
|
|
87
|
+
export type ApiRoute<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName];
|
|
88
|
+
export type InferDataFromUnifiedResponse<S extends ZodTypeAny> = S extends z.ZodVoid ? void : z.infer<S> extends {
|
|
89
|
+
data: infer D;
|
|
90
|
+
} ? D extends null ? null : D extends (infer ActualD | null) ? ActualD extends void ? void : ActualD : D : never;
|
|
91
|
+
export type ApiResponse<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
92
|
+
responses: infer R;
|
|
93
|
+
} ? R extends {
|
|
94
|
+
200: infer R200 extends ZodTypeAny;
|
|
95
|
+
} ? InferDataFromUnifiedResponse<R200> : R extends {
|
|
96
|
+
201: infer R201 extends ZodTypeAny;
|
|
97
|
+
} ? InferDataFromUnifiedResponse<R201> : R extends {
|
|
98
|
+
204: infer R204 extends ZodTypeAny;
|
|
99
|
+
} ? InferDataFromUnifiedResponse<R204> : any : any;
|
|
100
|
+
export type ApiBody<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
101
|
+
body: infer B extends z.ZodTypeAny;
|
|
102
|
+
} ? z.infer<B> : Record<string, any>;
|
|
103
|
+
export type ApiParams<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
104
|
+
params: infer P extends z.ZodTypeAny;
|
|
105
|
+
} ? z.infer<P> : Record<string, any>;
|
|
106
|
+
export type ApiQuery<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
107
|
+
query: infer Q extends z.ZodTypeAny;
|
|
108
|
+
} ? z.infer<Q> : Record<string, any>;
|
|
109
|
+
export type ApiClientBody<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
110
|
+
body: infer B extends ZodTypeAny;
|
|
111
|
+
} ? z.input<B> : undefined;
|
|
112
|
+
export type ApiClientParams<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
113
|
+
params: infer P extends ZodTypeAny;
|
|
114
|
+
} ? z.input<P> : undefined;
|
|
115
|
+
export type ApiClientQuery<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef[TDomain][TRouteName] extends {
|
|
116
|
+
query: infer Q extends ZodTypeAny;
|
|
117
|
+
} ? z.input<Q> : undefined;
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiDefinition = exports.createResponses = exports.HttpServerErrorCodes = exports.HttpClientErrorCodes = exports.HttpSuccessCodes = exports.CustomResponse = exports.TsTypeMarker = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// Marker class for raw TypeScript types
|
|
6
|
+
class TsTypeMarker {
|
|
7
|
+
_isTsTypeMarker = true;
|
|
8
|
+
_type; // Phantom type, used for inference
|
|
9
|
+
constructor() {
|
|
10
|
+
// This constructor doesn't need to do anything with T at runtime.
|
|
11
|
+
// T is purely a compile-time construct.
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.TsTypeMarker = TsTypeMarker;
|
|
15
|
+
// Helper function to create a TsTypeMarker instance
|
|
16
|
+
function CustomResponse() {
|
|
17
|
+
return new TsTypeMarker();
|
|
18
|
+
}
|
|
19
|
+
exports.CustomResponse = CustomResponse;
|
|
20
|
+
// Define the structure for error details
|
|
21
|
+
const errorDetailSchema = zod_1.z.object({
|
|
22
|
+
field: zod_1.z.string(),
|
|
23
|
+
type: zod_1.z.enum(['body', 'query', 'param', 'general']),
|
|
24
|
+
message: zod_1.z.string(),
|
|
25
|
+
});
|
|
26
|
+
// Define the schema for the error list
|
|
27
|
+
const unifiedErrorSchema = zod_1.z.array(errorDetailSchema).nullable(); // Nullable if no errors
|
|
28
|
+
// Helper function to create the success-specific unified response schema
|
|
29
|
+
// This wraps the original data schema with a 'data' field and sets 'error' to null.
|
|
30
|
+
function createSuccessUnifiedResponseSchema(dataSchema) {
|
|
31
|
+
return zod_1.z.object({
|
|
32
|
+
data: dataSchema, // Data is present as per dataSchema (can be nullable if dataSchema itself is)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Schema for error responses (e.g., 422 Validation Error)
|
|
36
|
+
// Ensures 'data' is null and 'error' is populated.
|
|
37
|
+
const errorUnifiedResponseSchema = zod_1.z.object({
|
|
38
|
+
error: unifiedErrorSchema.refine(val => val !== null, { message: "Error list cannot be null for errorUnifiedResponseSchema" }), // Error list is mandatory
|
|
39
|
+
});
|
|
40
|
+
// Define allowed HTTP status codes
|
|
41
|
+
exports.HttpSuccessCodes = [200, 201, 202, 204];
|
|
42
|
+
exports.HttpClientErrorCodes = [400, 401, 403, 404, 409]; // 422 is handled separately
|
|
43
|
+
exports.HttpServerErrorCodes = [500];
|
|
44
|
+
// Helper function to create response schemas with unified structure and default 422 error
|
|
45
|
+
// Schemas input is now constrained to use AllowedInputStatusCode as keys.
|
|
46
|
+
function createResponses(schemas) {
|
|
47
|
+
const builtResult = {}; // Using any for intermediate dynamic construction.
|
|
48
|
+
for (const stringStatusKey in schemas) {
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(schemas, stringStatusKey)) {
|
|
50
|
+
const numericKey = parseInt(stringStatusKey);
|
|
51
|
+
const schemaOrMarker = schemas[numericKey];
|
|
52
|
+
if (schemaOrMarker) { // Check if schemaOrMarker is defined (due to Partial)
|
|
53
|
+
if (schemaOrMarker instanceof TsTypeMarker) {
|
|
54
|
+
// For TsTypeMarker, create a ZodObject with data typed as z.any() at runtime.
|
|
55
|
+
// The actual type T is carried by CreateResponsesReturnType for compile-time inference.
|
|
56
|
+
builtResult[numericKey] = zod_1.z.object({
|
|
57
|
+
data: zod_1.z.any(), // Runtime placeholder
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (schemaOrMarker instanceof zod_1.ZodType) { // It's a Zod schema
|
|
61
|
+
builtResult[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
|
|
62
|
+
}
|
|
63
|
+
// Note: If schemaOrMarker is something else, it would be a type error
|
|
64
|
+
// based on InputSchemaOrMarker, or this runtime check would skip it.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Always set/overwrite the 422 response to use errorUnifiedResponseSchema
|
|
69
|
+
builtResult[422] = errorUnifiedResponseSchema;
|
|
70
|
+
// Cast to the more specific return type at the end.
|
|
71
|
+
// This is safe if builtResult's structure matches CreateResponsesReturnType<TInputMap>.
|
|
72
|
+
return builtResult;
|
|
73
|
+
}
|
|
74
|
+
exports.createResponses = createResponses;
|
|
75
|
+
// Helper function to ensure the definition conforms to ApiDefinitionSchema
|
|
76
|
+
// while preserving the literal types of the passed object.
|
|
77
|
+
function createApiDefinition(definition) {
|
|
78
|
+
return definition;
|
|
79
|
+
}
|
|
80
|
+
exports.createApiDefinition = createApiDefinition;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { ApiDefinitionSchema, ApiBody, ApiParams, ApiQuery, InferDataFromUnifiedResponse } from './definition';
|
|
3
|
+
export interface TypedRequest<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteKey extends keyof TDef[TDomain], // Using direct keyof for simplicity here
|
|
4
|
+
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> extends express.Request<P, any, ReqBody, Q, L> {
|
|
5
|
+
}
|
|
6
|
+
type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends keyof TDef[TDomain], TStatus extends keyof TDef[TDomain][TRouteName]['responses'] & number> = InferDataFromUnifiedResponse<TDef[TDomain][TRouteName]['responses'][TStatus]>;
|
|
7
|
+
type RespondFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends keyof TDef[TDomain]> = <TStatusLocal extends keyof TDef[TDomain][TRouteName]['responses'] & number>(status: TStatusLocal, data: ResponseDataForStatus<TDef, TDomain, TRouteName, TStatusLocal>) => void;
|
|
8
|
+
export interface TypedResponse<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteName extends keyof TDef[TDomain], L extends Record<string, any> = Record<string, any>> extends express.Response<any, L> {
|
|
9
|
+
respond: RespondFunction<TDef, TDomain, TRouteName>;
|
|
10
|
+
json: <B = any>(body: B) => this;
|
|
11
|
+
}
|
|
12
|
+
export declare function createRouteHandler<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef, TRouteKey extends keyof TDef[TDomain]>(domain: TDomain, routeKey: TRouteKey, handler: (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void): {
|
|
13
|
+
domain: TDomain;
|
|
14
|
+
routeKey: TRouteKey;
|
|
15
|
+
handler: (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
16
|
+
};
|
|
17
|
+
export declare function makeRouteHandlerCreator<TDef extends ApiDefinitionSchema>(): <TDomain extends keyof TDef, TRouteKey extends keyof TDef[TDomain]>(domain: TDomain, routeKey: TRouteKey, handler: (req: TypedRequest<TDef, TDomain, TRouteKey, ApiParams<TDef, TDomain, TRouteKey>, ApiBody<TDef, TDomain, TRouteKey>, ApiQuery<TDef, TDomain, TRouteKey>, Record<string, any>>, res: TypedResponse<TDef, TDomain, TRouteKey, Record<string, any>>) => Promise<void> | void) => {
|
|
18
|
+
domain: TDomain;
|
|
19
|
+
routeKey: TRouteKey;
|
|
20
|
+
handler: (req: TypedRequest<TDef, TDomain, TRouteKey, ApiParams<TDef, TDomain, TRouteKey>, ApiBody<TDef, TDomain, TRouteKey>, ApiQuery<TDef, TDomain, TRouteKey>, Record<string, any>>, res: TypedResponse<TDef, TDomain, TRouteKey, Record<string, any>>) => void | Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export type SpecificRouteHandler<TDef extends ApiDefinitionSchema> = {
|
|
23
|
+
[TDomain_ in keyof TDef]: {
|
|
24
|
+
[TRouteKey_ in keyof TDef[TDomain_]]: ReturnType<typeof createRouteHandler<TDef, TDomain_, TRouteKey_>>;
|
|
25
|
+
}[keyof TDef[TDomain_]];
|
|
26
|
+
}[keyof TDef];
|
|
27
|
+
export declare function registerRouteHandlers<TDef extends ApiDefinitionSchema>(app: express.Express, apiDefinition: TDef, // Pass the actual API definition object
|
|
28
|
+
routeHandlers: Array<SpecificRouteHandler<TDef>>): void;
|
|
29
|
+
export {};
|