Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.
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
34 changes: 16 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ For a procedure to support OpenAPI the following _must_ be true:
- Query `input` parsers extend `ZodObject<{ [string]: ZodString }>` or `ZodVoid`.
- Mutation `input` parsers extend `ZodObject<{ [string]: ZodAnyType }>` or `ZodVoid`.
- `meta.openapi.enabled` is set to `true`.
- `meta.openapi.method` is `GET`, `DELETE` for query OR `POST`, `PUT` or `PATCH` for mutation.
- `meta.openapi.method` is `GET`, `POST`, `PATCH`, `PUT` or `DELETE`.
- `meta.openapi.path` is a string starting with `/`.
- `meta.openapi.path` parameters exist in `input` parser as `ZodString`

Expand All @@ -116,17 +116,15 @@ Please note:

## HTTP Requests

Query procedures accept input via URL `query parameters`.

Mutation procedures accept input via the `request body` with a `application/json` content type.
Procedures with a `GET`/`DELETE` method will accept inputs via URL `query parameters`. Procedures with a `POST`/`PATCH`/`PUT` method will accept inputs via the `request body` with a `application/json` content type.

### Path parameters

Both queries & mutations can accept a set of their inputs via URL path parameters. You can add a path parameter to any OpenAPI enabled procedure by using curly brackets around an input name as a path segment in the `meta.openapi.path` field.
A procedure can accept a set of inputs via URL path parameters. You can add a path parameter to any OpenAPI enabled procedure by using curly brackets around an input name as a path segment in the `meta.openapi.path` field.

#### Query
#### Query parameters

