Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: global config #69

Merged
merged 5 commits into from
Jan 3, 2025
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
19 changes: 19 additions & 0 deletions apps/content/content/docs/openapi/config.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Config
description: How to configure your oRPC project.
---

```ts twoslash
import { configGlobal } from '@orpc/contract'; // or '@orpc/server'

configGlobal({
defaultMethod: 'GET', // Default HTTP method for requests
defaultSuccessStatus: 200, // Default HTTP status for successful responses
defaultInputStructure: 'compact', // Input payload structure: 'compact' or 'expanded'
defaultOutputStructure: 'compact', // Output payload structure: 'compact' or 'expanded'
});
```

I recommend placing this script at the top of the `main.ts` file, where the server is initialized.
Alternatively, include it at the top of the file where you define `global oRPC builders or the app router`.
This ensures the configuration is applied before any other part of your code is executed.
19 changes: 19 additions & 0 deletions packages/contract/src/config.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { configGlobal, fallbackToGlobalConfig } from './config'

it('configGlobal & fallbackToGlobalConfig', () => {
configGlobal({
defaultMethod: 'GET',
})

configGlobal({
// @ts-expect-error -- invalid value
defaultMethod: 'INVALID',
})

fallbackToGlobalConfig('defaultMethod', undefined)
fallbackToGlobalConfig('defaultMethod', 'GET')
// @ts-expect-error -- invalid value
fallbackToGlobalConfig('defaultMethod', 'INVALID')
// @ts-expect-error -- invalid global config
fallbackToGlobalConfig('INVALID', 'GET')
})
25 changes: 25 additions & 0 deletions packages/contract/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { configGlobal, fallbackToGlobalConfig } from './config'

it('configGlobal & fallbackToGlobalConfig', () => {
configGlobal({
defaultMethod: 'GET',
defaultSuccessStatus: 203,
})

expect(fallbackToGlobalConfig('defaultMethod', undefined)).toBe('GET')
expect(fallbackToGlobalConfig('defaultSuccessStatus', undefined)).toBe(203)

configGlobal({ defaultMethod: undefined, defaultSuccessStatus: undefined })

expect(fallbackToGlobalConfig('defaultMethod', undefined)).toBe('POST')
expect(fallbackToGlobalConfig('defaultSuccessStatus', undefined)).toBe(200)

expect(() => configGlobal({ defaultSuccessStatus: 300 })).toThrowError()

expect(fallbackToGlobalConfig('defaultMethod', 'DELETE')).toBe('DELETE')
expect(fallbackToGlobalConfig('defaultInputStructure', undefined)).toBe('compact')
expect(fallbackToGlobalConfig('defaultInputStructure', 'detailed')).toBe('detailed')

/** Reset to make sure the global config is not affected other tests */
configGlobal({ defaultMethod: undefined, defaultSuccessStatus: undefined })
})
66 changes: 66 additions & 0 deletions packages/contract/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { HTTPMethod, InputStructure } from './types'

export interface ORPCConfig {
/**
* @default 'POST'
*/
defaultMethod?: HTTPMethod

/**
*
* @default 200
*/
defaultSuccessStatus?: number

/**
*
* @default 'compact'
*/
defaultInputStructure?: InputStructure

/**
*
* @default 'compact'
*/
defaultOutputStructure?: InputStructure
}

const DEFAULT_CONFIG: Required<ORPCConfig> = {
defaultMethod: 'POST',
defaultSuccessStatus: 200,
defaultInputStructure: 'compact',
defaultOutputStructure: 'compact',
}

const GLOBAL_CONFIG_REF: { value: ORPCConfig } = { value: DEFAULT_CONFIG }

/**
* Set the global configuration, this configuration can effect entire project
*/
export function configGlobal(config: ORPCConfig): void {
if (
config.defaultSuccessStatus !== undefined
&& (config.defaultSuccessStatus < 200 || config.defaultSuccessStatus > 299)
) {
throw new Error('[configGlobal] The defaultSuccessStatus must be between 200 and 299')
}

GLOBAL_CONFIG_REF.value = config
}

/**
* Fallback the value to the global config if it is undefined
*/
export function fallbackToGlobalConfig<T extends keyof ORPCConfig>(key: T, value: ORPCConfig[T]): Exclude<ORPCConfig[T], undefined> {
if (value === undefined) {
const fallback = GLOBAL_CONFIG_REF.value[key]

if (fallback === undefined) {
return DEFAULT_CONFIG[key] as any
}

return fallback as any
}

return value as any
}
1 change: 1 addition & 0 deletions packages/contract/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { ContractBuilder } from './builder'

