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
46 changes: 23 additions & 23 deletions apps/content/content/docs/server/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,53 +45,53 @@ If your procedure only depends on `Middleware Context`, you can
[call it](/docs/server/client) or use it as a [Server Action](/docs/server/server-action) directly.

```ts twoslash
import { os, ORPCError, call } from '@orpc/server'
import { call, ORPCError, os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { headers } from 'next/headers'

const base = os.use(async ({ context, path, next }, input) => {
return next({
context: {
db: 'fake-db',
}
})
return next({
context: {
db: 'fake-db',
},
})
})

const authMid = base.middleware(async ({ context, next, path }, input) => {
const authMid = os
.context<{ db: string }>() // this middleware depends on some context
.middleware(async ({ context, next, path }, input) => {
const headersList = await headers()
const user = headersList.get('Authorization') ? { id: 'example' } : undefined

if (!user) {
throw new ORPCError({ code: 'UNAUTHORIZED' })
throw new ORPCError({ code: 'UNAUTHORIZED' })
}

return next({
context: {
user,
}
context: {
user,
},
})
})
})

export const router = base.router({
getUser: base
.use(authMid)
.handler(({ input, context }) => {
// ^ context is fully typed
}),
getUser: base
.use(authMid)
.handler(({ input, context }) => {
// ^ context is fully typed
}),
})

// You can call this procedure directly without manually providing context
const output = await call(router.getUser, null)

import { RPCHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch'

const rpcHandler = new RPCHandler(router)

export async function fetch(request: Request) {
// No need to pass context; middleware handles it
const { response } = await rpcHandler.handle(request)
// No need to pass context; middleware handles it
const { response } = await rpcHandler.handle(request)

return response ?? new Response('Not found', { status: 404 })
return response ?? new Response('Not found', { status: 404 })
}
```

Expand Down
15 changes: 1 addition & 14 deletions apps/content/examples/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,7 @@ import { z } from 'zod'

export type Context = { user?: { id: string } }

export const pub = os
.context<Context>()
.use(async ({ context, path, next }, input) => {
// This middleware will apply to everything create from pub
const start = Date.now()

try {
return await next({})
}
finally {
// eslint-disable-next-line no-console
console.log(`middleware cost ${Date.now() - start}ms`)
}
})
export const pub = os.context<Context>()