Query (& path parameter) inputs are always accepted as a `string`, if you wish to support other primitives such as `number`, `boolean`, `Date` etc. please use [`z.preprocess()`](https://github.com/colinhacks/zod#preprocess).
Query & path parameter inputs are always accepted as a `string`, if you wish to support other primitives such as `number`, `boolean`, `Date` etc. please use [`z.preprocess()`](https://github.com/colinhacks/zod#preprocess).

```typescript
// Router
Expand All @@ -146,7 +144,7 @@ const res = await fetch('http://localhost:3000/say-hello/James?greeting=Hello' /
const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */
```

#### Mutation
#### Request body

```typescript
// Router
Expand Down Expand Up @@ -310,16 +308,16 @@ Please see [full typings here](src/generator/index.ts).

Please see [full typings here](src/types.ts).

| Property | Type | Description | Required | Default |
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | ----------- |
| `enabled` | `boolean` | Exposes this procedure to `trpc-openapi` adapters and on the OpenAPI document. | `true` | `false` |
| `method` | `HttpMethod` | Method this endpoint is exposed on. Value can be `GET`/`DELETE` for queries OR `POST`/`PUT`/`PATCH` for mutations. | `true` | `undefined` |
| `path` | `string` | Pathname this endpoint is exposed on. Value must start with `/`, specify path parameters using `{}`. | `true` | `undefined` |
| `protect` | `boolean` | Requires this endpoint to use an `Authorization` header credential with `Bearer` scheme on OpenAPI document. | `false` | `false` |
| `summary` | `string` | A short summary of the endpoint included in the OpenAPI document. | `false` | `undefined` |
| `description` | `string` | A verbose description of the endpoint included in the OpenAPI document. | `false` | `undefined` |
| `tags` | `string[]` | A list of tags used for logical grouping of endpoints in the OpenAPI document. | `false` | `undefined` |
| `headers` | `ParameterObject[]` | An array of custom headers to add for this endpoint in the OpenAPI document. | `false` | `undefined` |
| Property | Type | Description | Required | Default |
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------ | -------- | ----------- |
| `enabled` | `boolean` | Exposes this procedure to `trpc-openapi` adapters and on the OpenAPI document. | `true` | `false` |
| `method` | `HttpMethod` | HTTP method this endpoint is exposed on. Value can be `GET`, `POST`, `PATCH`, `PUT` or `DELETE`. | `true` | `undefined` |
| `path` | `string` | Pathname this endpoint is exposed on. Value must start with `/`, specify path parameters using `{}`. | `true` | `undefined` |
| `protect` | `boolean` | Requires this endpoint to use an `Authorization` header credential with `Bearer` scheme on OpenAPI document. | `false` | `false` |
| `summary` | `string` | A short summary of the endpoint included in the OpenAPI document. | `false` | `undefined` |
| `description` | `string` | A verbose description of the endpoint included in the OpenAPI document. | `false` | `undefined` |
| `tags` | `string[]` | A list of tags used for logical grouping of endpoints in the OpenAPI document. | `false` | `undefined` |
| `headers` | `ParameterObject[]` | An array of custom headers to add for this endpoint in the OpenAPI document. | `false` | `undefined` |

#### CreateOpenApiNodeHttpHandlerOptions

Expand Down
2 changes: 1 addition & 1 deletion examples/with-express/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ const postsProtectedRouter = createProtectedRouter()
return { post };
},
})
.query('deletePostById', {
.mutation('deletePostById', {
meta: {
openapi: {
enabled: true,
Expand Down
2 changes: 1 addition & 1 deletion examples/with-nextjs/src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const postsProtectedRouter = createProtectedRouter()
return { post };
},
})
.query('deletePostById', {
.mutation('deletePostById', {
meta: {
openapi: {
enabled: true,
Expand Down
109 changes: 43 additions & 66 deletions src/generator/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,55 @@ import { OpenAPIV3 } from 'openapi-types';

import { OpenApiRouter } from '../types';
import { getPathParameters, normalizePath } from '../utils/path';
import { forEachOpenApiProcedure, getInputOutputParsers } from '../utils/procedure';
import {
forEachOpenApiProcedure,
getInputOutputParsers,
mergeProcedureRecords,
} from '../utils/procedure';
import { getParameterObjects, getRequestBodyObject, getResponsesObject } from './schema';

const acceptsRequestBody = (httpMethod: OpenAPIV3.HttpMethods) => {
if (httpMethod === 'get' || httpMethod === 'delete') {
return false;
}
return true;
};

export const getOpenApiPathsObject = (
appRouter: OpenApiRouter,
pathsObject: OpenAPIV3.PathsObject,
): OpenAPIV3.PathsObject => {
const { queries, mutations, subscriptions } = appRouter._def;

forEachOpenApiProcedure(queries, ({ path: queryPath, procedure, openapi }) => {
forEachOpenApiProcedure(subscriptions, ({ path: subscriptionPath }) => {
try {
const { method, protect, summary, description, tags, tag, headers } = openapi;
if (method !== 'GET' && method !== 'DELETE') {
throw new TRPCError({
message: 'Query method must be GET or DELETE',
code: 'INTERNAL_SERVER_ERROR',
});
}

const path = normalizePath(openapi.path);
const pathParameters = getPathParameters(path);
const headerParameters = headers?.map((header) => ({ ...header, in: 'header' })) || [];
const httpMethod = OpenAPIV3.HttpMethods[method];
if (pathsObject[path]?.[httpMethod]) {
throw new TRPCError({
message: `Duplicate procedure defined for route ${method} ${path}`,
code: 'INTERNAL_SERVER_ERROR',
});
}

const { inputParser, outputParser } = getInputOutputParsers(procedure);

pathsObject[path] = {
...pathsObject[path],
[httpMethod]: {
operationId: queryPath,
summary,
description,
tags: tags ?? (tag ? [tag] : undefined),
security: protect ? [{ Authorization: [] }] : undefined,
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'all') || []),
],
responses: getResponsesObject(outputParser),
},
};
throw new TRPCError({
message: 'Subscriptions are not supported by OpenAPI v3',
code: 'INTERNAL_SERVER_ERROR',
});
} catch (error: any) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
error.message = `[query.${queryPath}] - ${error.message}`;
error.message = `[subscription.${subscriptionPath}] - ${error.message}`;
throw error;
}
});

forEachOpenApiProcedure(mutations, ({ path: mutationPath, procedure, openapi }) => {
const procedures = mergeProcedureRecords(queries, mutations);

forEachOpenApiProcedure(procedures, ({ path: operationId, procedure, openapi }) => {
try {
const { method, protect, summary, description, tags, tag, headers } = openapi;
if (method !== 'POST' && method !== 'PATCH' && method !== 'PUT') {
throw new TRPCError({
message: 'Mutation method must be POST, PATCH or PUT',
code: 'INTERNAL_SERVER_ERROR',
});
}

const path = normalizePath(openapi.path);
const pathParameters = getPathParameters(path);
const headerParameters = headers?.map((header) => ({ ...header, in: 'header' })) || [];
const httpMethod = OpenAPIV3.HttpMethods[method];
if (!httpMethod) {
throw new TRPCError({
message: 'Method must be GET, POST, PATCH, PUT or DELETE',
code: 'INTERNAL_SERVER_ERROR',
});
}
if (pathsObject[path]?.[httpMethod]) {
throw new TRPCError({
message: `Duplicate procedure defined for route ${method} ${path}`,
Expand All @@ -83,35 +64,31 @@ export const getOpenApiPathsObject = (
pathsObject[path] = {
...pathsObject[path],
[httpMethod]: {
operationId: mutationPath,
operationId,
summary,
description,
tags: tags ?? (tag ? [tag] : undefined),
security: protect ? [{ Authorization: [] }] : undefined,
requestBody: getRequestBodyObject(inputParser, pathParameters),
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'path') || []),
],
...(acceptsRequestBody(httpMethod)
? {
requestBody: getRequestBodyObject(inputParser, pathParameters),
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'path') || []),
],
}
: {
parameters: [
...headerParameters,
...(getParameterObjects(inputParser, pathParameters, 'all') || []),
],
}),
responses: getResponsesObject(outputParser),
},
};
} catch (error: any) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
error.message = `[mutation.${mutationPath}] - ${error.message}`;
throw error;
}
});

forEachOpenApiProcedure(subscriptions, ({ path: subscriptionPath }) => {
try {
throw new TRPCError({
message: 'Subscriptions are not supported by OpenAPI v3',
code: 'INTERNAL_SERVER_ERROR',
});
} catch (error: any) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
error.message = `[subscription.${subscriptionPath}] - ${error.message}`;
error.message = `[${operationId}] - ${error.message}`;
throw error;
}
});
Expand Down
17 changes: 17 additions & 0 deletions src/utils/procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ export const getInputOutputParsers = (procedure: Procedure<any, any, any, any, a
};
};

export const mergeProcedureRecords = (
queries: OpenApiProcedureRecord,
mutations: OpenApiProcedureRecord,
) => {
const prefix = (procedures: OpenApiProcedureRecord, type: string) => {
const next: OpenApiProcedureRecord = {};
Object.keys(procedures).forEach((path) => {
next[`${type}.${path}`] = procedures[path]!;
});
return next;
};
return {
...prefix(queries, 'query'),
...prefix(mutations, 'mutation'),
};
};

export const forEachOpenApiProcedure = (
procedureRecord: OpenApiProcedureRecord,
callback: (values: {
Expand Down
Loading