Skip to content
Open
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
40 changes: 32 additions & 8 deletions packages/cli/src/typegen/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,25 @@ export type Property<T, K extends keyof any, D = unknown> =
export type ValueOf<T, D = never> = T extends Record<string, unknown> ? T[keyof T] : D;

export type EmptyObject = Record<never, never>;

export type StatusCode<Status> =
Status extends '4XX' ? FourXX :
Status extends '5XX' ? FiveXX :
Status extends number ? Status :
Status extends \`\${infer N extends number}\` ? N :
never;

export type FourXX = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 |
410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 |
422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451;

export type FiveXX = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511;
`;

export const requests = `
import {Params, Request, Response} from '@openapi-ts/request-types';
import {Params, Request} from '@openapi-ts/request-types';
import {operations} from './spec';
import {EmptyObject, Property, ValueOf} from './utils';
import {EmptyObject, Property, ValueOf, StatusCode} from './utils';

export type RequestBody<OperationId extends keyof operations> =
operations[OperationId] extends {requestBody: Record<string, any>} ?
Expand All @@ -38,8 +51,23 @@ export type RequestQuery<OperationId extends keyof operations> =
export type RequestHeaders<OperationId extends keyof operations> =
Property<Property<operations[OperationId], 'parameters', EmptyObject>, 'header', EmptyObject>;

export type ResponseBody<OperationId extends keyof operations> =
ValueOf<Property<ValueOf<Property<operations[OperationId], 'responses', EmptyObject>>, 'content'>, void>;
export type OperationResponse<OpName extends keyof operations> = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps stay consistent with naming OpName OperationId instead?

[Status in keyof operations[OpName]['responses']]:
operations[OpName]['responses'][Status] extends {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matter of style, but personally I prefer to name keys in mapped types P or K or similar to make it clear, but no big deal.
I would perhaps also consider extracting a type alias such as:

type OperationResponseMap<OperationId extends keyof operations> = operations[OperationId]['responses'];

... to also make it clearer since this is duplicated several times.

content: infer Content;
}
? {
[CT in keyof Content]: {
statusCode: StatusCode<Status>;
headers: { 'Content-Type': CT };
body: Content[CT];
}
}[keyof Content]
: {
statusCode: StatusCode<Status>;
headers: EmptyObject;
};
}[keyof operations[OpName]['responses']]

export type ResponseHeaders<OperationId extends keyof operations> =
Property<ValueOf<Property<operations[OperationId], 'responses', EmptyObject>>, 'headers'>;
Expand All @@ -49,10 +77,6 @@ export type OperationRequest<OperationId extends keyof operations> = Request<
Params & RequestPathParams<OperationId>,
Params & RequestQuery<OperationId>,
Params & RequestHeaders<OperationId>>;

export type OperationResponse<OperationId extends keyof operations> = Response<
ResponseBody<OperationId>,
Params & ResponseHeaders<OperationId>>;
`;

export const handlers = `
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './types';
export * from './errors';
export * from './openapi';
export * from './response';
6 changes: 4 additions & 2 deletions packages/lib/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,10 @@ export class OpenApi<T> {

// Note: The handler function may modify the "res" object and/or return a response body.
// If "res.body" is undefined we use the return value as the body.
const resBody = await operationHandler(req, res, operationParams);
res.body = res.body ?? resBody;
const returnedResponse = await operationHandler(req, res, operationParams);
if (returnedResponse !== undefined) {
Object.assign(res, returnedResponse);
}

// If status code is not specified and a non-ambiguous default status code is available, use it
res.statusCode = res.statusCode ?? this.getDefaultStatusCode(operation);
Expand Down
16 changes: 16 additions & 0 deletions packages/lib/src/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function json<T>(body: T): { statusCode: 200; body: T; headers: { "Content-Type": "application/json" } };
export function json<T, CT extends number>(body: T, status: CT): { statusCode: CT; body: T; headers: { "Content-Type": "application/json" } };

export function json<T, CT extends number = 200>(
body: T,
status?: CT
): {statusCode: CT | 200; body: T; headers: { "Content-Type": "application/json" } } {
const statusCode = status ?? 200
return {
statusCode,
body,
headers: {
"Content-Type": "application/json",
},
}
}
5 changes: 2 additions & 3 deletions packages/lib/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ export type RequestHandler<P = unknown,
Req extends Request = Request,
Res extends Response = Response> = (
req: Req,
res: Res,
params: P) => Awaitable<Res['body'] | void>;

res: Response,
params: P) => Awaitable<Res | void>;

type SecuritySchemeObject = OpenAPIV3_1.SecuritySchemeObject | OpenAPIV3.SecuritySchemeObject;
/**
Expand Down
31 changes: 31 additions & 0 deletions packages/test/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ paths:
schema:
type: object
additionalProperties: true
'/multiresponse':
get:
operationId: multiResponse
summary: test multiple possible responses
parameters:
- in: query
name: type
schema:
type: string

responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
okBody:
type: number
text/html:
schema:
type: string

'404':
description: Not found
content:
application/json:
schema:
type: object




2 changes: 1 addition & 1 deletion packages/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "test",
"scripts": {
"pretest": "npm -w ../lib run build && openapi-ts-backend generate-types api.yml src/gen",
"test": "LOG_LEVEL=${LOG_LEVEL:=error} jest"
"test": "tsc && LOG_LEVEL=${LOG_LEVEL:=error} jest"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why should the test script implicitly compile? Not that it hurts, but seems a bit unnecessary. Or at least move it to pretest?

(Or is ts-jest not used so jest runs on the transpiled files? Too long since I worked on this so I don't remember. If so perhaps a better solution is to fix the jest setup?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it because the tests would also pass when there were typescript errors between the handler implemented in the test and the generated code. But you are right in that calling tsc here manually seems random and like a crutch.

},
"devDependencies": {
"@openapi-ts/cli": "*",
Expand Down
34 changes: 27 additions & 7 deletions packages/test/src/openapi.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {HttpError, OpenApi} from "@openapi-ts/backend";
import {HttpError, json, OpenApi} from "@openapi-ts/backend";
import { OperationHandlers} from './gen';

function greet(title: string, name: string): string {
Expand All @@ -13,23 +13,43 @@ const operations: OperationHandlers<unknown> = {
greet: req => {
const {params: {name}, query: {title = ''}} = req;

return {
message: greet(title, name),
};
return json({ message: greet(title, name)})
},
addPerson: req => {
return req.body.person;
return json(req.body.person, 201);
},
getTypes: req => {
return {
return json({
params: getTypeMap(req.params),
headers: getTypeMap(req.headers),
query: getTypeMap(req.query),
cookies: getTypeMap(req.cookies),
}
})
},
deletePerson: () => {
return;
},
multiResponse: (req) => {
if (req.query.type === '') {
return {
statusCode: 404,
body: {},
headers: {
'Content-Type': 'application/json',
'Additional-Header': 'somethingelse'
}
}
}
if (req.query.type === 'html') {
return {
statusCode: 200,
body: 'htmlContent',
headers: {
'Content-Type': 'text/html'
}
}
}
return json({ okBody: 1337 })
}
};

Expand Down
3 changes: 2 additions & 1 deletion packages/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"noEmit": true,
"noEmit": true
},
"exclude": ["jest.config.ts"]
}