Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/contract/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class ContractBuilder<
}

export const oc = new ContractBuilder<
Schema<unknown, unknown>,
Schema<void, void>,
Schema<unknown, unknown>,
Record<never, never>,
Record<never, never>
Expand Down
85 changes: 85 additions & 0 deletions packages/contract/src/default-void-input.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Schema } from '@orpc/contract'
import { expectTypeOf } from 'vitest'
import { z } from 'zod'
import { oc } from './builder'

describe('default void input - contract - type tests', () => {
it('should infer void input type when input() is not specified', () => {
const contract = oc
.output(z.string())

type InputSchema = typeof contract extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should match explicit void input type', () => {
const contractWithoutInput = oc
.output(z.string())

const contractWithVoidInput = oc
.input(z.void())
.output(z.string())

type InputSchema1 = typeof contractWithoutInput extends { '~orpc': { inputSchema?: infer S } } ? S : never
type InputSchema2 = typeof contractWithVoidInput extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema1>().toEqualTypeOf<InputSchema2>()
})

it('should still allow explicit input schema to override', () => {
const contract = oc
.input(z.object({ name: z.string() }))
.output(z.string())

type InputSchema = typeof contract extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema>().not.toEqualTypeOf<Schema<void, void>>()
})

it('should work with metadata chaining', () => {
const contract = oc
.meta({ description: 'A test procedure' })
.output(z.string())

type InputSchema = typeof contract extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should work with route definition', () => {
const contract = oc
.route({ method: 'GET', path: '/test' })
.output(z.string())

type InputSchema = typeof contract extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should work with error mapping', () => {
const contract = oc
.errors({ NOT_FOUND: z.object({ message: z.string() }) })
.output(z.string())

type InputSchema = typeof contract extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should work in contract router definition', () => {
const contractRouter = {
withoutInput: oc
.output(z.string()),
withInput: oc
.input(z.object({ id: z.string() }))
.output(z.string()),
}

type WithoutInputSchema = typeof contractRouter.withoutInput extends { '~orpc': { inputSchema?: infer S } } ? S : never
type WithInputSchema = typeof contractRouter.withInput extends { '~orpc': { inputSchema?: infer S } } ? S : never

expectTypeOf<WithoutInputSchema>().toEqualTypeOf<Schema<void, void>>()
expectTypeOf<WithInputSchema>().not.toEqualTypeOf<Schema<void, void>>()
})
})
76 changes: 76 additions & 0 deletions packages/contract/src/default-void-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { z } from 'zod'
import { oc } from './builder'
import { isContractProcedure } from './procedure'

describe('default void input - contract', () => {
it('should be a valid contract procedure', () => {
const contract = oc
.output(z.string())

expect(isContractProcedure(contract)).toBe(true)
})

it('should work with explicit void input same as default', () => {
const contractWithoutInput = oc
.output(z.string())

const contractWithVoidInput = oc
.input(z.void())
.output(z.string())

expect(isContractProcedure(contractWithoutInput)).toBe(true)
expect(isContractProcedure(contractWithVoidInput)).toBe(true)
})

it('should still work with explicit input schema', () => {
const contract = oc
.input(z.object({ name: z.string() }))
.output(z.string())

const inputSchema = contract['~orpc'].inputSchema

expect(inputSchema).toBeDefined()
expect(inputSchema?.['~standard'].vendor).toBe('zod')
})

it('should work in contract router without input()', () => {
const contractRouter = {
getAll: oc
.output(z.array(z.string())),
getOne: oc
.input(z.object({ id: z.string() }))
.output(z.string()),
}

expect(isContractProcedure(contractRouter.getAll)).toBe(true)
expect(isContractProcedure(contractRouter.getOne)).toBe(true)
expect(contractRouter.getOne['~orpc'].inputSchema).toBeDefined()
})

it('should allow metadata chaining with default void input', () => {
const contract = oc
.meta({ description: 'A test procedure' })
.output(z.string())

expect(isContractProcedure(contract)).toBe(true)
expect(contract['~orpc'].meta).toEqual({ description: 'A test procedure' })
})

it('should allow route definition with default void input', () => {
const contract = oc
.route({ method: 'GET', path: '/test' })
.output(z.string())

expect(isContractProcedure(contract)).toBe(true)
expect(contract['~orpc'].route).toEqual({ method: 'GET', path: '/test' })
})

it('should allow error mapping with default void input', () => {
const contract = oc
.errors({ NOT_FOUND: z.object({ message: z.string() }) })
.output(z.string())

expect(isContractProcedure(contract)).toBe(true)
expect(contract['~orpc'].errorMap).toHaveProperty('NOT_FOUND')
})
})
2 changes: 1 addition & 1 deletion packages/server/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export class Builder<
export const os = new Builder<
Record<never, never>,
Record<never, never>,
Schema<unknown, unknown>,
Schema<void, void>,
Schema<unknown, unknown>,
Record<never, never>,
Record<never, never>
Expand Down
86 changes: 86 additions & 0 deletions packages/server/src/default-void-input.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Schema } from '@orpc/contract'
import { expectTypeOf } from 'vitest'
import { z } from 'zod'
import { os } from './builder'