export const authMiddleware = pub.middleware(async ({ context, next, path, procedure }, input) => {
if (!context.user) {
Expand Down
165 changes: 47 additions & 118 deletions packages/contract/src/builder.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { DecoratedContractProcedure } from './procedure-decorated'
import type { ContractProcedure } from './procedure'
import type { ContractProcedureBuilder } from './procedure-builder'
import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input'
import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output'
import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder'
import { z } from 'zod'
import { ContractBuilder } from './builder'
import { ContractProcedure } from './procedure'

const schema = z.object({
value: z.string(),
})
const schema = z.object({ value: z.string() })

const baseErrorMap = {
BASE: {
status: 500,
data: z.object({
message: z.string(),
}),
Expand All @@ -19,142 +18,72 @@ const baseErrorMap = {

const builder = new ContractBuilder({ errorMap: baseErrorMap, OutputSchema: undefined, InputSchema: undefined })

it('also is a contract procedure', () => {
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, typeof baseErrorMap>>()
})

describe('self chainable', () => {
describe('errors', () => {
const errors = {
BAD: {
status: 500,
data: schema,
},
ERROR2: {
status: 401,
data: schema,
},
} as const

it('should merge and strict with old one', () => {
expectTypeOf(builder.errors(errors)).toEqualTypeOf<
ContractBuilder<typeof errors & typeof baseErrorMap>
>()
})

it('should prevent redefine errorMap', () => {
// @ts-expect-error - not allow redefine errorMap
builder.errors({ BASE: baseErrorMap.BASE })
// @ts-expect-error - not allow redefine errorMap - even with undefined
builder.errors({ BASE: undefined })
})
describe('ContractBuilder', () => {
it('is a contract procedure', () => {
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, typeof baseErrorMap>>()
})
})

describe('to ContractRouterBuilder', () => {
it('prefix', () => {
expectTypeOf(builder.prefix('/prefix')).toEqualTypeOf<
ContractRouterBuilder<typeof baseErrorMap>
>()

// @ts-expect-error - invalid prefix
builder.prefix(1)
// @ts-expect-error - invalid prefix
builder.prefix('')
})
it('.errors', () => {
const errors = { BAD_GATEWAY: { data: schema } } as const

it('tags', () => {
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<
ContractRouterBuilder<typeof baseErrorMap>
>()
expectTypeOf(builder.errors(errors))
.toEqualTypeOf<ContractBuilder<typeof baseErrorMap & typeof errors>>()

// @ts-expect-error - invalid tag
builder.tag(1)
// @ts-expect-error - invalid tag
builder.tag({})
// @ts-expect-error - not allow redefine error map
builder.errors({ BASE: baseErrorMap.BASE })
})
})

describe('to DecoratedContractProcedure', () => {
it('route', () => {
expectTypeOf(builder.route({ method: 'GET', path: '/path' })).toEqualTypeOf<
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
>()

expectTypeOf(builder.route({ })).toEqualTypeOf<
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
>()
it('.route', () => {
expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf<ContractProcedureBuilder<typeof baseErrorMap>>()

// @ts-expect-error - invalid method
builder.route({ method: 'HE' })
// @ts-expect-error - invalid path
builder.route({ method: 'GET', path: '' })
})

it('input', () => {
it('.input', () => {
expectTypeOf(builder.input(schema)).toEqualTypeOf<
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
>()

expectTypeOf(builder.input(schema, { value: 'example' })).toEqualTypeOf<
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
ContractProcedureBuilderWithInput<typeof schema, typeof baseErrorMap >
>()

// @ts-expect-error - invalid schema
builder.input({})

// @ts-expect-error - invalid example
builder.input(schema, { })
})

it('output', () => {
it('.output', () => {
expectTypeOf(builder.output(schema)).toEqualTypeOf<
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
>()

expectTypeOf(builder.output(schema, { value: 'example' })).toEqualTypeOf<
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
ContractProcedureBuilderWithOutput<typeof schema, typeof baseErrorMap >
>()
})

// @ts-expect-error - invalid schema
builder.output({})
it('.prefix', () => {
expectTypeOf(builder.prefix('/api')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()

// @ts-expect-error - invalid example
builder.output(schema, {})
// @ts-expect-error - invalid prefix
builder.prefix(1)
})
})

describe('to router', () => {
const errors = {
CONFLICT: {
status: 400,
data: z.object({
message: z.string(),
}),
},
}

const router = { a: { b: {
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: errors }),
} } }

it('adapt all procedures', () => {
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()
expectTypeOf(builder.router({})).toEqualTypeOf<Record<never, never>>()
it('.tag', () => {
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()

// @ts-expect-error - invalid router
builder.router({ a: 1 })
// @ts-expect-error - invalid tag
builder.tag(1)
})

it('throw on conflict error map', () => {
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: typeof baseErrorMap['BASE'] }> })
// @ts-expect-error conflict
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: { message: string } }> })
})
it('.router', () => {
const router = {
ping: {} as ContractProcedure<undefined, typeof schema, typeof baseErrorMap>,
pong: {} as ContractProcedure<typeof schema, undefined, Record<never, never>>,
}

it('only required partial match error map', () => {
expectTypeOf(builder.router({ ping: {} as ContractProcedure<any, any, { OTHER: { status: number } }> })).toEqualTypeOf<{
ping: DecoratedContractProcedure<any, any, { OTHER: { status: number } } & typeof baseErrorMap>
}>()
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()

const invalidErrorMap = {
BASE: {
...baseErrorMap.BASE,
status: 400,
},
}

builder.router({
// @ts-expect-error - error map is not match
ping: {} as ContractProcedure<undefined, typeof schema, typeof invalidErrorMap>,
})
})
})
Loading