Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/openapi-ts-router/express/petstore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"devDependencies": {
"@blgc/config": "workspace:*",
"@types/express": "^5.0.3",
"@types/node": "^22.15.30",
"@types/node": "^24.1.0",
"nodemon": "^3.1.10",
"openapi-typescript": "^7.8.0",
"ts-node": "^10.9.2",
Expand Down
2 changes: 1 addition & 1 deletion examples/openapi-ts-router/express/petstore/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const port = 3000;

app.use(express.json());

app.get('/*', router);
app.use(router);

app.use(invalidPathMiddleware);
app.use(errorMiddleware);
Expand Down
4 changes: 2 additions & 2 deletions examples/openapi-ts-router/express/petstore/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ openApiRouter.get('/pet/{petId}', {
}
],
handler: (req, res) => {
const { petId } = req.params;
console.log('handler');
const { petId } = req.valid.params;
console.log('handler', petId, typeof petId);

res.send({
name: 'Falko',
Expand Down
2 changes: 1 addition & 1 deletion examples/openapi-ts-router/hono/petstore/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ openApiRouter.get('/pet/{petId}', {
],
handler: (c) => {
const { petId } = c.req.valid('param');
console.log('handler');
console.log('handler', petId, typeof petId);

return c.json({
name: 'Falko',
Expand Down
2 changes: 1 addition & 1 deletion packages/feature-state/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "feature-state",
"version": "0.0.56",
"version": "0.0.57",
"private": false,
"description": "Straightforward, typesafe, and feature-based state management library for ReactJs",
"keywords": [],
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ openApiRouter.get('/pet/{petId}', {
})
),
handler: (req, res) => {
const { petId } = req.params; // Access validated params
const { petId } = req.valid.params; // Access parsed & validated params
res.send({ name: 'Falko', photoUrls: [] });
}
});
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts-router/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-ts-router",
"version": "0.2.14",
"version": "0.3.2",
"private": false,
"description": "Thin wrapper around the router of web frameworks like Express and Hono, offering OpenAPI typesafety and seamless integration with validation libraries such as Valibot and Zod",
"keywords": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { TEnforceFeatureConstraint, TFeatureDefinition } from '@blgc/types/features';
import { type TOperationPathParams, type TOperationQueryParams } from '@blgc/types/openapi';
import type * as express from 'express';
import { type ParamsDictionary } from 'express-serve-static-core';
import { createValidationContext, type TValidationError } from 'validation-adapter';
import { ValidationError } from '../../exceptions';
import { formatPath, parseParams } from '../../helper';
import { formatPath, parseParams, ValidationError } from '../../lib';
import {
TOpenApiExpressFeature,
TOpenApiExpressParsedData,
TParams,
type TOpenApiExpressParamsParserOptions,
type TOpenApiExpressRequest,
type TOpenApiExpressValidators,
type TOpenApiRouter,
type TParams
type TOpenApiRouter
} from '../../types';

export function withExpress<GPaths extends object, GFeatures extends TFeatureDefinition[]>(
Expand Down Expand Up @@ -77,7 +77,7 @@ export function withExpress<GPaths extends object, GFeatures extends TFeatureDef
>;
}

function parseParamsMiddleware(
function parseParamsMiddleware<GPathOperation>(
paramsParser: TOpenApiExpressParamsParserOptions = {}
): express.RequestHandler {
const {
Expand All @@ -88,23 +88,19 @@ function parseParamsMiddleware(
parseQueryParamsBlacklist
} = paramsParser;

if (shouldParseParams) {
return (req, _res, next) => {
// Extend Express query params & path params parsing to handle numbers and booleans
// as primitive type instead of string.
// See: https://expressjs.com/en/5x/api.html#req.query
// https://github.com/ljharb/qs/issues/91
req.query = parseQueryParams(req.query as TParams, parseQueryParamsBlacklist) as TParams;
req.params = parsePathParams(
req.params as TParams,
parsePathParamsBlacklist
) as ParamsDictionary;

next();
};
}

return (_req, _res, next) => {
return (req, _res, next) => {
if (shouldParseParams) {
(req as TOpenApiExpressRequest<GPathOperation>).valid = {
query: parseQueryParams(
req.query as TParams,
parseQueryParamsBlacklist
) as TOperationQueryParams<GPathOperation>,
params: parsePathParams(
req.params as TParams,
parsePathParamsBlacklist
) as TOperationPathParams<GPathOperation>
} as TOpenApiExpressParsedData<GPathOperation>;
}
next();
};
}
Expand All @@ -128,8 +124,10 @@ function validationMiddleware<GPathOperation>(
}

if (pathValidator != null) {
const pathParams =
(req as TOpenApiExpressRequest<GPathOperation>).valid?.params ?? req.params;
const pathValidationContext = createValidationContext<TOperationPathParams<GPathOperation>>(
req.params as TOperationPathParams<GPathOperation>
pathParams as TOperationPathParams<GPathOperation>
);
await pathValidator.validate(pathValidationContext);
for (const error of pathValidationContext.errors) {
Expand All @@ -139,9 +137,11 @@ function validationMiddleware<GPathOperation>(
}

if (queryValidator != null) {
const queryParams =
(req as TOpenApiExpressRequest<GPathOperation>).valid?.query ?? req.query;
const queryValidationContext = createValidationContext<
TOperationQueryParams<GPathOperation>
>(req.query as TOperationQueryParams<GPathOperation>);
>(queryParams as TOperationQueryParams<GPathOperation>);
await queryValidator.validate(queryValidationContext);
for (const error of queryValidationContext.errors) {
error['source'] = 'query';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { type TOperationPathParams, type TOperationQueryParams } from '@blgc/typ
import { type Hono } from 'hono';
import type * as hono from 'hono/types';
import { createValidationContext, type TValidationError } from 'validation-adapter';
import { ValidationError } from '../../exceptions';
import { formatPath, parseParams } from '../../helper';
import { formatPath, parseParams, ValidationError } from '../../lib';
import {
TOpenApiHonoFeature,
type TOpenApiHonoParamsParserOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-ts-router/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './create-openapi-router';
export * from './exceptions';
export * from './features';
export * from './lib';
export * from './types';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// "/users/{userId}/books/{bookId}" -> "/users/:userId/books/:bookId"
export function formatPath(path: string): string {
return path.replace(/\{(?<name>\w+)\}/g, ':$<name>');
return path.replace(/\{(\w+)\}/g, ':$1');
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './exceptions';
export * from './format-path';
export * from './parse-params';
33 changes: 20 additions & 13 deletions packages/openapi-ts-router/src/types/features/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,15 @@ export type TOpenApiExpressRequestHandler<GPathOperation> = (
) => Promise<void> | void;

export type TOpenApiExpressRequest<GPathOperation> = express.Request<
TOpenApiExpressPathParams<GPathOperation>, // Params
core.ParamsDictionary, // Params
TOperationSuccessResponseContent<GPathOperation>, // ResBody
TOpenApiExpressRequestBody<GPathOperation>, // ReqBody
TOpenApiExpressQueryParams<GPathOperation> // ReqQuery
>;

export type TOpenApiExpressQueryParams<GPathOperation> =
TOperationQueryParams<GPathOperation> extends never
? core.Query
: TOperationQueryParams<GPathOperation>;

export type TOpenApiExpressPathParams<GPathOperation> =
TOperationPathParams<GPathOperation> extends never
? core.ParamsDictionary
: TOperationPathParams<GPathOperation>;
core.Query // ReqQuery
> & {
// Express 5: req.query/req.params are read-only -> parsed & validated params stored here
// https://expressjs.com/en/api.html#req.params
valid: TOpenApiExpressParsedData<GPathOperation>;
};

export type TOpenApiExpressRequestBody<GPathOperation> = TRequestBody<GPathOperation>;

Expand All @@ -97,6 +91,19 @@ export type TOpenApiExpressResponse<GPathOperation> = express.Response<
express.Locals
>;

export type TOpenApiExpressParsedData<GPathOperation> = TOpenApiExpressParsedQuery<GPathOperation> &
TOpenApiExpressParsedParams<GPathOperation>;

export type TOpenApiExpressParsedQuery<GPathOperation> =
TOperationQueryParams<GPathOperation> extends never
? { query?: never }
: { query: TOperationQueryParams<GPathOperation> };

export type TOpenApiExpressParsedParams<GPathOperation> =
TOperationPathParams<GPathOperation> extends never
? { params?: never }
: { params: TOperationPathParams<GPathOperation> };

// =============================================================================
// Router Options
// =============================================================================
Expand Down
Loading
Loading