export * from './builder'
export * from './config'
export * from './procedure'
export * from './procedure-decorated'
export * from './router'
Expand Down
6 changes: 4 additions & 2 deletions packages/contract/src/procedure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
HTTPMethod,
HTTPPath,
InputStructure,
OutputStructure,
Schema,
SchemaOutput,
} from './types'
Expand Down Expand Up @@ -41,7 +43,7 @@ export interface RouteOptions {
*
* @default 'compact'
*/
inputStructure?: 'compact' | 'detailed'
inputStructure?: InputStructure

/**
* Determines how the response should be structured based on the output.
Expand All @@ -64,7 +66,7 @@ export interface RouteOptions {
*
* @default 'compact'
*/
outputStructure?: 'compact' | 'detailed'
outputStructure?: OutputStructure
}

export interface ContractProcedureDef<TInputSchema extends Schema, TOutputSchema extends Schema> {
Expand Down
2 changes: 2 additions & 0 deletions packages/contract/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { StandardSchemaV1 } from '@standard-schema/spec'

export type HTTPPath = `/${string}`
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export type InputStructure = 'compact' | 'detailed'
export type OutputStructure = 'compact' | 'detailed'

export type Schema = StandardSchemaV1 | undefined

Expand Down
16 changes: 6 additions & 10 deletions packages/openapi/src/adapters/fetch/openapi-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ANY_PROCEDURE, Context, Router, WithSignal } from '@orpc/server'
import type { ConditionalFetchHandler, FetchOptions } from '@orpc/server/fetch'
import type { Params } from 'hono/router'
import type { PublicInputStructureCompact } from './input-structure-compact'
import { createProcedureClient, ORPCError } from '@orpc/server'
import { createProcedureClient, fallbackToGlobalConfig, ORPCError } from '@orpc/server'
import { executeWithHooks, type Hooks, isPlainObject, ORPC_HANDLER_HEADER, trim } from '@orpc/shared'
import { JSONSerializer, type PublicJSONSerializer } from '../../json-serializer'
import { InputStructureCompact } from './input-structure-compact'
Expand Down Expand Up @@ -86,7 +86,7 @@ export class OpenAPIHandler<T extends Context> implements ConditionalFetchHandle

return new Response(body, {
headers: resHeaders,
status: contractDef.route?.successStatus ?? 200,
status: fallbackToGlobalConfig('defaultSuccessStatus', contractDef.route?.successStatus),
})
}

Expand Down Expand Up @@ -128,13 +128,13 @@ export class OpenAPIHandler<T extends Context> implements ConditionalFetchHandle
}