describe('default void input - type tests', () => {
it('should infer void input type when input() is not specified', () => {
const procedure = os
.output(z.string())
.handler(() => 'result')

type InputSchema = typeof procedure extends { '~orpc': { inputSchema: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should allow handler without input parameter when input() is not specified', () => {
const procedure = os
.output(z.string())
.handler(() => 'result')

expectTypeOf(procedure).toMatchTypeOf<{
'~orpc': {
inputSchema: Schema<void, void>
outputSchema: Schema<string, string>
}
}>()
})

it('should match explicit void input type', () => {
const procedureWithoutInput = os
.output(z.string())
.handler(() => 'result1')

const procedureWithVoidInput = os
.input(z.void())
.output(z.string())
.handler(() => 'result2')

type InputSchema1 = typeof procedureWithoutInput extends { '~orpc': { inputSchema: infer S } } ? S : never
type InputSchema2 = typeof procedureWithVoidInput extends { '~orpc': { inputSchema: infer S } } ? S : never

expectTypeOf<InputSchema1>().toEqualTypeOf<InputSchema2>()
})

it('should still allow explicit input schema to override', () => {
const procedure = os
.input(z.object({ name: z.string() }))
.output(z.string())
.handler(({ input }) => `Hello ${input.name}`)

type InputSchema = typeof procedure extends { '~orpc': { inputSchema: infer S } } ? S : never

expectTypeOf<InputSchema>().not.toEqualTypeOf<Schema<void, void>>()
})

it('should work with middleware chaining', () => {
const middleware = os.middleware(() => ({ context: { user: 'test' } }))

const procedure = middleware
.output(z.string())
.handler(({ context }) => `User: ${context.user}`)

type InputSchema = typeof procedure extends { '~orpc': { inputSchema: infer S } } ? S : never

expectTypeOf<InputSchema>().toEqualTypeOf<Schema<void, void>>()
})

it('should work in router definition', () => {
const testRouter = {
withoutInput: os
.output(z.string())
.handler(() => 'result'),
withInput: os
.input(z.object({ id: z.string() }))
.output(z.string())
.handler(({ input }) => `ID: ${input.id}`),
}

type WithoutInputSchema = typeof testRouter.withoutInput extends { '~orpc': { inputSchema: infer S } } ? S : never
type WithInputSchema = typeof testRouter.withInput extends { '~orpc': { inputSchema: infer S } } ? S : never

expectTypeOf<WithoutInputSchema>().toEqualTypeOf<Schema<void, void>>()
expectTypeOf<WithInputSchema>().not.toEqualTypeOf<Schema<void, void>>()
})
})
119 changes: 119 additions & 0 deletions packages/server/src/default-void-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { z } from 'zod'
import { os } from './builder'

describe('default void input', () => {
it('should accept no input when input() is not specified', async () => {
const procedure = os
.output(z.string())
.handler(() => 'result')

// Should work without providing input
const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

expect(result).toBe('result')
})

it('should accept undefined as input when input() is not specified', async () => {
const procedure = os
.output(z.string())
.handler(() => 'result')

// Should work with undefined input
const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

expect(result).toBe('result')
})
Comment on lines +5 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

These two test cases, 'should accept no input when input() is not specified' and 'should accept undefined as input when input() is not specified', are functionally identical. Both tests use input: undefined in the handler call, effectively testing the same scenario. To improve conciseness and remove redundancy, they can be merged into a single test.

Suggested change
it('should accept no input when input() is not specified', async () => {
const procedure = os
.output(z.string())
.handler(() => 'result')
// Should work without providing input
const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})
expect(result).toBe('result')
})
it('should accept undefined as input when input() is not specified', async () => {
const procedure = os
.output(z.string())
.handler(() => 'result')
// Should work with undefined input
const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})
expect(result).toBe('result')
})
it('should accept undefined as input when input() is not specified', async () => {
const procedure = os
.output(z.string())
.handler(() => 'result')
// Should work with undefined input
const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})
expect(result).toBe('result')
})


it('should work with explicit void input same as default', async () => {
const procedureWithoutInput = os
.output(z.string())
.handler(() => 'result1')

const procedureWithVoidInput = os
.input(z.void())
.output(z.string())
.handler(() => 'result2')

const result1 = await procedureWithoutInput['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

const result2 = await procedureWithVoidInput['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

expect(result1).toBe('result1')
expect(result2).toBe('result2')
})

it('should still work with explicit input schema', async () => {
const procedure = os
.input(z.object({ name: z.string() }))
.output(z.string())
.handler(({ input }) => `Hello ${input.name}`)

const result = await procedure['~orpc'].handler({
context: {},
input: { name: 'World' },
rawInput: { name: 'World' },
})

expect(result).toBe('Hello World')
})

it('should work after use() without explicit input()', async () => {
const middleware = ({ next }) => next()

const procedure = os
.use(middleware)
.output(z.string())
.handler(() => 'result after middleware')

const result = await procedure['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

expect(result).toBe('result after middleware')
})

it('should work in router without input()', async () => {
const testRouter = {
getAll: os
.output(z.array(z.string()))
.handler(() => ['item1', 'item2']),
getOne: os
.input(z.object({ id: z.string() }))
.output(z.string())
.handler(({ input }) => `Item: ${input.id}`),
}

const result1 = await testRouter.getAll['~orpc'].handler({
context: {},
input: undefined,
rawInput: undefined,
})

const result2 = await testRouter.getOne['~orpc'].handler({
context: {},
input: { id: '123' },
rawInput: { id: '123' },
})

expect(result1).toEqual(['item1', 'item2'])
expect(result2).toBe('Item: 123')
})
})
Loading