Skip to content

Commit

Permalink
feat(openapi): input/output detailed structure (#66)
Browse files Browse the repository at this point in the history
* typed

* input structure

* improve

* output structure

* docs

* openapi builder

* openapi generator testing for input and output structure

* optional on query will have no effect
  • Loading branch information
unnoq authored Jan 1, 2025
1 parent 1cee930 commit fd1db03
Show file tree
Hide file tree
Showing 14 changed files with 1,011 additions and 176 deletions.
85 changes: 85 additions & 0 deletions apps/content/content/docs/openapi/inout-structure.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: Input/Output Structure
description: Control how input and output data is structured in your API.
---

## Introduction

Input and output structures define how data is organized when procedures are executed,
affecting how input data is processed and how output data is formatted in the HTTP response.

```ts twoslash
import { oc } from '@orpc/contract'
import { os } from '@orpc/server'
import { z } from 'zod'

const contract = oc.route({
inputStructure: 'detailed',
outputStructure: 'detailed',
})

// or

const procedure = os.route({
path: '/ping/{name}',
method: 'POST',
inputStructure: 'detailed',
outputStructure: 'detailed',
})
.input(z.object({
params: z.object({ name: z.string() }),
query: z.object({ search: z.string() }),
body: z.object({ description: z.string() }).optional(),
headers: z.object({ 'content-type': z.string() }), // please use lowercase
}))
.handler((input, context, meta) => {
return {
body: 'the body',
headers: {
'x-custom-header': 'custom-value',
},
}
})
```

> **Note**: You only need to define the input and output schemas according to your requirements. The example provided above is solely for demonstration purposes.
## Input Structure Options

The `inputStructure` option determines how the input data is structured based on `params`, `query`, `headers`, and `body`.

- **'compact'**: Combines `params` and either `query` or `body` (depending on the HTTP method) into a single object.

- For example, in a GET request, `params` and `query` are combined.
- In a POST request, `params` and `body` are combined.

- **'detailed'**: Keeps each part of the request (`params`, `query`, `headers`, and `body`) as separate fields in the input object.

```ts
const input = {
params: { id: 1 },
query: { search: 'hello' },
headers: { 'Content-Type': 'application/json' },
body: { name: 'John' },
};
```

## Output Structure Options

The `outputStructure` option determines how the response is structured based on the output.

- **'compact'**: Includes only the body data, encoded directly in the response.

- **'detailed'**: Separates the output into `headers` and `body` fields.

```ts
const output = {
headers: { 'x-custom-header': 'value' },
body: { message: 'Hello, world!' },
};
```

## Default Behavior

- `inputStructure` defaults to `'compact'`
- `outputStructure` defaults to `'compact'`
52 changes: 52 additions & 0 deletions packages/contract/src/procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,59 @@ export interface RouteOptions {
description?: string
deprecated?: boolean
tags?: readonly string[]

/**
* The status code of the response when the procedure is successful.
*
* @default 200
*/
successStatus?: number

/**
* Determines how the input should be structured based on `params`, `query`, `headers`, and `body`.
*
* @option 'compact'
* Combines `params` and either `query` or `body` (depending on the HTTP method) into a single object.
*
* @option 'detailed'
* Keeps each part of the request (`params`, `query`, `headers`, and `body`) as separate fields in the input object.
*
* Example:
* ```ts
* const input = {
* params: { id: 1 },
* query: { search: 'hello' },
* headers: { 'Content-Type': 'application/json' },
* body: { name: 'John' },
* }
* ```
*
* @default 'compact'
*/
inputStructure?: 'compact' | 'detailed'

/**
* Determines how the response should be structured based on the output.
*
* @option 'compact'
* Includes only the body data, encoded directly in the response.
*
* @option 'detailed'
* Separates the output into `headers` and `body` fields.
* - `headers`: Custom headers to merge with the response headers.
* - `body`: The response data.
*
* Example:
* ```ts
* const output = {
* headers: { 'x-custom-header': 'value' },
* body: { message: 'Hello, world!' },
* };
* ```
*
* @default 'compact'
*/
outputStructure?: 'compact' | 'detailed'
}

export interface ContractProcedureDef<TInputSchema extends Schema, TOutputSchema extends Schema> {
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi/src/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './bracket-notation'
export * from './input-builder-full'
export * from './input-builder-simple'
export * from './input-structure-compact'
export * from './input-structure-detailed'
export * from './openapi-handler'
export * from './openapi-handler-server'
export * from './openapi-handler-serverless'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Params } from 'hono/router'
import { isPlainObject } from '@orpc/shared'

export class InputBuilderSimple {
export class InputStructureCompact {
build(params: Params, payload: unknown): unknown {
if (Object.keys(params).length === 0) {
return payload
Expand All @@ -18,4 +18,4 @@ export class InputBuilderSimple {
}
}

export type PublicInputBuilderSimple = Pick<InputBuilderSimple, keyof InputBuilderSimple>
export type PublicInputStructureCompact = Pick<InputStructureCompact, keyof InputStructureCompact>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Params } from 'hono/router'

export class InputBuilderFull {
export class InputStructureDetailed {
build(params: Params, query: unknown, headers: unknown, body: unknown): { params: Params, query: unknown, headers: unknown, body: unknown } {
return {
params,
Expand All @@ -11,4 +11,4 @@ export class InputBuilderFull {
}
}

export type PublicInputBuilderFull = Pick<InputBuilderFull, keyof InputBuilderFull>
export type PublicInputStructureDetailed = Pick<InputStructureDetailed, keyof InputStructureDetailed>
Loading

0 comments on commit fd1db03

Please sign in to comment.