private async decodeInput(procedure: ANY_PROCEDURE, params: Params, request: Request): Promise<unknown> {
const inputStructure = procedure['~orpc'].contract['~orpc'].route?.inputStructure
const inputStructure = fallbackToGlobalConfig('defaultInputStructure', procedure['~orpc'].contract['~orpc'].route?.inputStructure)

const url = new URL(request.url)
const query = url.searchParams
const headers = request.headers

if (!inputStructure || inputStructure === 'compact') {
if (inputStructure === 'compact') {
return this.inputStructureCompact.build(
params,
request.method === 'GET'
Expand All @@ -143,8 +143,6 @@ export class OpenAPIHandler<T extends Context> implements ConditionalFetchHandle
)
}

const _expect: 'detailed' = inputStructure

const decodedQuery = await this.payloadCodec.decode(query)
const decodedHeaders = await this.payloadCodec.decode(headers)
const decodedBody = await this.payloadCodec.decode(request)
Expand All @@ -157,14 +155,12 @@ export class OpenAPIHandler<T extends Context> implements ConditionalFetchHandle
output: unknown,
accept: string | undefined,
): { body: string | Blob | FormData | undefined, headers?: Headers } {
const outputStructure = procedure['~orpc'].contract['~orpc'].route?.outputStructure
const outputStructure = fallbackToGlobalConfig('defaultOutputStructure', procedure['~orpc'].contract['~orpc'].route?.outputStructure)

if (!outputStructure || outputStructure === 'compact') {
if (outputStructure === 'compact') {
return this.payloadCodec.encode(output, accept)
}

const _expect: 'detailed' = outputStructure

this.assertDetailedOutput(output)

const headers = new Headers()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { HTTPPath } from '@orpc/contract'
import type { Router as BaseHono, ParamIndexMap, Params } from 'hono/router'
import { type ANY_PROCEDURE, type ANY_ROUTER, getLazyRouterPrefix, getRouterChild, isProcedure, unlazy } from '@orpc/server'
import { type ANY_PROCEDURE, type ANY_ROUTER, fallbackToGlobalConfig, getLazyRouterPrefix, getRouterChild, isProcedure, unlazy } from '@orpc/server'
import { mapValues } from '@orpc/shared'
import { forEachContractProcedure, standardizeHTTPPath } from '../../utils'

Expand Down Expand Up @@ -75,7 +75,7 @@ export class OpenAPIProcedureMatcher {

private add(path: string[], router: ANY_ROUTER): void {
const lazies = forEachContractProcedure({ path, router }, ({ path, contract }) => {
const method = contract['~orpc'].route?.method ?? 'POST'
const method = fallbackToGlobalConfig('defaultMethod', contract['~orpc'].route?.method)
const httpPath = contract['~orpc'].route?.path
? this.convertOpenAPIPathToRouterPath(contract['~orpc'].route?.path)
: `/${path.map(encodeURIComponent).join('/')}`
Expand Down
12 changes: 7 additions & 5 deletions packages/openapi/src/openapi-generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ContractRouter } from '@orpc/contract'
import type { ANY_ROUTER } from '@orpc/server'
import type { PublicOpenAPIInputStructureParser } from './openapi-input-structure-parser'
import type { PublicOpenAPIOutputStructureParser } from './openapi-output-structure-parser'
import type { PublicOpenAPIPathParser } from './openapi-path-parser'
import type { SchemaConverter } from './schema-converter'
import { type ContractRouter, fallbackToGlobalConfig } from '@orpc/contract'
import { JSONSerializer, type PublicJSONSerializer } from './json-serializer'
import { type OpenAPI, OpenApiBuilder } from './openapi'
import { OpenAPIContentBuilder, type PublicOpenAPIContentBuilder } from './openapi-content-builder'
Expand Down Expand Up @@ -99,11 +99,13 @@ export class OpenAPIGenerator {
return
}

const method = def.route?.method ?? 'POST'
const method = fallbackToGlobalConfig('defaultMethod', def.route?.method)
const httpPath = def.route?.path ? standardizeHTTPPath(def.route?.path) : `/${path.map(encodeURIComponent).join('/')}`
const inputStructure = fallbackToGlobalConfig('defaultInputStructure', def.route?.inputStructure)
const outputStructure = fallbackToGlobalConfig('defaultOutputStructure', def.route?.outputStructure)

const { paramsSchema, querySchema, headersSchema, bodySchema } = this.inputStructureParser.parse(contract, def.route?.inputStructure ?? 'compact')
const { headersSchema: resHeadersSchema, bodySchema: resBodySchema } = this.outputStructureParser.parse(contract, def.route?.outputStructure ?? 'compact')
const { paramsSchema, querySchema, headersSchema, bodySchema } = this.inputStructureParser.parse(contract, inputStructure)
const { headersSchema: resHeadersSchema, bodySchema: resBodySchema } = this.outputStructureParser.parse(contract, outputStructure)

const params = paramsSchema
? this.parametersBuilder.build('path', paramsSchema, {
Expand Down Expand Up @@ -161,7 +163,7 @@ export class OpenAPIGenerator {
parameters: parameters.length ? parameters : undefined,
requestBody,
responses: {
[def.route?.successStatus ?? 200]: successResponse,
[fallbackToGlobalConfig('defaultSuccessStatus', def.route?.successStatus)]: successResponse,
},
}

Expand Down
4 changes: 2 additions & 2 deletions packages/openapi/src/openapi-input-structure-parser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ANY_CONTRACT_PROCEDURE } from '@orpc/contract'
import type { PublicOpenAPIPathParser } from './openapi-path-parser'
import type { JSONSchema, ObjectSchema } from './schema'
import type { SchemaConverter } from './schema-converter'
import type { PublicSchemaUtils } from './schema-utils'
import { type ANY_CONTRACT_PROCEDURE, fallbackToGlobalConfig } from '@orpc/contract'
import { OpenAPIError } from './openapi-error'

export interface OpenAPIInputStructureParseResult {
Expand All @@ -21,7 +21,7 @@ export class OpenAPIInputStructureParser {

parse(contract: ANY_CONTRACT_PROCEDURE, structure: 'compact' | 'detailed'): OpenAPIInputStructureParseResult {
const inputSchema = this.schemaConverter.convert(contract['~orpc'].InputSchema, { strategy: 'input' })
const method = contract['~orpc'].route?.method ?? 'POST'
const method = fallbackToGlobalConfig('defaultMethod', contract['~orpc'].route?.method)
const httpPath = contract['~orpc'].route?.path

if (this.schemaUtils.isAnySchema(inputSchema)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export * from './router-client'
export * from './router-implementer'
export * from './types'
export * from './utils'

export { configGlobal, fallbackToGlobalConfig } from '@orpc/contract'
export * from '@orpc/shared/error'

export const os = new Builder<WELL_CONTEXT, undefined>({})
Loading