From 43889a7e6b25d377c0c6373000757207d36a43e0 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 19 Jan 2025 10:40:45 +0700 Subject: [PATCH] feat(server)!: improve context (#96) * builders * handlers * wip * fix * fix docs * fix tests * fix naming --- apps/content/content/docs/server/lazy.mdx | 2 +- .../content/docs/server/server-action.mdx | 2 +- apps/content/examples/server.ts | 4 +- .../src/adapters/fetch/openapi-handler.ts | 2 +- .../server/src/adapters/fetch/orpc-handler.ts | 4 +- .../server/src/adapters/fetch/types.test-d.ts | 4 +- packages/server/src/adapters/fetch/types.ts | 9 ++- .../server/src/adapters/hono/middleware.ts | 12 +-- packages/server/src/adapters/next/serve.ts | 10 ++- .../server/src/adapters/node/orpc-handler.ts | 12 +-- packages/server/src/adapters/node/types.ts | 8 +- .../builder-with-errors-middlewares.test-d.ts | 40 +++++----- .../src/builder-with-errors-middlewares.ts | 76 +++++++++++------- .../server/src/builder-with-errors.test-d.ts | 35 ++++---- packages/server/src/builder-with-errors.ts | 69 +++++++++------- .../src/builder-with-middlewares.test-d.ts | 42 +++++----- .../server/src/builder-with-middlewares.ts | 68 ++++++++-------- packages/server/src/builder.test-d.ts | 39 +++++---- packages/server/src/builder.ts | 70 ++++++++-------- packages/server/src/context.ts | 23 +++--- .../src/implementer-chainable.test-d.ts | 44 ++++++----- packages/server/src/implementer-chainable.ts | 28 ++++--- packages/server/src/index.ts | 5 +- packages/server/src/lazy-decorated.test-d.ts | 9 ++- packages/server/src/lazy-decorated.test.ts | 2 +- packages/server/src/lazy-decorated.ts | 9 ++- packages/server/src/lazy.test-d.ts | 4 +- packages/server/src/lazy.test.ts | 3 +- .../server/src/middleware-decorated.test-d.ts | 10 +-- packages/server/src/middleware-decorated.ts | 49 ++++++------ packages/server/src/middleware.test-d.ts | 36 +++++---- packages/server/src/middleware.ts | 41 ++++++---- .../procedure-builder-with-input.test-d.ts | 24 ++++-- .../src/procedure-builder-with-input.ts | 46 +++++------ .../procedure-builder-with-output.test-d.ts | 16 ++-- .../src/procedure-builder-with-output.ts | 45 ++++++----- .../server/src/procedure-builder.test-d.ts | 25 ++++-- packages/server/src/procedure-builder.ts | 40 +++++----- .../server/src/procedure-client.test-d.ts | 21 ++--- packages/server/src/procedure-client.test.ts | 32 ++++---- packages/server/src/procedure-client.ts | 36 ++++----- .../server/src/procedure-decorated.test-d.ts | 44 +++++------ packages/server/src/procedure-decorated.ts | 79 ++++++++----------- .../src/procedure-implementer.test-d.ts | 42 +++++----- packages/server/src/procedure-implementer.ts | 54 +++++-------- packages/server/src/procedure-utils.test-d.ts | 4 +- packages/server/src/procedure-utils.ts | 8 +- packages/server/src/procedure.ts | 30 +++---- packages/server/src/router-builder.test-d.ts | 55 +++++++------ packages/server/src/router-builder.ts | 67 ++++++++-------- packages/server/src/router-client.test-d.ts | 11 ++- .../server/src/router-implementer.test-d.ts | 21 ++--- packages/server/src/router-implementer.ts | 42 +++++----- packages/server/src/router.test-d.ts | 12 +-- packages/server/src/router.ts | 8 +- packages/server/src/types.test-d.ts | 18 ----- packages/server/src/types.ts | 8 -- packages/server/src/utils.test.ts | 16 ---- packages/server/src/utils.ts | 16 ---- 59 files changed, 819 insertions(+), 772 deletions(-) delete mode 100644 packages/server/src/types.test-d.ts delete mode 100644 packages/server/src/utils.test.ts delete mode 100644 packages/server/src/utils.ts diff --git a/apps/content/content/docs/server/lazy.mdx b/apps/content/content/docs/server/lazy.mdx index eb495a96..e87ccdaa 100644 --- a/apps/content/content/docs/server/lazy.mdx +++ b/apps/content/content/docs/server/lazy.mdx @@ -16,7 +16,7 @@ Here's how you can set up and use them: ```typescript twoslash import { os, call } from '@orpc/server' -const pub = os.context<{ user?: { id: string } } | undefined>() +const pub = os.context<{ user?: { id: string } }>() // Define a router with lazy loading const router = pub.router({ diff --git a/apps/content/content/docs/server/server-action.mdx b/apps/content/content/docs/server/server-action.mdx index fe6db999..366f8ffb 100644 --- a/apps/content/content/docs/server/server-action.mdx +++ b/apps/content/content/docs/server/server-action.mdx @@ -43,7 +43,7 @@ When calling `.actionable()`, you can pass a context function that provides addi import { os } from '@orpc/server' import { z } from 'zod' -const pub = os.context<{ db: string } | undefined>() +const pub = os.context<{ db: string }>() export const getting = pub .input(z.object({ diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index cca5ed25..19c30dfe 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -3,13 +3,13 @@ import { ORPCError, os } from '@orpc/server' import { oz, ZodCoercer } from '@orpc/zod' import { z } from 'zod' -export type Context = { user?: { id: string } } | undefined +export type Context = { user?: { id: string } } // global pub, authed completely optional export const pub = os.context() export const authed = pub.use(({ context, path, next }, input) => { /** put auth logic here */ - return next({}) + return next() }) export const router = pub.router({ diff --git a/packages/openapi/src/adapters/fetch/openapi-handler.ts b/packages/openapi/src/adapters/fetch/openapi-handler.ts index d24a244b..60b191a0 100644 --- a/packages/openapi/src/adapters/fetch/openapi-handler.ts +++ b/packages/openapi/src/adapters/fetch/openapi-handler.ts @@ -45,7 +45,7 @@ export class OpenAPIHandler implements FetchHandler { } async handle(request: Request, ...[options]: FetchHandleRest): Promise { - const context = options?.context as T + const context = options?.context ?? {} as T const headers = request.headers const accept = headers.get('accept') || undefined diff --git a/packages/server/src/adapters/fetch/orpc-handler.ts b/packages/server/src/adapters/fetch/orpc-handler.ts index b58d9b6b..fab695c7 100644 --- a/packages/server/src/adapters/fetch/orpc-handler.ts +++ b/packages/server/src/adapters/fetch/orpc-handler.ts @@ -1,6 +1,6 @@ import type { Hooks } from '@orpc/shared' +import type { Context } from '../../context' import type { Router } from '../../router' -import type { Context } from '../../types' import type { FetchHandler, FetchHandleRest, FetchHandleResult } from './types' import { ORPCError } from '@orpc/contract' import { executeWithHooks, trim } from '@orpc/shared' @@ -25,7 +25,7 @@ export class RPCHandler implements FetchHandler { } async handle(request: Request, ...[options]: FetchHandleRest): Promise { - const context = options?.context as T + const context = options?.context ?? {} as T const execute = async (): Promise => { const url = new URL(request.url) diff --git a/packages/server/src/adapters/fetch/types.test-d.ts b/packages/server/src/adapters/fetch/types.test-d.ts index b01e6bf5..2f113700 100644 --- a/packages/server/src/adapters/fetch/types.test-d.ts +++ b/packages/server/src/adapters/fetch/types.test-d.ts @@ -1,8 +1,8 @@ import type { FetchHandler } from './types' describe('FetchHandler', () => { - it('optional context when context is undefinable', () => { - const handler = {} as FetchHandler<{ auth: boolean } | undefined> + it('optional context when all context is optional', () => { + const handler = {} as FetchHandler<{ auth?: boolean }> handler.handle(new Request('https://example.com')) handler.handle(new Request('https://example.com'), { context: { auth: true } }) diff --git a/packages/server/src/adapters/fetch/types.ts b/packages/server/src/adapters/fetch/types.ts index 6a32f4d9..cb82c3bb 100644 --- a/packages/server/src/adapters/fetch/types.ts +++ b/packages/server/src/adapters/fetch/types.ts @@ -1,11 +1,14 @@ import type { HTTPPath } from '@orpc/contract' -import type { Context } from '../../types' +import type { Context } from '../../context' export type FetchHandleOptions = & { prefix?: HTTPPath } - & (undefined extends T ? { context?: T } : { context: T }) + & (Record extends T ? { context?: T } : { context: T }) + +export type FetchHandleRest = + | [options: FetchHandleOptions] + | (Record extends T ? [] : never) -export type FetchHandleRest = [options: FetchHandleOptions] | (undefined extends T ? [] : never) export type FetchHandleResult = { matched: true, response: Response } | { matched: false, response: undefined } export interface FetchHandler { diff --git a/packages/server/src/adapters/hono/middleware.ts b/packages/server/src/adapters/hono/middleware.ts index 7a05ee04..e1c65f47 100644 --- a/packages/server/src/adapters/hono/middleware.ts +++ b/packages/server/src/adapters/hono/middleware.ts @@ -1,19 +1,21 @@ import type { Context as HonoContext, MiddlewareHandler } from 'hono' -import type { Context } from '../../types' +import type { Context } from '../../context' import type { FetchHandleOptions, FetchHandler } from '../fetch' import { value, type Value } from '@orpc/shared' export type CreateMiddlewareOptions = & Omit, 'context'> - & (undefined extends T ? { context?: Value } : { context: Value }) + & (Record extends T ? { context?: Value } : { context: Value }) -export type CreateMiddlewareRest = [options: CreateMiddlewareOptions] | (undefined extends T ? [] : never) +export type CreateMiddlewareRest = + | [options: CreateMiddlewareOptions] + | (Record extends T ? [] : never) export function createMiddleware(handler: FetchHandler, ...[options]: CreateMiddlewareRest): MiddlewareHandler { return async (c, next) => { - const context = await value(options?.context, c) + const context = await value(options?.context ?? {}, c) as any - const { matched, response } = await handler.handle(c.req.raw, { ...options, context } as any) + const { matched, response } = await handler.handle(c.req.raw, { ...options, context }) if (matched) { c.res = response diff --git a/packages/server/src/adapters/next/serve.ts b/packages/server/src/adapters/next/serve.ts index 316dcee6..32d97d7d 100644 --- a/packages/server/src/adapters/next/serve.ts +++ b/packages/server/src/adapters/next/serve.ts @@ -1,13 +1,15 @@ import type { NextRequest } from 'next/server' -import type { Context } from '../../types' +import type { Context } from '../../context' import type { FetchHandleOptions, FetchHandler } from '../fetch' import { value, type Value } from '@orpc/shared' export type ServeOptions = & Omit, 'context'> - & (undefined extends T ? { context?: Value } : { context: Value }) + & (Record extends T ? { context?: Value } : { context: Value }) -export type ServeRest = [options: ServeOptions] | (undefined extends T ? [] : never) +export type ServeRest = + | [options: ServeOptions] + | (Record extends T ? [] : never) export interface ServeResult { GET: (req: NextRequest) => Promise @@ -19,7 +21,7 @@ export interface ServeResult { export function serve(handler: FetchHandler, ...[options]: ServeRest): ServeResult { const main = async (req: NextRequest) => { - const context = await value(options?.context, req) as any + const context = await value(options?.context ?? {}, req) as any const { matched, response } = await handler.handle(req, { ...options, context }) diff --git a/packages/server/src/adapters/node/orpc-handler.ts b/packages/server/src/adapters/node/orpc-handler.ts index 41eb74f7..8d966970 100644 --- a/packages/server/src/adapters/node/orpc-handler.ts +++ b/packages/server/src/adapters/node/orpc-handler.ts @@ -1,6 +1,6 @@ import type { ServerResponse } from 'node:http' +import type { Context } from '../../context' import type { Router } from '../../router' -import type { Context } from '../../types' import type { RPCHandlerOptions } from '../fetch/orpc-handler' import type { RequestHandler, RequestHandleRest, RequestHandleResult } from './types' import { RPCHandler as ORPCFetchHandler } from '../fetch/orpc-handler' @@ -13,18 +13,18 @@ export class RPCHandler implements RequestHandler { this.orpcFetchHandler = new ORPCFetchHandler(router, options) } - async handle(req: ExpressableIncomingMessage, res: ServerResponse, ...[options]: RequestHandleRest): Promise { + async handle(req: ExpressableIncomingMessage, res: ServerResponse, ...rest: RequestHandleRest): Promise { const request = createRequest(req, res) - const castedOptions = (options ?? {}) as Exclude - - const result = await this.orpcFetchHandler.handle(request, castedOptions) + const result = await this.orpcFetchHandler.handle(request, ...rest) if (result.matched === false) { return { matched: false } } - await options?.beforeSend?.(result.response, castedOptions.context as T) + const context = rest[0]?.context ?? {} as T + + await rest[0]?.beforeSend?.(result.response, context) await sendResponse(res, result.response) diff --git a/packages/server/src/adapters/node/types.ts b/packages/server/src/adapters/node/types.ts index 3a7d4efb..20dcf076 100644 --- a/packages/server/src/adapters/node/types.ts +++ b/packages/server/src/adapters/node/types.ts @@ -3,13 +3,15 @@ import type { HTTPPath } from '@orpc/contract' import type { Promisable } from '@orpc/shared' import type { IncomingMessage, ServerResponse } from 'node:http' -import type { Context } from '../../types' +import type { Context } from '../../context' export type RequestHandleOptions = & { prefix?: HTTPPath, beforeSend?: (response: Response, context: T) => Promisable } - & (undefined extends T ? { context?: T } : { context: T }) + & (Record extends T ? { context?: T } : { context: T }) -export type RequestHandleRest = [options: RequestHandleOptions] | (undefined extends T ? [] : never) +export type RequestHandleRest = + | [options: RequestHandleOptions] + | (Record extends T ? [] : never) export type RequestHandleResult = { matched: true } | { matched: false } diff --git a/packages/server/src/builder-with-errors-middlewares.test-d.ts b/packages/server/src/builder-with-errors-middlewares.test-d.ts index f2616419..d99cde57 100644 --- a/packages/server/src/builder-with-errors-middlewares.test-d.ts +++ b/packages/server/src/builder-with-errors-middlewares.test-d.ts @@ -1,4 +1,5 @@ import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Lazy } from './lazy' import type { MiddlewareOutputFn } from './middleware' @@ -8,7 +9,6 @@ import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' -import type { WELL_CONTEXT } from './types' import { z } from 'zod' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) @@ -26,11 +26,11 @@ const errors = { }, } -const builder = {} as BuilderWithErrorsMiddlewares<{ db: string }, { auth?: boolean }, typeof baseErrors> +const builder = {} as BuilderWithErrorsMiddlewares<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> describe('BuilderWithErrorsMiddlewares', () => { it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf>() + expectTypeOf(builder.errors(errors)).toEqualTypeOf>() // @ts-expect-error --- not allow redefine error map builder.errors({ BASE: baseErrors.BASE }) @@ -39,7 +39,7 @@ describe('BuilderWithErrorsMiddlewares', () => { it('.use', () => { const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() + expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() expectTypeOf(path).toEqualTypeOf() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(output).toEqualTypeOf>() @@ -52,7 +52,7 @@ describe('BuilderWithErrorsMiddlewares', () => { }) }) - expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { auth?: boolean } & { extra: boolean }, typeof baseErrors>>() + expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { db: string, auth?: boolean } & { extra: boolean }, typeof baseErrors>>() // @ts-expect-error --- conflict context builder.use(({ next }) => next({ db: 123 })) @@ -64,26 +64,26 @@ describe('BuilderWithErrorsMiddlewares', () => { it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors> + ProcedureBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> >() }) it('.input', () => { expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { auth?: boolean }, typeof schema, typeof baseErrors> + ProcedureBuilderWithInput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors> >() }) it('.output', () => { expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { auth?: boolean }, typeof schema, typeof baseErrors> + ProcedureBuilderWithOutput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors> >() }) it('.handler', () => { const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() + expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(path).toEqualTypeOf() expectTypeOf(signal).toEqualTypeOf>() @@ -93,26 +93,26 @@ describe('BuilderWithErrorsMiddlewares', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { auth?: boolean }, undefined, undefined, number, typeof baseErrors> + DecoratedProcedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, number, typeof baseErrors> >() }) it('.prefix', () => { expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors> + RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> >() }) it('.tag', () => { expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors> + RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors> >() }) it('.router', () => { const router = { - ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.router(router)).toEqualTypeOf< @@ -121,7 +121,7 @@ describe('BuilderWithErrorsMiddlewares', () => { builder.router({ // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, }) const invalidErrorMap = { @@ -133,14 +133,14 @@ describe('BuilderWithErrorsMiddlewares', () => { builder.router({ // @ts-expect-error - error map is not match - ping: {} as ContractProcedure, + ping: {} as Procedure, }) }) it('.lazy', () => { const router = { - ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< @@ -149,13 +149,13 @@ describe('BuilderWithErrorsMiddlewares', () => { // @ts-expect-error - context is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, } })) // @ts-expect-error - error map is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure, + ping: {} as Procedure, }, })) }) diff --git a/packages/server/src/builder-with-errors-middlewares.ts b/packages/server/src/builder-with-errors-middlewares.ts index fd3b42ac..795a0dfa 100644 --- a/packages/server/src/builder-with-errors-middlewares.ts +++ b/packages/server/src/builder-with-errors-middlewares.ts @@ -1,12 +1,11 @@ import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput, StrictErrorMap } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { ProcedureHandler } from './procedure' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context, MergeContext } from './types' import { ContractProcedure } from '@orpc/contract' import { ProcedureBuilder } from './procedure-builder' import { ProcedureBuilderWithInput } from './procedure-builder-with-input' @@ -14,10 +13,15 @@ import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import { DecoratedProcedure } from './procedure-decorated' import { RouterBuilder } from './router-builder' -export interface BuilderWithErrorsMiddlewaresDef { - types?: { context: TContext } +export interface BuilderWithErrorsMiddlewaresDef< + TInitialContext extends Context, + TCurrentContext extends Context, + TErrorMap extends ErrorMap, +> { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext errorMap: TErrorMap - middlewares: Middleware, Partial | undefined, unknown, any, ORPCErrorConstructorMap>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number config: ContractBuilderConfig @@ -32,15 +36,21 @@ export interface BuilderWithErrorsMiddlewaresDef { +export class BuilderWithErrorsMiddlewares< + TInitialContext extends Context, + TCurrentContext extends Context, + TErrorMap extends ErrorMap, +> { '~type' = 'BuilderWithErrorsMiddlewares' as const - '~orpc': BuilderWithErrorsMiddlewaresDef + '~orpc': BuilderWithErrorsMiddlewaresDef - constructor(def: BuilderWithErrorsMiddlewaresDef) { + constructor(def: BuilderWithErrorsMiddlewaresDef) { this['~orpc'] = def } - errors & ErrorMapSuggestions>(errors: U): BuilderWithErrorsMiddlewares { + errors & ErrorMapSuggestions>( + errors: U, + ): BuilderWithErrorsMiddlewares { return new BuilderWithErrorsMiddlewares({ ...this['~orpc'], errorMap: { @@ -50,18 +60,22 @@ export class BuilderWithErrorsMiddlewares>>( - middleware: Middleware, U, unknown, unknown, ORPCErrorConstructorMap>, - ): BuilderWithErrorsMiddlewares, TErrorMap> { - return new BuilderWithErrorsMiddlewares, TErrorMap>({ - ...this['~orpc'], + use( + middleware: Middleware>, + ): ConflictContextGuard + & BuilderWithErrorsMiddlewares { + const builder = new BuilderWithErrorsMiddlewares({ + config: this['~orpc'].config, + errorMap: this['~orpc'].errorMap, inputValidationIndex: this['~orpc'].inputValidationIndex + 1, outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware as any], // FIXME: I believe we can remove `as any` here + middlewares: [...this['~orpc'].middlewares, middleware], }) + + return builder as typeof builder & ConflictContextGuard } - route(route: RouteOptions): ProcedureBuilder { + route(route: RouteOptions): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: new ContractProcedure({ @@ -76,7 +90,10 @@ export class BuilderWithErrorsMiddlewares(schema: USchema, example?: SchemaInput): ProcedureBuilderWithInput { + input( + schema: USchema, + example?: SchemaInput, + ): ProcedureBuilderWithInput { return new ProcedureBuilderWithInput({ ...this['~orpc'], contract: new ContractProcedure({ @@ -89,7 +106,10 @@ export class BuilderWithErrorsMiddlewares(schema: USchema, example?: SchemaOutput): ProcedureBuilderWithOutput { + output( + schema: USchema, + example?: SchemaOutput, + ): ProcedureBuilderWithOutput { return new ProcedureBuilderWithOutput({ ...this['~orpc'], contract: new ContractProcedure({ @@ -102,7 +122,9 @@ export class BuilderWithErrorsMiddlewares(handler: ProcedureHandler): DecoratedProcedure { + handler( + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], contract: new ContractProcedure({ @@ -115,29 +137,29 @@ export class BuilderWithErrorsMiddlewares { + prefix(prefix: HTTPPath): RouterBuilder { return new RouterBuilder({ ...this['~orpc'], prefix, }) } - tag(...tags: string[]): RouterBuilder { + tag(...tags: string[]): RouterBuilder { return new RouterBuilder({ ...this['~orpc'], tags, }) } - router, ContractRouter>>>>( + router>>>>( router: U, - ): AdaptedRouter { - return new RouterBuilder(this['~orpc']).router(router) + ): AdaptedRouter { + return new RouterBuilder(this['~orpc']).router(router) } - lazy, ContractRouter>>>>( + lazy>>>>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { - return new RouterBuilder(this['~orpc']).lazy(loader) + ): AdaptedRouter, TErrorMap> { + return new RouterBuilder(this['~orpc']).lazy(loader) } } diff --git a/packages/server/src/builder-with-errors.test-d.ts b/packages/server/src/builder-with-errors.test-d.ts index 80596c79..d09f8b1c 100644 --- a/packages/server/src/builder-with-errors.test-d.ts +++ b/packages/server/src/builder-with-errors.test-d.ts @@ -1,5 +1,6 @@ import type { BuilderWithErrors } from './builder-with-errors' import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Lazy } from './lazy' import type { MiddlewareOutputFn } from './middleware' @@ -10,7 +11,6 @@ import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' -import type { WELL_CONTEXT } from './types' import { z } from 'zod' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) @@ -33,7 +33,10 @@ const builder = {} as BuilderWithErrors<{ db: string }, typeof baseErrors> describe('BuilderWithErrors', () => { it('.context', () => { expectTypeOf(builder.context()).toEqualTypeOf>() - expectTypeOf(builder.context<{ anything: string }>()).toEqualTypeOf>() + expectTypeOf(builder.context<{ db: string, anything: string }>()).toEqualTypeOf>() + + // @ts-expect-error - new context must satisfy old context + builder.context<{ anything: string }>() }) it('.config', () => { @@ -66,7 +69,7 @@ describe('BuilderWithErrors', () => { const mid2 = builder.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next({})) expectTypeOf(mid2).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, undefined, 'input', 'output', ORPCErrorConstructorMap> + DecoratedMiddleware<{ db: string }, Record, 'input', 'output', ORPCErrorConstructorMap> >() // @ts-expect-error --- conflict context @@ -96,7 +99,7 @@ describe('BuilderWithErrors', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { db: string } & { extra: boolean }, typeof baseErrors>>() // @ts-expect-error --- conflict context builder.use(({ next }) => next({ db: 123 })) @@ -108,19 +111,19 @@ describe('BuilderWithErrors', () => { it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, undefined, typeof baseErrors> + ProcedureBuilder<{ db: string }, { db: string }, typeof baseErrors> >() }) it('.input', () => { expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, undefined, typeof schema, typeof baseErrors> + ProcedureBuilderWithInput<{ db: string }, { db: string }, typeof schema, typeof baseErrors> >() }) it('.output', () => { expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, undefined, typeof schema, typeof baseErrors> + ProcedureBuilderWithOutput<{ db: string }, { db: string }, typeof schema, typeof baseErrors> >() }) @@ -137,26 +140,26 @@ describe('BuilderWithErrors', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, undefined, undefined, undefined, number, typeof baseErrors> + DecoratedProcedure<{ db: string }, { db: string }, undefined, undefined, number, typeof baseErrors> >() }) it('.prefix', () => { expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, undefined, typeof baseErrors> + RouterBuilder<{ db: string }, { db: string }, typeof baseErrors> >() }) it('.tag', () => { expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, undefined, typeof baseErrors> + RouterBuilder<{ db: string }, { db: string }, typeof baseErrors> >() }) it('.router', () => { const router = { - ping: {} as Procedure<{ db: string }, undefined, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.router(router)).toEqualTypeOf< @@ -183,8 +186,8 @@ describe('BuilderWithErrors', () => { it('.lazy', () => { const router = { - ping: {} as Procedure<{ db: string }, undefined, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< @@ -193,13 +196,13 @@ describe('BuilderWithErrors', () => { // @ts-expect-error - context is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, } })) // @ts-expect-error - error map is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure, + ping: {} as Procedure, }, })) }) diff --git a/packages/server/src/builder-with-errors.ts b/packages/server/src/builder-with-errors.ts index 0b03681a..75ab32c1 100644 --- a/packages/server/src/builder-with-errors.ts +++ b/packages/server/src/builder-with-errors.ts @@ -1,6 +1,6 @@ import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput, StrictErrorMap } from '@orpc/contract' import type { BuilderConfig } from './builder' -import type { ContextGuard } from './context' +import type { Context, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' @@ -8,7 +8,6 @@ import type { DecoratedMiddleware } from './middleware-decorated' import type { ProcedureHandler } from './procedure' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context, MergeContext } from './types' import { ContractProcedure } from '@orpc/contract' import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' import { fallbackConfig } from './config' @@ -19,8 +18,8 @@ import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import { DecoratedProcedure } from './procedure-decorated' import { RouterBuilder } from './router-builder' -export interface BuilderWithErrorsDef { - types?: { context: TContext } +export interface BuilderWithErrorsDef { + __initialContext?: TypeInitialContext errorMap: TErrorMap config: BuilderConfig } @@ -32,15 +31,15 @@ export interface BuilderWithErrorsDef { +export class BuilderWithErrors { '~type' = 'BuilderWithErrors' as const - '~orpc': BuilderWithErrorsDef + '~orpc': BuilderWithErrorsDef - constructor(def: BuilderWithErrorsDef) { + constructor(def: BuilderWithErrorsDef) { this['~orpc'] = def } - config(config: ContractBuilderConfig): BuilderWithErrors { + config(config: ContractBuilderConfig): BuilderWithErrors { return new BuilderWithErrors({ ...this['~orpc'], config: { @@ -50,11 +49,13 @@ export class BuilderWithErrors(): BuilderWithErrors { + context(): BuilderWithErrors { return this as any // just change at type level so safely cast here } - errors & ErrorMapSuggestions>(errors: U): BuilderWithErrors { + errors & ErrorMapSuggestions>( + errors: U, + ): BuilderWithErrors { return new BuilderWithErrors({ ...this['~orpc'], errorMap: { @@ -64,24 +65,24 @@ export class BuilderWithErrors, TInput, TOutput = any>( - middleware: Middleware>, - ): DecoratedMiddleware> { + middleware( + middleware: Middleware>, + ): DecoratedMiddleware> { return decorateMiddleware(middleware) } - use>( - middleware: Middleware>, - ): BuilderWithErrorsMiddlewares { - return new BuilderWithErrorsMiddlewares({ + use( + middleware: Middleware>, + ): BuilderWithErrorsMiddlewares { + return new BuilderWithErrorsMiddlewares({ ...this['~orpc'], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + 1, outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + 1, - middlewares: [middleware as any], // FIXME: I believe we can remove `as any` here + middlewares: [middleware], }) } - route(route: RouteOptions): ProcedureBuilder { + route(route: RouteOptions): ProcedureBuilder { return new ProcedureBuilder({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -98,7 +99,10 @@ export class BuilderWithErrors(schema: USchema, example?: SchemaInput): ProcedureBuilderWithInput { + input( + schema: USchema, + example?: SchemaInput, + ): ProcedureBuilderWithInput { return new ProcedureBuilderWithInput({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -113,7 +117,10 @@ export class BuilderWithErrors(schema: USchema, example?: SchemaOutput): ProcedureBuilderWithOutput { + output( + schema: USchema, + example?: SchemaOutput, + ): ProcedureBuilderWithOutput { return new ProcedureBuilderWithOutput({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -128,7 +135,9 @@ export class BuilderWithErrors(handler: ProcedureHandler): DecoratedProcedure { + handler( + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -143,7 +152,7 @@ export class BuilderWithErrors { + prefix(prefix: HTTPPath): RouterBuilder { return new RouterBuilder({ middlewares: [], errorMap: this['~orpc'].errorMap, @@ -151,7 +160,7 @@ export class BuilderWithErrors { + tag(...tags: string[]): RouterBuilder { return new RouterBuilder({ middlewares: [], errorMap: this['~orpc'].errorMap, @@ -159,19 +168,19 @@ export class BuilderWithErrors, ContractRouter>>>>( + router>>>>( router: U, - ): AdaptedRouter { - return new RouterBuilder({ + ): AdaptedRouter { + return new RouterBuilder({ middlewares: [], ...this['~orpc'], }).router(router) } - lazy, ContractRouter>>>>( + lazy>>>>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { - return new RouterBuilder({ + ): AdaptedRouter, TErrorMap> { + return new RouterBuilder({ middlewares: [], ...this['~orpc'], }).lazy(loader) diff --git a/packages/server/src/builder-with-middlewares.test-d.ts b/packages/server/src/builder-with-middlewares.test-d.ts index 947bd047..d5d00c3b 100644 --- a/packages/server/src/builder-with-middlewares.test-d.ts +++ b/packages/server/src/builder-with-middlewares.test-d.ts @@ -1,5 +1,6 @@ import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' import type { BuilderWithMiddlewares } from './builder-with-middlewares' +import type { Context } from './context' import type { ChainableImplementer } from './implementer-chainable' import type { Lazy } from './lazy' import type { MiddlewareOutputFn } from './middleware' @@ -9,7 +10,6 @@ import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' -import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' @@ -22,13 +22,13 @@ const errors = { }, } -const builder = {} as BuilderWithMiddlewares<{ db: string }, { auth?: boolean }> +const builder = {} as BuilderWithMiddlewares<{ db: string }, { db: string, auth?: boolean }> describe('BuilderWithMiddlewares', () => { it('.use', () => { const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() + expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() expectTypeOf(path).toEqualTypeOf() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(output).toEqualTypeOf>() @@ -41,10 +41,12 @@ describe('BuilderWithMiddlewares', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf>() // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({})) // @ts-expect-error --- output is not match @@ -52,31 +54,31 @@ describe('BuilderWithMiddlewares', () => { }) it('.errors', () => { - expectTypeOf(builder.errors(errors)).toEqualTypeOf>() + expectTypeOf(builder.errors(errors)).toEqualTypeOf>() }) it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, { auth?: boolean }, Record> + ProcedureBuilder<{ db: string }, { db: string, auth?: boolean }, Record> >() }) it('.input', () => { expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { auth?: boolean }, typeof schema, Record> + ProcedureBuilderWithInput<{ db: string }, { db: string, auth?: boolean }, typeof schema, Record> >() }) it('.output', () => { expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { auth?: boolean }, typeof schema, Record> + ProcedureBuilderWithOutput<{ db: string }, { db: string, auth?: boolean }, typeof schema, Record> >() }) it('.handler', () => { const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>() + expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(path).toEqualTypeOf() expectTypeOf(signal).toEqualTypeOf>() @@ -86,26 +88,26 @@ describe('BuilderWithMiddlewares', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { auth?: boolean }, undefined, undefined, number, Record> + DecoratedProcedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, number, Record> >() }) it('.prefix', () => { expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, { auth?: boolean }, Record> + RouterBuilder<{ db: string }, { db: string, auth?: boolean }, Record> >() }) it('.tag', () => { expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, { auth?: boolean }, Record> + RouterBuilder<{ db: string }, { db: string, auth?: boolean }, Record> >() }) it('.router', () => { const router = { - ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.router(router)).toEqualTypeOf< @@ -114,14 +116,14 @@ describe('BuilderWithMiddlewares', () => { builder.router({ // @ts-expect-error - context is not match - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, }) }) it('.lazy', () => { const router = { - ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< @@ -130,7 +132,7 @@ describe('BuilderWithMiddlewares', () => { // @ts-expect-error - context is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, } })) }) @@ -140,7 +142,7 @@ describe('BuilderWithMiddlewares', () => { }) expectTypeOf(builder.contract(contract)).toEqualTypeOf< - ChainableImplementer<{ db: string }, { auth?: boolean }, typeof contract> + ChainableImplementer<{ db: string }, { db: string, auth?: boolean }, typeof contract> >() }) }) diff --git a/packages/server/src/builder-with-middlewares.ts b/packages/server/src/builder-with-middlewares.ts index 8c8c3d81..28c66b70 100644 --- a/packages/server/src/builder-with-middlewares.ts +++ b/packages/server/src/builder-with-middlewares.ts @@ -1,11 +1,10 @@ import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapSuggestions, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { ProcedureHandler } from './procedure' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context, MergeContext } from './types' import { ContractProcedure } from '@orpc/contract' import { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares' import { type ChainableImplementer, createChainableImplementer } from './implementer-chainable' @@ -23,46 +22,47 @@ import { RouterBuilder } from './router-builder' * - prevents .context after .use (middlewares required current context, so it tricky when change the current context) * */ -export interface BuilderWithMiddlewaresDef { +export interface BuilderWithMiddlewaresDef { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext config: ContractBuilderConfig - middlewares: Middleware, Partial | undefined, unknown, any, Record>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number } -export class BuilderWithMiddlewares { +export class BuilderWithMiddlewares { '~type' = 'BuilderHasMiddlewares' as const - '~orpc': BuilderWithMiddlewaresDef + '~orpc': BuilderWithMiddlewaresDef - constructor(def: BuilderWithMiddlewaresDef) { + constructor(def: BuilderWithMiddlewaresDef) { this['~orpc'] = def } - use>>( - middleware: Middleware< - MergeContext, - U, - unknown, - unknown, - Record - >, - ): BuilderWithMiddlewares> { - return new BuilderWithMiddlewares({ - ...this['~orpc'], + use( + middleware: Middleware >, + ): ConflictContextGuard + & BuilderWithMiddlewares { + const builder = new BuilderWithMiddlewares({ + config: this['~orpc'].config, inputValidationIndex: this['~orpc'].inputValidationIndex + 1, outputValidationIndex: this['~orpc'].outputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware as any], + middlewares: [...this['~orpc'].middlewares, middleware], }) + + return builder as typeof builder & ConflictContextGuard } - errors(errors: U): BuilderWithErrorsMiddlewares { + errors( + errors: U, + ): BuilderWithErrorsMiddlewares { return new BuilderWithErrorsMiddlewares({ ...this['~orpc'], errorMap: errors, }) } - route(route: RouteOptions): ProcedureBuilder> { + route(route: RouteOptions): ProcedureBuilder> { return new ProcedureBuilder({ ...this['~orpc'], contract: new ContractProcedure({ @@ -80,7 +80,7 @@ export class BuilderWithMiddlewares( schema: USchema, example?: SchemaInput, - ): ProcedureBuilderWithInput> { + ): ProcedureBuilderWithInput> { return new ProcedureBuilderWithInput({ ...this['~orpc'], contract: new ContractProcedure({ @@ -96,7 +96,7 @@ export class BuilderWithMiddlewares( schema: USchema, example?: SchemaOutput, - ): ProcedureBuilderWithOutput> { + ): ProcedureBuilderWithOutput> { return new ProcedureBuilderWithOutput({ ...this['~orpc'], contract: new ContractProcedure({ @@ -110,8 +110,8 @@ export class BuilderWithMiddlewares( - handler: ProcedureHandler>, - ): DecoratedProcedure> { + handler: ProcedureHandler>, + ): DecoratedProcedure> { return new DecoratedProcedure({ ...this['~orpc'], contract: new ContractProcedure({ @@ -124,7 +124,7 @@ export class BuilderWithMiddlewares> { + prefix(prefix: HTTPPath): RouterBuilder> { return new RouterBuilder({ middlewares: this['~orpc'].middlewares, errorMap: {}, @@ -132,7 +132,7 @@ export class BuilderWithMiddlewares> { + tag(...tags: string[]): RouterBuilder> { return new RouterBuilder({ middlewares: this['~orpc'].middlewares, errorMap: {}, @@ -140,19 +140,19 @@ export class BuilderWithMiddlewares, any>>( + router>( router: U, - ): AdaptedRouter> { - return new RouterBuilder>({ + ): AdaptedRouter> { + return new RouterBuilder>({ errorMap: {}, ...this['~orpc'], }).router(router) } - lazy, any>>( + lazy>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, Record> { - return new RouterBuilder>({ + ): AdaptedRouter, Record> { + return new RouterBuilder>({ errorMap: {}, ...this['~orpc'], }).lazy(loader) @@ -160,7 +160,7 @@ export class BuilderWithMiddlewares>( contract: U, - ): ChainableImplementer { + ): ChainableImplementer { return createChainableImplementer(contract, this['~orpc']) } } diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts index f3d39918..a6f03653 100644 --- a/packages/server/src/builder.test-d.ts +++ b/packages/server/src/builder.test-d.ts @@ -1,6 +1,7 @@ import type { Builder } from './builder' import type { BuilderWithErrors } from './builder-with-errors' import type { BuilderWithMiddlewares } from './builder-with-middlewares' +import type { Context } from './context' import type { ChainableImplementer } from './implementer-chainable' import type { Lazy } from './lazy' import type { MiddlewareOutputFn } from './middleware' @@ -11,7 +12,6 @@ import type { ProcedureBuilderWithInput } from './procedure-builder-with-input' import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' -import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' @@ -29,7 +29,10 @@ const builder = {} as Builder<{ db: string }> describe('Builder', () => { it('.context', () => { expectTypeOf(builder.context()).toEqualTypeOf>() - expectTypeOf(builder.context<{ anything: string }>()).toEqualTypeOf>() + expectTypeOf(builder.context<{ db: string, anything: string }>()).toEqualTypeOf>() + + // @ts-expect-error - new context must satisfy old context + builder.context<{ anything: string }>() }) it('.config', () => { @@ -62,7 +65,7 @@ describe('Builder', () => { const mid2 = builder.middleware(({ next }, input: 'input', output: MiddlewareOutputFn<'output'>) => next({})) expectTypeOf(mid2).toEqualTypeOf< - DecoratedMiddleware<{ db: string }, undefined, 'input', 'output', Record> + DecoratedMiddleware<{ db: string }, Record, 'input', 'output', Record> >() // @ts-expect-error --- conflict context @@ -89,10 +92,12 @@ describe('Builder', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf>() // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) + builder.use(({ next }) => next({ context: { db: 123 } })) + // conflict but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf() // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({})) // @ts-expect-error --- output is not match @@ -101,19 +106,19 @@ describe('Builder', () => { it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< - ProcedureBuilder<{ db: string }, undefined, Record> + ProcedureBuilder<{ db: string }, { db: string }, Record> >() }) it('.input', () => { expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, undefined, typeof schema, Record> + ProcedureBuilderWithInput<{ db: string }, { db: string }, typeof schema, Record> >() }) it('.output', () => { expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, undefined, typeof schema, Record> + ProcedureBuilderWithOutput<{ db: string }, { db: string }, typeof schema, Record> >() }) @@ -130,26 +135,26 @@ describe('Builder', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, undefined, undefined, undefined, number, Record> + DecoratedProcedure<{ db: string }, { db: string }, undefined, undefined, number, Record> >() }) it('.prefix', () => { expectTypeOf(builder.prefix('/test')).toEqualTypeOf< - RouterBuilder<{ db: string }, undefined, Record> + RouterBuilder<{ db: string }, { db: string }, Record> >() }) it('.tag', () => { expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< - RouterBuilder<{ db: string }, undefined, Record> + RouterBuilder<{ db: string }, { db: string }, Record> >() }) it('.router', () => { const router = { - ping: {} as Procedure<{ db: string }, undefined, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.router(router)).toEqualTypeOf< @@ -164,8 +169,8 @@ describe('Builder', () => { it('.lazy', () => { const router = { - ping: {} as Procedure<{ db: string }, undefined, undefined, undefined, unknown, typeof errors>, - pong: {} as Procedure>, + ping: {} as Procedure<{ db: string }, { db: string }, undefined, undefined, unknown, typeof errors>, + pong: {} as Procedure>, } expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< @@ -174,7 +179,7 @@ describe('Builder', () => { // @ts-expect-error - context is not match builder.lazy(() => Promise.resolve({ default: { - ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>, + ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>, } })) }) @@ -184,7 +189,7 @@ describe('Builder', () => { }) expectTypeOf(builder.contract(contract)).toEqualTypeOf< - ChainableImplementer<{ db: string }, undefined, typeof contract> + ChainableImplementer<{ db: string }, { db: string }, typeof contract> >() }) }) diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index 7e0cbd1d..15b1d77e 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,12 +1,11 @@ import type { ContractBuilderConfig, ContractRouter, ErrorMap, ErrorMapSuggestions, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeInitialContext } from './context' import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' import type { ProcedureHandler } from './procedure' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context, MergeContext } from './types' import { ContractProcedure } from '@orpc/contract' import { BuilderWithErrors } from './builder-with-errors' import { BuilderWithMiddlewares } from './builder-with-middlewares' @@ -24,20 +23,20 @@ export interface BuilderConfig extends ContractBuilderConfig { initialOutputValidationIndex?: number } -export interface BuilderDef { - types?: { context: TContext } +export interface BuilderDef { + __initialContext?: TypeInitialContext config: BuilderConfig } -export class Builder { +export class Builder { '~type' = 'Builder' as const - '~orpc': BuilderDef + '~orpc': BuilderDef - constructor(def: BuilderDef) { + constructor(def: BuilderDef) { this['~orpc'] = def } - config(config: ContractBuilderConfig): Builder { + config(config: ContractBuilderConfig): Builder { return new Builder({ ...this['~orpc'], config: { @@ -47,35 +46,38 @@ export class Builder { }) } - context(): Builder { + context(): Builder { return this as any // just change at type level so safely cast here } - middleware, TInput, TOutput = any >( - middleware: Middleware>, - ): DecoratedMiddleware> { + middleware( + middleware: Middleware>, + ): DecoratedMiddleware> { return decorateMiddleware(middleware) } - errors(errors: U): BuilderWithErrors { + errors(errors: U): BuilderWithErrors { return new BuilderWithErrors({ ...this['~orpc'], errorMap: errors, }) } - use>( - middleware: Middleware>, - ): BuilderWithMiddlewares { - return new BuilderWithMiddlewares({ + use( + middleware: Middleware>, + ): ConflictContextGuard + & BuilderWithMiddlewares { + const builder = new BuilderWithMiddlewares({ ...this['~orpc'], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) + 1, outputValidationIndex: fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) + 1, middlewares: [middleware as any], // FIXME: I believe we can remove `as any` here }) + + return builder as typeof builder & ConflictContextGuard } - route(route: RouteOptions): ProcedureBuilder> { + route(route: RouteOptions): ProcedureBuilder> { return new ProcedureBuilder({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -92,7 +94,10 @@ export class Builder { }) } - input(schema: USchema, example?: SchemaInput): ProcedureBuilderWithInput> { + input( + schema: USchema, + example?: SchemaInput, + ): ProcedureBuilderWithInput> { return new ProcedureBuilderWithInput({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -107,7 +112,10 @@ export class Builder { }) } - output(schema: USchema, example?: SchemaOutput): ProcedureBuilderWithOutput> { + output( + schema: USchema, + example?: SchemaOutput, + ): ProcedureBuilderWithOutput> { return new ProcedureBuilderWithOutput({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -123,8 +131,8 @@ export class Builder { } handler( - handler: ProcedureHandler>, - ): DecoratedProcedure> { + handler: ProcedureHandler>, + ): DecoratedProcedure> { return new DecoratedProcedure({ middlewares: [], inputValidationIndex: fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex), @@ -139,7 +147,7 @@ export class Builder { }) } - prefix(prefix: HTTPPath): RouterBuilder> { + prefix(prefix: HTTPPath): RouterBuilder> { return new RouterBuilder({ middlewares: [], errorMap: {}, @@ -147,7 +155,7 @@ export class Builder { }) } - tag(...tags: string[]): RouterBuilder> { + tag(...tags: string[]): RouterBuilder> { return new RouterBuilder({ middlewares: [], errorMap: {}, @@ -155,19 +163,19 @@ export class Builder { }) } - router, any>>( + router>( router: U, - ): AdaptedRouter> { - return new RouterBuilder>({ + ): AdaptedRouter> { + return new RouterBuilder>({ middlewares: [], errorMap: [], }).router(router) } - lazy, any>>( + lazy>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, Record> { - return new RouterBuilder>({ + ): AdaptedRouter, Record> { + return new RouterBuilder>({ middlewares: [], errorMap: {}, }).lazy(loader) @@ -175,7 +183,7 @@ export class Builder { contract>( contract: U, - ): ChainableImplementer { + ): ChainableImplementer { return createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, diff --git a/packages/server/src/context.ts b/packages/server/src/context.ts index 9ccb87a5..1577b7a6 100644 --- a/packages/server/src/context.ts +++ b/packages/server/src/context.ts @@ -1,11 +1,12 @@ -import type { Context } from './types' - -/** - * U extends Context & ContextGuard - * - * Purpose: - * - Ensures that any extension `U` of `Context` must conform to the current `TContext`. - * - This is useful when redefining `TContext` to maintain type compatibility with the existing context. - * - */ -export type ContextGuard = Partial | undefined +import type { IsNever } from '@orpc/shared' + +export type Context = Record + +export type TypeInitialContext = (type: T) => any + +export type TypeCurrentContext = { type: T } + +export type ConflictContextGuard = + true extends IsNever | { [K in keyof T]: IsNever }[keyof T] + ? never // 'Conflict context detected: Please ensure your middlewares do not return conflicting context' + : unknown diff --git a/packages/server/src/implementer-chainable.test-d.ts b/packages/server/src/implementer-chainable.test-d.ts index 40ef1b36..c4b41cdf 100644 --- a/packages/server/src/implementer-chainable.test-d.ts +++ b/packages/server/src/implementer-chainable.test-d.ts @@ -1,8 +1,8 @@ +import type { Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ChainableImplementer } from './implementer-chainable' import type { Middleware } from './middleware' import type { ProcedureImplementer } from './procedure-implementer' import type { RouterImplementer } from './router-implementer' -import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' import { createChainableImplementer } from './implementer-chainable' @@ -24,11 +24,11 @@ const contract = oc.router({ describe('ChainableImplementer', () => { it('with procedure', () => { expectTypeOf(createChainableImplementer(ping, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 })).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(createChainableImplementer(pong, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 })).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() }) @@ -36,27 +36,27 @@ describe('ChainableImplementer', () => { const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) expectTypeOf(implementer).toMatchTypeOf< - Omit, '~type' | '~orpc'> + Omit, '~type' | '~orpc'> >() expectTypeOf(implementer.ping).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(implementer.pong).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(implementer.nested).toMatchTypeOf< - Omit, '~type' | '~orpc'> + Omit, '~type' | '~orpc'> >() expectTypeOf(implementer.nested.ping).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(implementer.nested.pong).toEqualTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() }) @@ -81,23 +81,23 @@ describe('ChainableImplementer', () => { const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) expectTypeOf(implementer).toMatchTypeOf< - Omit, '~type' | '~orpc'> + Omit, '~type' | '~orpc'> >() expectTypeOf(implementer.use).toMatchTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(implementer.router).toMatchTypeOf< - Omit, '~type' | '~orpc'> + Omit, '~type' | '~orpc'> >() expectTypeOf(implementer.router.use).toMatchTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() expectTypeOf(implementer.router.router).toMatchTypeOf< - ProcedureImplementer> + ProcedureImplementer> >() }) }) @@ -105,17 +105,25 @@ describe('ChainableImplementer', () => { describe('createChainableImplementer', () => { it('with procedure', () => { const implementer = createChainableImplementer(ping, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - expectTypeOf(implementer).toEqualTypeOf>() + expectTypeOf(implementer).toEqualTypeOf>() }) it('with router', () => { const implementer = createChainableImplementer(contract, { middlewares: [], inputValidationIndex: 0, outputValidationIndex: 0 }) - expectTypeOf(implementer).toEqualTypeOf>() + expectTypeOf(implementer).toEqualTypeOf>() }) it('with middlewares', () => { const mid = {} as Middleware<{ auth: boolean }, { db: string }, unknown, unknown, Record> - const implementer = createChainableImplementer(contract, { middlewares: [mid], inputValidationIndex: 1, outputValidationIndex: 1 }) - expectTypeOf(implementer).toEqualTypeOf>() + const implementer = createChainableImplementer(contract, { + __initialContext: {} as TypeInitialContext<{ auth: boolean }>, + __currentContext: {} as TypeCurrentContext<{ auth: boolean } & { db: string }>, + middlewares: [mid], + inputValidationIndex: 1, + outputValidationIndex: 1, + }) + expectTypeOf(implementer).toEqualTypeOf< + ChainableImplementer<{ auth: boolean }, { auth: boolean } & { db: string }, typeof contract> + >() }) }) diff --git a/packages/server/src/implementer-chainable.ts b/packages/server/src/implementer-chainable.ts index 1b98f72e..f88a214b 100644 --- a/packages/server/src/implementer-chainable.ts +++ b/packages/server/src/implementer-chainable.ts @@ -1,32 +1,34 @@ +import type { Context, TypeCurrentContext, TypeInitialContext } from './context' import type { Middleware } from './middleware' -import type { Context, MergeContext, WELL_CONTEXT } from './types' import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' import { createCallableObject } from '@orpc/shared' import { ProcedureImplementer } from './procedure-implementer' import { RouterImplementer } from './router-implementer' export type ChainableImplementer< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TContract extends ContractRouter, > = TContract extends ContractProcedure - ? ProcedureImplementer + ? ProcedureImplementer : { - [K in keyof TContract]: TContract[K] extends ContractRouter ? ChainableImplementer : never - } & Omit, '~type' | '~orpc'> + [K in keyof TContract]: TContract[K] extends ContractRouter ? ChainableImplementer : never + } & Omit, '~type' | '~orpc'> export function createChainableImplementer< - TContext extends Context = WELL_CONTEXT, - TExtraContext extends Context = undefined, - TContract extends ContractRouter = any, + TInitialContext extends Context, + TCurrentContext extends Context, + TContract extends ContractRouter, >( contract: TContract, options: { - middlewares: Middleware, Partial | undefined, unknown, any, any>[] + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number }, -): ChainableImplementer { +): ChainableImplementer { if (isContractProcedure(contract)) { const implementer = new ProcedureImplementer({ contract, @@ -38,7 +40,7 @@ export function createChainableImplementer< return implementer as any } - const chainable = {} as ChainableImplementer + const chainable = {} as ChainableImplementer for (const key in contract) { (chainable as any)[key] = createChainableImplementer(contract[key]!, options) @@ -51,7 +53,7 @@ export function createChainableImplementer< const merged = new Proxy(chainable, { get(target, key) { - const next = Reflect.get(target, key) as ChainableImplementer | undefined + const next = Reflect.get(target, key) as ChainableImplementer | undefined const method = Reflect.get(routerImplementer, key) if (typeof key !== 'string' || typeof method !== 'function') { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 68593d0c..73f6496a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,8 +1,8 @@ -import type { WELL_CONTEXT } from './types' import { Builder } from './builder' export * from './builder' export * from './config' +export * from './context' export * from './error' export * from './hidden' export * from './implementer-chainable' @@ -21,10 +21,9 @@ export * from './router-builder' export * from './router-client' export * from './router-implementer' export * from './types' -export * from './utils' export { isDefinedError, ORPCError, safe, type } from '@orpc/contract' -export const os = new Builder({ +export const os = new Builder({ config: {}, }) diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts index 4081b49e..f72ed08c 100644 --- a/packages/server/src/lazy-decorated.test-d.ts +++ b/packages/server/src/lazy-decorated.test-d.ts @@ -1,14 +1,17 @@ -import type { ANY_PROCEDURE, ANY_ROUTER, DecoratedProcedure, Procedure, WELL_CONTEXT } from '.' +import type { Context } from './context' import type { Lazy } from './lazy' import type { DecoratedLazy } from './lazy-decorated' +import type { ANY_PROCEDURE, Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { ANY_ROUTER } from './router' import { z } from 'zod' import { lazy } from './lazy' import { decorateLazy } from './lazy-decorated' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) -const ping = {} as Procedure> -const pong = {} as DecoratedProcedure> +const ping = {} as Procedure> +const pong = {} as DecoratedProcedure> const lazyPing = lazy(() => Promise.resolve({ default: ping })) const lazyPong = lazy(() => Promise.resolve({ default: pong })) diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts index 80e6787e..2f4f7018 100644 --- a/packages/server/src/lazy-decorated.test.ts +++ b/packages/server/src/lazy-decorated.test.ts @@ -11,7 +11,7 @@ beforeEach(() => { describe('decorated lazy', () => { const schema = z.object({ val: z.string().transform(val => Number(val)) }) - const ping = new Procedure>({ + const ping = new Procedure({ contract: new ContractProcedure({ InputSchema: schema, OutputSchema: undefined, diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index 412b9e4e..0ec1f983 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -1,4 +1,5 @@ import type { Lazy } from './lazy' +import type { ANY_PROCEDURE } from './procedure' import { flatLazy } from './lazy' import { type ANY_ROUTER, getRouterChild } from './router' @@ -6,9 +7,11 @@ export type DecoratedLazy = T extends Lazy ? DecoratedLazy : Lazy & ( - T extends ANY_ROUTER ? { - [K in keyof T]: DecoratedLazy - } : unknown + T extends ANY_PROCEDURE + ? unknown + : T extends ANY_ROUTER ? { + [K in keyof T]: DecoratedLazy + } : unknown ) export function decorateLazy>(lazied: T): DecoratedLazy { diff --git a/packages/server/src/lazy.test-d.ts b/packages/server/src/lazy.test-d.ts index d7b9f258..d59146b8 100644 --- a/packages/server/src/lazy.test-d.ts +++ b/packages/server/src/lazy.test-d.ts @@ -1,9 +1,9 @@ +import type { Context } from './context' import type { ANY_LAZY, FlattenLazy, Lazy } from './lazy' import type { Procedure } from './procedure' -import type { WELL_CONTEXT } from './types' import { flatLazy, isLazy, lazy, unlazy } from './lazy' -const procedure = {} as Procedure> +const procedure = {} as Procedure> const router = { procedure } diff --git a/packages/server/src/lazy.test.ts b/packages/server/src/lazy.test.ts index e7aaa1b4..eae32c64 100644 --- a/packages/server/src/lazy.test.ts +++ b/packages/server/src/lazy.test.ts @@ -1,9 +1,8 @@ -import type { WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' import { flatLazy, isLazy, lazy, LAZY_LOADER_SYMBOL, unlazy } from './lazy' import { Procedure } from './procedure' -const procedure = new Procedure>({ +const procedure = new Procedure({ contract: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, diff --git a/packages/server/src/middleware-decorated.test-d.ts b/packages/server/src/middleware-decorated.test-d.ts index 10c875f7..fd9869e8 100644 --- a/packages/server/src/middleware-decorated.test-d.ts +++ b/packages/server/src/middleware-decorated.test-d.ts @@ -1,16 +1,16 @@ +import type { Context } from './context' import type { Middleware } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' -import type { WELL_CONTEXT } from './types' describe('decorateMiddleware', () => { const decorated = {} as DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, { name: string }, unknown, Record> it('assignable to middleware', () => { - const decorated = {} as DecoratedMiddleware> - const mid: Middleware> = decorated + const decorated = {} as DecoratedMiddleware, { input: 'input' }, unknown, Record> + const mid: Middleware, { input: 'input' }, unknown, Record> = decorated - const decorated2 = {} as DecoratedMiddleware> - const mid2: Middleware> = decorated2 + const decorated2 = {} as DecoratedMiddleware> + const mid2: Middleware> = decorated2 }) it('can map input', () => { diff --git a/packages/server/src/middleware-decorated.ts b/packages/server/src/middleware-decorated.ts index 09acd3c3..aaf68121 100644 --- a/packages/server/src/middleware-decorated.ts +++ b/packages/server/src/middleware-decorated.ts @@ -1,49 +1,44 @@ -import type { ContextGuard } from './context' +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware, MiddlewareNextFn } from './middleware' -import type { Context, MergeContext } from './types' -import { mergeContext } from './utils' export interface DecoratedMiddleware< - TContext extends Context, - TExtraContext extends Context, + TInContext extends Context, + TOutContext extends Context, TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, -> extends Middleware { - concat: (< - UExtraContext extends Context & ContextGuard>, - UInput, - >( +> extends Middleware { + concat: (( middleware: Middleware< - MergeContext, - UExtraContext, + TInContext & TOutContext, + UOutContext, UInput & TInput, TOutput, TErrorConstructorMap >, ) => DecoratedMiddleware< - TContext, - MergeContext, + TInContext, + TOutContext & UOutContext, UInput & TInput, TOutput, TErrorConstructorMap >) & (< - UExtraContext extends Context & ContextGuard>, + UOutContext extends Context, UInput = TInput, UMappedInput = unknown, >( middleware: Middleware< - MergeContext, - UExtraContext, + TInContext & TOutContext, + UOutContext, UMappedInput, TOutput, TErrorConstructorMap >, mapInput: MapInputMiddleware, ) => DecoratedMiddleware< - TContext, - MergeContext, + TInContext, + TOutContext & UOutContext, UInput & TInput, TOutput, TErrorConstructorMap @@ -51,19 +46,19 @@ export interface DecoratedMiddleware< mapInput: ( map: MapInputMiddleware, - ) => DecoratedMiddleware + ) => DecoratedMiddleware } export function decorateMiddleware< - TContext extends Context, - TExtraContext extends Context, + TInContext extends Context, + TOutContext extends Context, TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, >( - middleware: Middleware, -): DecoratedMiddleware { - const decorated = middleware as DecoratedMiddleware + middleware: Middleware, +): DecoratedMiddleware { + const decorated = middleware as DecoratedMiddleware decorated.mapInput = (mapInput) => { const mapped = decorateMiddleware( @@ -79,8 +74,8 @@ export function decorateMiddleware< : concatMiddleware const concatted = decorateMiddleware((options, input, output, ...rest) => { - const next: MiddlewareNextFn = async (nextOptions) => { - return mapped({ ...options, context: mergeContext(nextOptions.context, options.context) }, input, output, ...rest) + const next: MiddlewareNextFn = async (...[nextOptions]) => { + return mapped({ ...options, context: { ...nextOptions?.context, ...options.context } }, input, output, ...rest) } const merged = middleware({ ...options, next } as any, input as any, output as any, ...rest) diff --git a/packages/server/src/middleware.test-d.ts b/packages/server/src/middleware.test-d.ts index 7d97c209..48141ed2 100644 --- a/packages/server/src/middleware.test-d.ts +++ b/packages/server/src/middleware.test-d.ts @@ -1,3 +1,4 @@ +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Middleware, MiddlewareNextFn, MiddlewareOptions, MiddlewareOutputFn } from './middleware' import type { ANY_PROCEDURE } from './procedure' @@ -15,29 +16,29 @@ const baseErrors = { describe('middleware', () => { it('just a function', () => { - const mid: Middleware<{ auth: boolean }, undefined, unknown, unknown, ORPCErrorConstructorMap> = ({ context, path, procedure, signal, next, errors }, input, output) => { + const mid: Middleware<{ auth: boolean }, Record, unknown, unknown, ORPCErrorConstructorMap> = ({ context, path, procedure, signal, next, errors }, input, output) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(path).toEqualTypeOf() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(signal).toEqualTypeOf>() expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(next).toEqualTypeOf>() + expectTypeOf(next).toEqualTypeOf>() expectTypeOf(errors).toEqualTypeOf>() return next({}) } - const mid2: Middleware<{ auth: boolean }, undefined, unknown, unknown, Record> = async ({ context, path, procedure, signal, next }, input, output) => { + const mid2: Middleware<{ auth: boolean }, Record, unknown, unknown, Record> = async ({ context, path, procedure, signal, next }, input, output) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(path).toEqualTypeOf() expectTypeOf(procedure).toEqualTypeOf() expectTypeOf(signal).toEqualTypeOf>() expectTypeOf(output).toEqualTypeOf>() - expectTypeOf(next).toEqualTypeOf>() + expectTypeOf(next).toEqualTypeOf>() - return await next({}) + return await next() } // @ts-expect-error - missing return type @@ -50,27 +51,30 @@ describe('middleware', () => { }) it('require return valid extra context', () => { - const mid0: Middleware> = ({ next }) => { + const mid0: Middleware, unknown, unknown, Record> = ({ next }) => { return next({ }) } - const mid: Middleware> = ({ next }) => { + const mid: Middleware> = ({ next }) => { return next({ context: { userId: '1' } }) } // @ts-expect-error invalid extra context - const mid2: Middleware = ({ next }) => { + const mid2: Middleware = ({ next }) => { return next({ context: { userId: 1 } }) } - const mid3: Middleware> = ({ next }) => { - // @ts-expect-error missing extra context - return next({}) + const mid3: Middleware> = ({ next }) => { + return next({ + context: { + userId: '1', + }, + }) } }) it('can type input', () => { - const mid: Middleware> = ({ next }, input) => { + const mid: Middleware, { id: string }, unknown, Record> = ({ next }, input) => { expectTypeOf(input).toEqualTypeOf<{ id: string }>() return next({}) @@ -78,7 +82,7 @@ describe('middleware', () => { }) it('can type output', () => { - const mid: Middleware> = async ({ next }, input, output) => { + const mid: Middleware, unknown, { id: string }, Record> = async ({ next }, input, output) => { const result = await next({}) expectTypeOf(result.output).toEqualTypeOf<{ id: string }>() @@ -86,7 +90,7 @@ describe('middleware', () => { return output({ id: '1' }) } - const mid2: Middleware> = async (_, __, output) => { + const mid2: Middleware, unknown, { id: string }, Record> = async (_, __, output) => { // @ts-expect-error invalid output return output({ id: 123 }) } @@ -97,8 +101,8 @@ describe('middleware', () => { return next({ context: { extra: 'extra' as const } }) } - type Inferred = typeof handler extends Middleware - ? [TContext, TExtraContext, TInput, TOutput, TErrorConstructorMap] + type Inferred = typeof handler extends Middleware + ? [TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap] : never expectTypeOf().toEqualTypeOf< diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index d3da51b7..6d1fdc9c 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,49 +1,56 @@ import type { Promisable } from '@orpc/shared' +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { ANY_PROCEDURE } from './procedure' -import type { AbortSignal, Context } from './types' +import type { AbortSignal } from './types' -export type MiddlewareResult = Promisable<{ +export type MiddlewareResult = Promisable<{ output: TOutput - context: TExtraContext + context: TOutContext }> -export interface MiddlewareNextFn { - ( - options: UExtraContext extends undefined ? { context?: UExtraContext } : { context: UExtraContext } - ): MiddlewareResult +export type MiddlewareNextFnOptions = Record extends TOutContext + ? { context?: TOutContext } + : { context: TOutContext } + +export type MiddlewareNextFnRest = + | [options: MiddlewareNextFnOptions] + | (Record extends TOutContext ? [] : never) + +export interface MiddlewareNextFn { + = Record>(...rest: MiddlewareNextFnRest): MiddlewareResult } export interface MiddlewareOutputFn { - (output: TOutput): MiddlewareResult + (output: TOutput): MiddlewareResult, TOutput> } export interface MiddlewareOptions< - TContext extends Context, + TInContext extends Context, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, > { - context: TContext + context: TInContext path: string[] procedure: ANY_PROCEDURE signal?: AbortSignal - next: MiddlewareNextFn + next: MiddlewareNextFn errors: TErrorConstructorMap } export interface Middleware< - TContext extends Context, - TExtraContext extends Context, + TInContext extends Context, + TOutContext extends Context, TInput, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap, > { ( - options: MiddlewareOptions, + options: MiddlewareOptions, input: TInput, output: MiddlewareOutputFn, ): Promisable< - MiddlewareResult + MiddlewareResult > } @@ -55,6 +62,6 @@ export interface MapInputMiddleware { export type ANY_MAP_INPUT_MIDDLEWARE = MapInputMiddleware -export function middlewareOutputFn(output: TOutput): MiddlewareResult { - return { output, context: undefined } +export function middlewareOutputFn(output: TOutput): MiddlewareResult, TOutput> { + return { output, context: {} } } diff --git a/packages/server/src/procedure-builder-with-input.test-d.ts b/packages/server/src/procedure-builder-with-input.test-d.ts index aa7e7144..f3bf1bde 100644 --- a/packages/server/src/procedure-builder-with-input.test-d.ts +++ b/packages/server/src/procedure-builder-with-input.test-d.ts @@ -18,7 +18,7 @@ const baseErrors = { const inputSchema = z.object({ input: z.string().transform(v => Number.parseInt(v)) }) -const builder = {} as ProcedureBuilderWithInput<{ db: string }, { auth?: boolean }, typeof inputSchema, typeof baseErrors> +const builder = {} as ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof baseErrors> const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) @@ -27,7 +27,7 @@ describe('ProcedureBuilderWithInput', () => { const errors = { CODE: { message: 'MESSAGE' } } expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ProcedureBuilderWithInput<{ db: string }, { auth?: boolean }, typeof inputSchema, typeof baseErrors & typeof errors> + ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof baseErrors & typeof errors> >() // @ts-expect-error - not allow redefine error map @@ -55,14 +55,18 @@ describe('ProcedureBuilderWithInput', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof inputSchema, typeof baseErrors> + >() // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) + builder.use(({ next }) => next({ context: { db: 123 } })) // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({})) // @ts-expect-error --- output is not match builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() }) it('with map input', () => { @@ -84,20 +88,24 @@ describe('ProcedureBuilderWithInput', () => { return { mapped: input.input } }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithInput<{ db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof inputSchema, typeof baseErrors> + >() // @ts-expect-error --- conflict context - builder.use(({ next }) => ({ db: 123 }), () => {}) + builder.use(({ next }) => ({ context: { db: 123 } }), () => {}) // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({}), () => {}) // @ts-expect-error --- output is not match builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), () => {}) + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), () => {})).toEqualTypeOf() }) }) it('.output', () => { expectTypeOf(builder.output(schema)).toEqualTypeOf< - ProcedureImplementer<{ db: string }, { auth?: boolean }, typeof inputSchema, typeof schema, typeof baseErrors> + ProcedureImplementer<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, typeof schema, typeof baseErrors> >() }) @@ -114,7 +122,7 @@ describe('ProcedureBuilderWithInput', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { auth?: boolean }, typeof inputSchema, undefined, number, typeof baseErrors> + DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, typeof inputSchema, undefined, number, typeof baseErrors> >() }) }) diff --git a/packages/server/src/procedure-builder-with-input.ts b/packages/server/src/procedure-builder-with-input.ts index d09a92da..d6c7c4b0 100644 --- a/packages/server/src/procedure-builder-with-input.ts +++ b/packages/server/src/procedure-builder-with-input.ts @@ -1,22 +1,23 @@ import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, RouteOptions, Schema, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { MapInputMiddleware, Middleware } from './middleware' import type { ProcedureHandler } from './procedure' -import type { Context, MergeContext } from './types' import { ContractProcedureBuilderWithInput, DecoratedContractProcedure } from '@orpc/contract' import { decorateMiddleware } from './middleware-decorated' import { DecoratedProcedure } from './procedure-decorated' import { ProcedureImplementer } from './procedure-implementer' export interface ProcedureBuilderWithInputDef< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TErrorMap extends ErrorMap, > { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext contract: ContractProcedure - middlewares: Middleware, Partial | undefined, unknown, unknown, ORPCErrorConstructorMap>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number } @@ -30,21 +31,21 @@ export interface ProcedureBuilderWithInputDef< * */ export class ProcedureBuilderWithInput< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TErrorMap extends ErrorMap, > { '~type' = 'ProcedureBuilderWithInput' as const - '~orpc': ProcedureBuilderWithInputDef + '~orpc': ProcedureBuilderWithInputDef - constructor(def: ProcedureBuilderWithInputDef) { + constructor(def: ProcedureBuilderWithInputDef) { this['~orpc'] = def } errors & ErrorMapSuggestions>( errors: U, - ): ProcedureBuilderWithInput { + ): ProcedureBuilderWithInput { return new ProcedureBuilderWithInput({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -53,7 +54,7 @@ export class ProcedureBuilderWithInput< }) } - route(route: RouteOptions): ProcedureBuilderWithInput { + route(route: RouteOptions): ProcedureBuilderWithInput { return new ProcedureBuilderWithInput({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -62,17 +63,16 @@ export class ProcedureBuilderWithInput< }) } - use>>( - middleware: Middleware, U, SchemaOutput, unknown, ORPCErrorConstructorMap>, - ): ProcedureBuilderWithInput, TInputSchema, TErrorMap> + use( + middleware: Middleware, unknown, ORPCErrorConstructorMap>, + ): ConflictContextGuard + & ProcedureBuilderWithInput - use< - UExtra extends Context & ContextGuard>, - UInput, - >( - middleware: Middleware, UExtra, UInput, unknown, ORPCErrorConstructorMap>, + use( + middleware: Middleware>, mapInput: MapInputMiddleware, UInput>, - ): ProcedureBuilderWithInput, TInputSchema, TErrorMap> + ): ConflictContextGuard & + ProcedureBuilderWithInput use( middleware: Middleware, @@ -91,7 +91,7 @@ export class ProcedureBuilderWithInput< }) } - output(schema: U, example?: SchemaOutput): ProcedureImplementer { + output(schema: U, example?: SchemaOutput): ProcedureImplementer { return new ProcedureImplementer({ ...this['~orpc'], contract: new ContractProcedureBuilderWithInput(this['~orpc'].contract['~orpc']).output(schema, example), @@ -99,8 +99,8 @@ export class ProcedureBuilderWithInput< } handler( - handler: ProcedureHandler, - ): DecoratedProcedure { + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], handler, diff --git a/packages/server/src/procedure-builder-with-output.test-d.ts b/packages/server/src/procedure-builder-with-output.test-d.ts index 405fe241..1159077f 100644 --- a/packages/server/src/procedure-builder-with-output.test-d.ts +++ b/packages/server/src/procedure-builder-with-output.test-d.ts @@ -18,7 +18,7 @@ const baseErrors = { const outputSchema = z.object({ output: z.string().transform(v => Number.parseInt(v)) }) -const builder = {} as ProcedureBuilderWithOutput<{ db: string }, { auth?: boolean }, typeof outputSchema, typeof baseErrors> +const builder = {} as ProcedureBuilderWithOutput<{ db: string }, { db: string } & { auth?: boolean }, typeof outputSchema, typeof baseErrors> const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) @@ -27,7 +27,7 @@ describe('ProcedureBuilderWithOutput', () => { const errors = { CODE: { message: 'MESSAGE' } } expectTypeOf(builder.errors(errors)).toEqualTypeOf< - ProcedureBuilderWithOutput<{ db: string }, { auth?: boolean }, typeof outputSchema, typeof baseErrors & typeof errors> + ProcedureBuilderWithOutput < { db: string }, { db: string } & { auth?: boolean }, typeof outputSchema, typeof baseErrors & typeof errors> >() // @ts-expect-error - not allow redefine error map @@ -54,19 +54,23 @@ describe('ProcedureBuilderWithOutput', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilderWithOutput < { db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof outputSchema, typeof baseErrors> + >() // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) + builder.use(({ next }) => next({ context: { db: 123 } })) // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({})) // @ts-expect-error --- output is not match builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() }) it('.input', () => { expectTypeOf(builder.input(schema)).toEqualTypeOf< - ProcedureImplementer<{ db: string }, { auth?: boolean }, typeof schema, typeof outputSchema, typeof baseErrors> + ProcedureImplementer<{ db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof outputSchema, typeof baseErrors> >() }) @@ -83,7 +87,7 @@ describe('ProcedureBuilderWithOutput', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure < { db: string }, { auth?: boolean }, undefined, typeof outputSchema, { output: string }, typeof baseErrors> + DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, undefined, typeof outputSchema, { output: string }, typeof baseErrors> >() // @ts-expect-error --- invalid output diff --git a/packages/server/src/procedure-builder-with-output.ts b/packages/server/src/procedure-builder-with-output.ts index c166bb20..a2c8c54c 100644 --- a/packages/server/src/procedure-builder-with-output.ts +++ b/packages/server/src/procedure-builder-with-output.ts @@ -1,21 +1,22 @@ import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, RouteOptions, Schema, SchemaInput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Middleware } from './middleware' import type { ProcedureHandler } from './procedure' -import type { Context, MergeContext } from './types' import { ContractProcedureBuilderWithOutput, DecoratedContractProcedure } from '@orpc/contract' import { DecoratedProcedure } from './procedure-decorated' import { ProcedureImplementer } from './procedure-implementer' export interface ProcedureBuilderWithOutputDef< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TOutputSchema extends Schema, TErrorMap extends ErrorMap, > { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext contract: ContractProcedure - middlewares: Middleware, Partial | undefined, unknown, any, ORPCErrorConstructorMap>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number } @@ -29,21 +30,21 @@ export interface ProcedureBuilderWithOutputDef< * */ export class ProcedureBuilderWithOutput< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TOutputSchema extends Schema, TErrorMap extends ErrorMap, > { '~type' = 'ProcedureBuilderWithOutput' as const - '~orpc': ProcedureBuilderWithOutputDef + '~orpc': ProcedureBuilderWithOutputDef - constructor(def: ProcedureBuilderWithOutputDef) { + constructor(def: ProcedureBuilderWithOutputDef) { this['~orpc'] = def } errors & ErrorMapSuggestions>( errors: U, - ): ProcedureBuilderWithOutput { + ): ProcedureBuilderWithOutput { return new ProcedureBuilderWithOutput({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -52,7 +53,7 @@ export class ProcedureBuilderWithOutput< }) } - route(route: RouteOptions): ProcedureBuilderWithOutput { + route(route: RouteOptions): ProcedureBuilderWithOutput { return new ProcedureBuilderWithOutput({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -61,20 +62,24 @@ export class ProcedureBuilderWithOutput< }) } - use>>( - middleware: Middleware, U, unknown, SchemaInput, ORPCErrorConstructorMap>, - ): ProcedureBuilderWithOutput, TOutputSchema, TErrorMap> { - return new ProcedureBuilderWithOutput({ - ...this['~orpc'], + use( + middleware: Middleware, ORPCErrorConstructorMap>, + ): ConflictContextGuard + & ProcedureBuilderWithOutput { + const builder = new ProcedureBuilderWithOutput({ + contract: this['~orpc'].contract, + outputValidationIndex: this['~orpc'].outputValidationIndex, inputValidationIndex: this['~orpc'].inputValidationIndex + 1, - middlewares: [...this['~orpc'].middlewares, middleware as any], + middlewares: [...this['~orpc'].middlewares, middleware], }) + + return builder as typeof builder & ConflictContextGuard } input( schema: U, example?: SchemaInput, - ): ProcedureImplementer { + ): ProcedureImplementer { return new ProcedureImplementer({ ...this['~orpc'], contract: new ContractProcedureBuilderWithOutput(this['~orpc'].contract['~orpc']).input(schema, example), @@ -82,8 +87,8 @@ export class ProcedureBuilderWithOutput< } handler>( - handler: ProcedureHandler, - ): DecoratedProcedure { + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], handler, diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts index 13965e55..b8719b38 100644 --- a/packages/server/src/procedure-builder.test-d.ts +++ b/packages/server/src/procedure-builder.test-d.ts @@ -17,7 +17,7 @@ const baseErrors = { }, } -const builder = {} as ProcedureBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors> +const builder = {} as ProcedureBuilder<{ db: string }, { db: string } & { auth?: boolean }, typeof baseErrors> const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) @@ -25,7 +25,9 @@ describe('ProcedureBuilder', () => { it('.errors', () => { const errors = { CODE: { message: 'MESSAGE' } } - expectTypeOf(builder.errors(errors)).toEqualTypeOf>() + expectTypeOf(builder.errors(errors)).toEqualTypeOf< + ProcedureBuilder < { db: string }, { db: string } & { auth?: boolean }, typeof baseErrors & typeof errors> + >() // @ts-expect-error - not allow redefine error map builder.errors({ BASE: baseErrors.BASE }) @@ -51,22 +53,31 @@ describe('ProcedureBuilder', () => { }) }) - expectTypeOf(applied).toEqualTypeOf>() + expectTypeOf(applied).toEqualTypeOf< + ProcedureBuilder < { db: string }, { db: string } & { auth?: boolean } & { extra: boolean }, typeof baseErrors> + >() // @ts-expect-error --- conflict context - builder.use(({ next }) => next({ db: 123 })) + builder.use(({ next }) => next({ context: { db: 123 } })) // @ts-expect-error --- input is not match builder.use(({ next }, input: 'invalid') => next({})) // @ts-expect-error --- output is not match builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({})) + + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toEqualTypeOf() }) it('.input', () => { - expectTypeOf(builder.input(schema)).toEqualTypeOf>() + expectTypeOf(builder.input(schema)).toEqualTypeOf< + ProcedureBuilderWithInput < { db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof baseErrors> + >() }) it('.output', () => { - expectTypeOf(builder.output(schema)).toEqualTypeOf>() + expectTypeOf(builder.output(schema)).toEqualTypeOf< + ProcedureBuilderWithOutput<{ db: string }, { db: string } & { auth?: boolean }, typeof schema, typeof baseErrors> + >() }) it('.handler', () => { @@ -82,7 +93,7 @@ describe('ProcedureBuilder', () => { }) expectTypeOf(procedure).toMatchTypeOf< - DecoratedProcedure<{ db: string }, { auth?: boolean }, undefined, undefined, number, typeof baseErrors> + DecoratedProcedure<{ db: string }, { db: string } & { auth?: boolean }, undefined, undefined, number, typeof baseErrors> >() }) }) diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 6885acb8..cf639192 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -1,32 +1,33 @@ import type { ContractProcedure, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Middleware } from './middleware' import type { ProcedureHandler } from './procedure' -import type { Context, MergeContext } from './types' import { ContractProcedureBuilder, DecoratedContractProcedure } from '@orpc/contract' import { ProcedureBuilderWithInput } from './procedure-builder-with-input' import { ProcedureBuilderWithOutput } from './procedure-builder-with-output' import { DecoratedProcedure } from './procedure-decorated' -export interface ProcedureBuilderDef { +export interface ProcedureBuilderDef { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext contract: ContractProcedure - middlewares: Middleware, Partial | undefined, unknown, any, ORPCErrorConstructorMap>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number } -export class ProcedureBuilder { +export class ProcedureBuilder { '~type' = 'ProcedureBuilder' as const - '~orpc': ProcedureBuilderDef + '~orpc': ProcedureBuilderDef - constructor(def: ProcedureBuilderDef) { + constructor(def: ProcedureBuilderDef) { this['~orpc'] = def } errors & ErrorMapSuggestions>( errors: U, - ): ProcedureBuilder { + ): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -35,7 +36,7 @@ export class ProcedureBuilder { + route(route: RouteOptions): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -44,31 +45,34 @@ export class ProcedureBuilder>>( + use( middleware: Middleware< - MergeContext, + TCurrentContext, U, unknown, unknown, ORPCErrorConstructorMap >, - ): ProcedureBuilder, TErrorMap> { - return new ProcedureBuilder({ - ...this['~orpc'], + ): ConflictContextGuard + & ProcedureBuilder { + const builder = new ProcedureBuilder({ + contract: this['~orpc'].contract, inputValidationIndex: this['~orpc'].inputValidationIndex + 1, outputValidationIndex: this['~orpc'].outputValidationIndex + 1, middlewares: [...this['~orpc'].middlewares, middleware as any], }) + + return builder as typeof builder & ConflictContextGuard } - input(schema: U, example?: SchemaInput): ProcedureBuilderWithInput { + input(schema: U, example?: SchemaInput): ProcedureBuilderWithInput { return new ProcedureBuilderWithInput({ ...this['~orpc'], contract: new ContractProcedureBuilder(this['~orpc'].contract['~orpc']).input(schema, example), }) } - output(schema: U, example?: SchemaOutput): ProcedureBuilderWithOutput { + output(schema: U, example?: SchemaOutput): ProcedureBuilderWithOutput { return new ProcedureBuilderWithOutput({ ...this['~orpc'], contract: new ContractProcedureBuilder(this['~orpc'].contract['~orpc']).output(schema, example), @@ -76,8 +80,8 @@ export class ProcedureBuilder( - handler: ProcedureHandler, - ): DecoratedProcedure { + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], handler, diff --git a/packages/server/src/procedure-client.test-d.ts b/packages/server/src/procedure-client.test-d.ts index 616d41da..9631b854 100644 --- a/packages/server/src/procedure-client.test-d.ts +++ b/packages/server/src/procedure-client.test-d.ts @@ -1,7 +1,8 @@ import type { Client, ORPCError } from '@orpc/contract' +import type { Context } from './context' import type { Procedure } from './procedure' import type { ProcedureClient } from './procedure-client' -import type { Meta, WELL_CONTEXT, WithSignal } from './types' +import type { Meta, WithSignal } from './types' import { z } from 'zod' import { lazy } from './lazy' import { createProcedureClient } from './procedure-client' @@ -112,8 +113,8 @@ describe('createProcedureClient', () => { data: z.object({ why: z.string().transform(v => Number(v)) }), }, } - const procedure = {} as Procedure - const procedureWithContext = {} as Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }, Record> + const procedure = {} as Procedure + const procedureWithContext = {} as Procedure<{ userId: string }, { db: string }, typeof schema, typeof schema, { val: string }, Record> it('just a client', () => { const client = createProcedureClient(procedure) @@ -125,7 +126,7 @@ describe('createProcedureClient', () => { createProcedureClient(procedure) createProcedureClient(procedure, { - context: undefined, + context: {}, }) // @ts-expect-error - missing context @@ -166,7 +167,7 @@ describe('createProcedureClient', () => { createProcedureClient(procedure, { async interceptor(input, context, meta) { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf Promise<{ val: number }> }>() return { val: 123 } @@ -174,25 +175,25 @@ describe('createProcedureClient', () => { onStart(state, context, meta) { expectTypeOf(state).toEqualTypeOf<{ status: 'pending', input: unknown, output: undefined, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf() }, onSuccess(state, context, meta) { expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf() }, onError(state, context, meta) { expectTypeOf(state).toEqualTypeOf<{ status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf() }, onFinish(state, context, meta) { expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined } | { status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf() }, }) @@ -225,7 +226,7 @@ describe('createProcedureClient', () => { it('support lazy procedure', () => { const schema = z.object({ val: z.string().transform(v => Number(v)) }) - const procedure = {} as Procedure<{ userId?: string }, undefined, typeof schema, typeof schema, { val: string }, Record> + const procedure = {} as Procedure<{ userId?: string }, { userId?: string }, typeof schema, typeof schema, { val: string }, Record> const lazied = lazy(() => Promise.resolve({ default: procedure })) const client = createProcedureClient(lazied, { diff --git a/packages/server/src/procedure-client.test.ts b/packages/server/src/procedure-client.test.ts index c02910ec..1388cb5a 100644 --- a/packages/server/src/procedure-client.test.ts +++ b/packages/server/src/procedure-client.test.ts @@ -68,7 +68,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce expect(handler).toBeCalledTimes(1) expect(handler).toBeCalledWith({ input: { val: 123 }, - context: undefined, + context: {}, path: [], procedure: unwrappedProcedure, errors: '__constructors__', @@ -79,7 +79,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce path: [], procedure: unwrappedProcedure, next: expect.any(Function), - context: undefined, + context: {}, errors: '__constructors__', }), { val: '123' }, expect.any(Function)) @@ -88,7 +88,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce path: [], procedure: unwrappedProcedure, next: expect.any(Function), - context: undefined, + context: {}, errors: '__constructors__', }), { val: '123' }, expect.any(Function)) @@ -97,7 +97,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce path: [], procedure: unwrappedProcedure, next: expect.any(Function), - context: undefined, + context: {}, errors: '__constructors__', }), { val: 123 }, expect.any(Function)) @@ -106,7 +106,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce path: [], procedure: unwrappedProcedure, next: expect.any(Function), - context: undefined, + context: {}, errors: '__constructors__', }), { val: 123 }, expect.any(Function)) }) @@ -144,7 +144,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce expect(postMid1).toBeCalledTimes(0) expect(postMid2).toBeCalledTimes(0) expect(handler).toBeCalledTimes(0) - expect(preMid1).toReturnWith(Promise.resolve({ output: { val: '9900' }, context: undefined })) + expect(preMid1).toReturnWith(Promise.resolve({ output: { val: '9900' }, context: {} })) vi.clearAllMocks() postMid1.mockReturnValueOnce({ output: { val: '9900' } }) @@ -154,8 +154,8 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce expect(postMid1).toBeCalledTimes(1) expect(postMid2).toBeCalledTimes(0) expect(handler).toBeCalledTimes(0) - expect(preMid1).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: undefined })) - expect(preMid2).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: undefined })) + expect(preMid1).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: {} })) + expect(preMid2).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: {} })) vi.clearAllMocks() postMid2.mockReturnValueOnce({ output: { val: '9900' } }) @@ -165,9 +165,9 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce expect(postMid1).toBeCalledTimes(1) expect(postMid2).toBeCalledTimes(1) expect(handler).toBeCalledTimes(0) - expect(preMid1).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: undefined })) - expect(preMid2).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: undefined })) - expect(postMid1).toReturnWith(Promise.resolve({ output: { val: '9900' }, context: undefined })) + expect(preMid1).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: {} })) + expect(preMid2).toReturnWith(Promise.resolve({ output: { val: 9900 }, context: {} })) + expect(postMid1).toReturnWith(Promise.resolve({ output: { val: '9900' }, context: {} })) }) it('middleware can add extra context', async () => { @@ -208,7 +208,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce await expect(client({ val: '123' })).resolves.toEqual({ val: 123 }) expect(preMid1).toBeCalledTimes(1) - expect(preMid1).toHaveBeenCalledWith(expect.objectContaining({ context: undefined }), expect.any(Object), expect.any(Function)) + expect(preMid1).toHaveBeenCalledWith(expect.objectContaining({ context: {} }), expect.any(Object), expect.any(Function)) expect(preMid2).toBeCalledTimes(1) expect(preMid2).toHaveBeenCalledWith(expect.objectContaining({ @@ -417,7 +417,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path: ['users'] })) expect(onSuccess).toBeCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) + expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), {}, expect.objectContaining({ path: ['users'] })) }) it('support signal', async () => { @@ -516,7 +516,7 @@ it('still work without InputSchema', async () => { await expect(client('anything')).resolves.toEqual({ val: 123 }) expect(handler).toBeCalledTimes(1) - expect(handler).toHaveBeenCalledWith({ input: 'anything', context: undefined, path: [], procedure }) + expect(handler).toHaveBeenCalledWith({ input: 'anything', context: {}, path: [], procedure }) }) it('still work without OutputSchema', async () => { @@ -540,7 +540,7 @@ it('still work without OutputSchema', async () => { await expect(client({ val: '123' })).resolves.toEqual('anything') expect(handler).toBeCalledTimes(1) - expect(handler).toHaveBeenCalledWith({ input: { val: 123 }, context: undefined, path: [], procedure }) + expect(handler).toHaveBeenCalledWith({ input: { val: 123 }, context: {}, path: [], procedure }) }) it('has helper `output` in meta', async () => { @@ -556,5 +556,5 @@ it('has helper `output` in meta', async () => { expect(preMid2).toBeCalledTimes(1) expect(handler).toBeCalledTimes(0) - expect(preMid1).toReturnWith(Promise.resolve({ output: { val: '99990' }, context: undefined })) + expect(preMid1).toReturnWith(Promise.resolve({ output: { val: '99990' }, context: {} })) }) diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index 81d0b91c..bff4734b 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -1,15 +1,15 @@ import type { Client, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' +import type { Context } from './context' import type { Lazyable } from './lazy' import type { MiddlewareNextFn } from './middleware' import type { ANY_PROCEDURE, Procedure, ProcedureHandlerOptions } from './procedure' -import type { Context, Meta } from './types' +import type { Meta } from './types' import { ORPCError, validateORPCError, ValidationError } from '@orpc/contract' import { executeWithHooks, toError, value } from '@orpc/shared' import { createORPCErrorConstructorMap } from './error' import { unlazy } from './lazy' import { middlewareOutputFn } from './middleware' -import { mergeContext } from './utils' export type ProcedureClient< TClientContext, @@ -23,9 +23,9 @@ export type ProcedureClient< * Options for creating a procedure caller with comprehensive type safety */ export type CreateProcedureClientOptions< - TContext extends Context, - TOutputSchema extends Schema, - THandlerOutput extends SchemaInput, + TInitialContext extends Context, + TCurrentContext extends Schema, + THandlerOutput extends SchemaInput, TClientContext, > = & { @@ -35,36 +35,36 @@ export type CreateProcedureClientOptions< path?: string[] } & ( - | { context: Value } - | (undefined extends TContext ? { context?: Value } : never) + | { context: Value } + | (Record extends TInitialContext ? Record : never) ) - & Hooks, TContext, Meta> + & Hooks, TInitialContext, Meta> export type CreateProcedureClientRest< - TContext extends Context, + TInitialContext extends Context, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TClientContext, > = - | [options: CreateProcedureClientOptions] - | (undefined extends TContext ? [] : never) + | [options: CreateProcedureClientOptions] + | (Record extends TInitialContext ? [] : never) export function createProcedureClient< - TContext extends Context, + TInitialContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, TClientContext, >( - lazyableProcedure: Lazyable>, - ...[options]: CreateProcedureClientRest + lazyableProcedure: Lazyable>, + ...[options]: CreateProcedureClientRest ): ProcedureClient { return async (...[input, callerOptions]) => { const path = options?.path ?? [] const { default: procedure } = await unlazy(lazyableProcedure) - const context = await value(options?.context, callerOptions?.context) as TContext + const context = await value(options?.context ?? {}, callerOptions?.context) as TInitialContext const errors = createORPCErrorConstructorMap(procedure['~orpc'].contract['~orpc'].errorMap) const executeOptions = { @@ -136,7 +136,7 @@ async function validateOutput(procedure: ANY_PROCEDURE, output: unknown): Promis return result.value } -async function executeProcedureInternal(procedure: ANY_PROCEDURE, options: ProcedureHandlerOptions): Promise { +async function executeProcedureInternal(procedure: ANY_PROCEDURE, options: ProcedureHandlerOptions): Promise { const middlewares = procedure['~orpc'].middlewares const inputValidationIndex = Math.min(Math.max(0, procedure['~orpc'].inputValidationIndex), middlewares.length) const outputValidationIndex = Math.min(Math.max(0, procedure['~orpc'].outputValidationIndex), middlewares.length) @@ -144,10 +144,10 @@ async function executeProcedureInternal(procedure: ANY_PROCEDURE, options: Proce let currentContext = options.context let currentInput = options.input - const next: MiddlewareNextFn = async (nextOptions) => { + const next: MiddlewareNextFn = async (...[nextOptions]) => { const index = currentIndex currentIndex += 1 - currentContext = mergeContext(currentContext, nextOptions.context) + currentContext = { ...currentContext, ...nextOptions?.context } if (index === inputValidationIndex) { currentInput = await validateInput(procedure, currentInput) diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts index 37ecbf55..74a3020f 100644 --- a/packages/server/src/procedure-decorated.test-d.ts +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -1,9 +1,9 @@ import type { Client, ClientRest, ORPCError } from '@orpc/contract' +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Middleware, MiddlewareOutputFn } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' -import type { WELL_CONTEXT } from './types' import { z } from 'zod' const baseSchema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) @@ -12,7 +12,7 @@ const baseErrors = { data: z.object({ why: z.string() }), }, } -const decorated = {} as DecoratedProcedure<{ auth: boolean }, { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> +const decorated = {} as DecoratedProcedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> describe('self chainable', () => { it('prefix', () => { @@ -58,7 +58,7 @@ describe('self chainable', () => { expectTypeOf(i).toEqualTypeOf< DecoratedProcedure< { auth: boolean }, - { db: string }, + { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, @@ -105,7 +105,7 @@ describe('self chainable', () => { expectTypeOf(i).toEqualTypeOf< DecoratedProcedure< { auth: boolean }, - { db: string } & { dev: boolean }, + { auth: boolean } & { db: string } & { dev: boolean } & Record, typeof baseSchema, typeof baseSchema, { val: string }, @@ -115,7 +115,7 @@ describe('self chainable', () => { }) it('use middleware with map input', () => { - const mid = {} as Middleware> + const mid = {} as Middleware> const i = decorated.use(mid, (input) => { expectTypeOf(input).toEqualTypeOf<{ val: number }>() @@ -125,7 +125,7 @@ describe('self chainable', () => { expectTypeOf(i).toEqualTypeOf< DecoratedProcedure< { auth: boolean }, - { db: string } & { extra: boolean }, + { auth: boolean } & { db: string } & { extra: boolean }, typeof baseSchema, typeof baseSchema, { val: string }, @@ -165,10 +165,10 @@ describe('self chainable', () => { }) it('handle middleware with output is typed', () => { - const mid1 = {} as Middleware> - const mid2 = {} as Middleware> - const mid3 = {} as Middleware> - const mid4 = {} as Middleware> + const mid1 = {} as Middleware, unknown, any, Record> + const mid2 = {} as Middleware, unknown, { val: string }, Record> + const mid3 = {} as Middleware, unknown, unknown, Record> + const mid4 = {} as Middleware, unknown, { val: number }, Record> decorated.use(mid1) decorated.use(mid2) @@ -190,18 +190,18 @@ describe('self chainable', () => { }) it('unshiftMiddleware', () => { - const mid1 = {} as Middleware> - const mid2 = {} as Middleware<{ auth: boolean }, undefined, unknown, any, Record> + const mid1 = {} as Middleware, unknown, any, Record> + const mid2 = {} as Middleware<{ auth: boolean }, Record, unknown, any, Record> const mid3 = {} as Middleware<{ auth: boolean }, { dev: boolean }, unknown, { val: number }, Record> expectTypeOf(decorated.unshiftMiddleware(mid1)).toEqualTypeOf() expectTypeOf(decorated.unshiftMiddleware(mid1, mid2)).toEqualTypeOf() expectTypeOf(decorated.unshiftMiddleware(mid1, mid2, mid3)).toEqualTypeOf() - const mid4 = {} as Middleware<{ auth: 'invalid' }, undefined, unknown, any, Record> - const mid5 = {} as Middleware<{ auth: boolean }, undefined, { val: number }, any, Record> - const mid7 = {} as Middleware<{ db: string }, undefined, unknown, { val: number }, Record> - const mid8 = {} as Middleware> + const mid4 = {} as Middleware<{ auth: 'invalid' }, Record, unknown, any, Record> + const mid5 = {} as Middleware<{ auth: boolean }, Record, { val: number }, any, Record> + const mid7 = {} as Middleware<{ db: string }, Record, unknown, { val: number }, Record> + const mid8 = {} as Middleware> // @ts-expect-error - context is not match decorated.unshiftMiddleware(mid4) @@ -209,13 +209,13 @@ describe('self chainable', () => { decorated.unshiftMiddleware(mid5) // @ts-expect-error - context is not match decorated.unshiftMiddleware(mid7) - // @ts-expect-error - extra context is conflict with context - decorated.unshiftMiddleware(mid8) + // extra context is conflict with context + expectTypeOf(decorated.unshiftMiddleware(mid8)).toEqualTypeOf() // @ts-expect-error - invalid middleware decorated.unshiftMiddleware(mid4, mid5, mid7, mid8) - const mid9 = {} as Middleware> - const mid10 = {} as Middleware> + const mid9 = {} as Middleware> + const mid10 = {} as Middleware> decorated.unshiftMiddleware(mid9) // @ts-expect-error - extra context of mid10 is conflict with extra context of mid9 @@ -233,7 +233,7 @@ describe('self chainable', () => { }) expectTypeOf(callable).toEqualTypeOf< - & Procedure<{ auth: boolean }, { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> + & Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> & Client<'something', { val: string }, { val: number }, Error | ORPCError<'CODE', { why: string }>> >() }) @@ -244,7 +244,7 @@ describe('self chainable', () => { }) expectTypeOf(actionable).toEqualTypeOf< - & Procedure<{ auth: boolean }, { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> + & Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseSchema, typeof baseSchema, { val: string }, typeof baseErrors> & ((...rest: ClientRest<'something', { val: string }>) => Promise<{ val: number }>) >() }) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 5975f22a..00898a24 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -1,31 +1,30 @@ import type { ClientRest, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { CreateProcedureClientRest, ProcedureClient } from './procedure-client' -import type { Context, MergeContext } from './types' import { DecoratedContractProcedure } from '@orpc/contract' import { decorateMiddleware } from './middleware-decorated' import { Procedure } from './procedure' import { createProcedureClient } from './procedure-client' export class DecoratedProcedure< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, -> extends Procedure { +> extends Procedure { static decorate< - UContext extends Context, - UExtraContext extends Context, + UInitialContext extends Context, + UCurrentContext extends Context, UInputSchema extends Schema, UOutputSchema extends Schema, UHandlerOutput extends SchemaInput, UErrorMap extends ErrorMap, >( - procedure: Procedure, + procedure: Procedure, ) { if (procedure instanceof DecoratedProcedure) { return procedure @@ -36,7 +35,7 @@ export class DecoratedProcedure< prefix( prefix: HTTPPath, - ): DecoratedProcedure { + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).prefix(prefix), @@ -45,57 +44,42 @@ export class DecoratedProcedure< route( route: RouteOptions, - ): DecoratedProcedure { + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).route(route), }) } - errors & ErrorMapSuggestions>(errors: U): DecoratedProcedure { + errors & ErrorMapSuggestions>(errors: U): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).errors(errors), }) } - use>>( + use( middleware: Middleware< - MergeContext, + TCurrentContext, U, SchemaOutput, THandlerOutput, ORPCErrorConstructorMap >, - ): DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - THandlerOutput, - TErrorMap - > - - use< - UExtra extends Context & ContextGuard>, - UInput = unknown, - >( + ): ConflictContextGuard + & DecoratedProcedure + + use( middleware: Middleware< - MergeContext, - UExtra, + TCurrentContext, + UOutContext, UInput, THandlerOutput, ORPCErrorConstructorMap >, mapInput: MapInputMiddleware, UInput>, - ): DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - THandlerOutput, - TErrorMap - > + ): ConflictContextGuard + & DecoratedProcedure use(middleware: Middleware, mapInput?: MapInputMiddleware): DecoratedProcedure { const middleware_ = mapInput @@ -108,22 +92,23 @@ export class DecoratedProcedure< }) } - unshiftTag(...tags: string[]): DecoratedProcedure { + unshiftTag(...tags: string[]): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], contract: DecoratedContractProcedure.decorate(this['~orpc'].contract).unshiftTag(...tags), }) } - unshiftMiddleware( + unshiftMiddleware( ...middlewares: Middleware< - TContext, - Context & Partial> | undefined, + TInitialContext, + U, unknown, SchemaOutput, ORPCErrorConstructorMap >[] - ): DecoratedProcedure { + ): ConflictContextGuard + & DecoratedProcedure { // FIXME: this is a hack to make the type checker happy, but it's not a good solution const castedMiddlewares = middlewares as ANY_MIDDLEWARE[] @@ -144,19 +129,21 @@ export class DecoratedProcedure< const numNewMiddlewares = castedMiddlewares.length - this['~orpc'].middlewares.length - return new DecoratedProcedure({ + const decorated = new DecoratedProcedure({ ...this['~orpc'], inputValidationIndex: this['~orpc'].inputValidationIndex + numNewMiddlewares, outputValidationIndex: this['~orpc'].outputValidationIndex + numNewMiddlewares, middlewares: castedMiddlewares, }) + + return decorated as typeof decorated & ConflictContextGuard } /** * Make this procedure callable (works like a function while still being a procedure). */ - callable(...rest: CreateProcedureClientRest): - & Procedure + callable(...rest: CreateProcedureClientRest): + & Procedure & ProcedureClient { return Object.assign(createProcedureClient(this, ...rest), { '~type': 'Procedure' as const, @@ -167,8 +154,8 @@ export class DecoratedProcedure< /** * Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action). */ - actionable(...rest: CreateProcedureClientRest): - & Procedure + actionable(...rest: CreateProcedureClientRest): + & Procedure & ((...rest: ClientRest>) => Promise>) { return this.callable(...rest) } diff --git a/packages/server/src/procedure-implementer.test-d.ts b/packages/server/src/procedure-implementer.test-d.ts index e901a841..7e77a096 100644 --- a/packages/server/src/procedure-implementer.test-d.ts +++ b/packages/server/src/procedure-implementer.test-d.ts @@ -1,9 +1,9 @@ +import type { Context } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Middleware, MiddlewareOutputFn } from './middleware' import type { ANY_PROCEDURE } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { ProcedureImplementer } from './procedure-implementer' -import type { WELL_CONTEXT } from './types' import { z } from 'zod' const baseSchema = z.object({ base: z.string().transform(v => Number.parseInt(v)) }) @@ -15,7 +15,7 @@ const baseErrors = { }, } -const implementer = {} as ProcedureImplementer<{ id?: string }, { extra: true }, typeof baseSchema, typeof baseSchema, typeof baseErrors> +const implementer = {} as ProcedureImplementer<{ id?: string }, { id?: string } & { extra: true }, typeof baseSchema, typeof baseSchema, typeof baseErrors> describe('self chainable', () => { it('use middleware', () => { @@ -45,21 +45,21 @@ describe('self chainable', () => { return next({}) }) - expectTypeOf(i).toEqualTypeOf< - ProcedureImplementer< - { id?: string }, - { auth: boolean } & { extra: true }, - typeof baseSchema, - typeof baseSchema, - typeof baseErrors - > - >() + const a = {} as ProcedureImplementer< + { id?: string }, + { id?: string } & { auth: boolean } & { extra: true } & Record, + typeof baseSchema, + typeof baseSchema, + typeof baseErrors + > + + expectTypeOf(i).toEqualTypeOf(a) }) it('use middleware with map input', () => { - const mid: Middleware> = ({ next }) => { + const mid: Middleware> = ({ next }) => { return next({ - context: { id: 'string', extra: true }, + context: { id: 'string', extra: true as const }, }) } @@ -71,7 +71,7 @@ describe('self chainable', () => { expectTypeOf(i).toEqualTypeOf< ProcedureImplementer< { id?: string }, - { extra: true } & { id: string, extra: true }, + { id?: string } & { extra: true } & { id: string, extra: true }, typeof baseSchema, typeof baseSchema, typeof baseErrors @@ -107,13 +107,17 @@ describe('self chainable', () => { // @ts-expect-error - conflict with context implementer.use(({ context, path, next }, input) => next({ context: { id: 1, extra: true } }), () => 'anything') + + // conflict context but not detected + expectTypeOf(implementer.use(({ next }) => next({ context: { extra: undefined } }))).toEqualTypeOf() + expectTypeOf(implementer.use(({ next }) => next({ context: { extra: undefined } }), () => {})).toEqualTypeOf() }) it('handle middleware with output is typed', () => { - const mid1 = {} as Middleware> - const mid2 = {} as Middleware> - const mid3 = {} as Middleware> - const mid4 = {} as Middleware> + const mid1 = {} as Middleware, unknown, any, Record> + const mid2 = {} as Middleware, unknown, { base: string }, Record> + const mid3 = {} as Middleware, unknown, unknown, Record> + const mid4 = {} as Middleware, unknown, { base: number }, Record> implementer.use(mid1) implementer.use(mid2) @@ -138,7 +142,7 @@ describe('to DecoratedProcedure', () => { }) expectTypeOf(procedure).toEqualTypeOf< - DecoratedProcedure<{ id?: string }, { extra: true }, typeof baseSchema, typeof baseSchema, { base: string }, typeof baseErrors> + DecoratedProcedure<{ id?: string }, { id?: string } & { extra: true }, typeof baseSchema, typeof baseSchema, { base: string }, typeof baseErrors> >() // @ts-expect-error - invalid output diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 3a04742d..73daaa65 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -1,74 +1,62 @@ import type { ContractProcedure, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { ProcedureHandler } from './procedure' -import type { Context, MergeContext } from './types' import { decorateMiddleware } from './middleware-decorated' import { DecoratedProcedure } from './procedure-decorated' export type ProcedureImplementerDef< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, TErrorMap extends ErrorMap, > = { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext contract: ContractProcedure - middlewares: Middleware, Partial | undefined, unknown, unknown, any>[] + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number } export class ProcedureImplementer< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, TErrorMap extends ErrorMap, > { '~type' = 'ProcedureImplementer' as const - '~orpc': ProcedureImplementerDef + '~orpc': ProcedureImplementerDef - constructor(def: ProcedureImplementerDef) { + constructor(def: ProcedureImplementerDef) { this['~orpc'] = def } - use>>( + use( middleware: Middleware< - MergeContext, + TCurrentContext, U, SchemaOutput, SchemaInput, ORPCErrorConstructorMap >, - ): ProcedureImplementer< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TErrorMap - > + ): ConflictContextGuard + & ProcedureImplementer - use< - UExtra extends Context & ContextGuard>, - UInput, - >( + use( middleware: Middleware< - MergeContext, - UExtra, + TCurrentContext, + UOutContext, UInput, SchemaInput, ORPCErrorConstructorMap >, mapInput: MapInputMiddleware, UInput>, - ): ProcedureImplementer< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TErrorMap - > + ): ConflictContextGuard + & ProcedureImplementer use( middleware: ANY_MIDDLEWARE, @@ -85,8 +73,8 @@ export class ProcedureImplementer< } handler>( - handler: ProcedureHandler, - ): DecoratedProcedure { + handler: ProcedureHandler, + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], handler, diff --git a/packages/server/src/procedure-utils.test-d.ts b/packages/server/src/procedure-utils.test-d.ts index 4561db7f..8d5f0dae 100644 --- a/packages/server/src/procedure-utils.test-d.ts +++ b/packages/server/src/procedure-utils.test-d.ts @@ -1,6 +1,6 @@ import type { ORPCError } from '@orpc/contract' +import type { Context } from './context' import type { Procedure } from './procedure' -import type { WELL_CONTEXT } from './types' import { safe } from '@orpc/contract' import { z } from 'zod' import { call } from './procedure-utils' @@ -17,7 +17,7 @@ const baseErrors = { }, } -const procedure = {} as Procedure +const procedure = {} as Procedure const procedureWithContext = {} as Procedure<{ db: string }, { auth: boolean }, typeof schema, typeof schema, { val: string }, typeof baseErrors> describe('call', () => { diff --git a/packages/server/src/procedure-utils.ts b/packages/server/src/procedure-utils.ts index 8e9dfa54..523cd93a 100644 --- a/packages/server/src/procedure-utils.ts +++ b/packages/server/src/procedure-utils.ts @@ -1,7 +1,7 @@ import type { ClientPromiseResult, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Context } from './context' import type { Lazyable } from './lazy' import type { Procedure } from './procedure' -import type { Context } from './types' import { createProcedureClient, type CreateProcedureClientRest } from './procedure-client' /** @@ -15,15 +15,15 @@ import { createProcedureClient, type CreateProcedureClientRest } from './procedu * */ export function call< - TContext extends Context, + TInitialContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, >( - procedure: Lazyable>, + procedure: Lazyable>, input: SchemaInput, - ...rest: CreateProcedureClientRest + ...rest: CreateProcedureClientRest ): ClientPromiseResult, ErrorFromErrorMap> { return createProcedureClient(procedure, ...rest)(input) } diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 71a2d6cd..f2d92cec 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -1,18 +1,18 @@ import type { ContractProcedure, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Promisable } from '@orpc/shared' +import type { Context, TypeInitialContext } from './context' import type { ORPCErrorConstructorMap } from './error' import type { Lazy } from './lazy' import type { Middleware } from './middleware' -import type { AbortSignal, Context, MergeContext } from './types' +import type { AbortSignal } from './types' import { isContractProcedure } from '@orpc/contract' export interface ProcedureHandlerOptions< - TContext extends Context, - TExtraContext extends Context, + TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap, > { - context: MergeContext + context: TCurrentContext input: TInput path: string[] procedure: ANY_PROCEDURE @@ -21,15 +21,14 @@ export interface ProcedureHandlerOptions< } export interface ProcedureHandler< - TContext extends Context, - TExtraContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, > { ( - opt: ProcedureHandlerOptions, ORPCErrorConstructorMap> + opt: ProcedureHandlerOptions, ORPCErrorConstructorMap> ): Promisable> } @@ -45,32 +44,33 @@ export interface ProcedureHandler< * The only downside is that direct access to them requires careful type checking to ensure safety. */ export interface ProcedureDef< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, > { - middlewares: Middleware, Partial | undefined, unknown, unknown, any>[] + __initialContext?: TypeInitialContext + middlewares: Middleware[] inputValidationIndex: number outputValidationIndex: number contract: ContractProcedure - handler: ProcedureHandler + handler: ProcedureHandler } export class Procedure< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, THandlerOutput extends SchemaInput, TErrorMap extends ErrorMap, > { '~type' = 'Procedure' as const - '~orpc': ProcedureDef + '~orpc': ProcedureDef - constructor(def: ProcedureDef) { + constructor(def: ProcedureDef) { this['~orpc'] = def } } diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index 217c7b6e..ceea9a0a 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -1,10 +1,10 @@ +import type { Context } from './context' import type { Lazy } from './lazy' import type { DecoratedLazy } from './lazy-decorated' import type { Middleware, MiddlewareOutputFn } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' -import type { WELL_CONTEXT } from './types' import { z } from 'zod' import { lazy } from './lazy' @@ -14,11 +14,11 @@ const baseErrors = { }, } -const builder = {} as RouterBuilder<{ auth: boolean }, { db: string }, typeof baseErrors> +const builder = {} as RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string }, typeof baseErrors> describe('AdaptedRouter', () => { - const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown, typeof baseErrors> - const pong = {} as Procedure> + const ping = {} as Procedure<{ auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> + const pong = {} as Procedure> it('without lazy', () => { const router = { @@ -32,16 +32,16 @@ describe('AdaptedRouter', () => { const adapted = {} as AdaptedRouter<{ log: true, auth: boolean }, typeof router, typeof baseErrors> expectTypeOf(adapted.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, { db: string }, undefined, undefined, unknown, typeof baseErrors> + DecoratedProcedure<{ log: true, auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> >() expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, undefined, undefined, undefined, unknown, Record & typeof baseErrors> + DecoratedProcedure<{ log: true, auth: boolean }, Context, undefined, undefined, unknown, Record & typeof baseErrors> >() expectTypeOf(adapted.nested.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, { db: string }, undefined, undefined, unknown, typeof baseErrors> + DecoratedProcedure<{ log: true, auth: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> >() expectTypeOf(adapted.nested.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true, auth: boolean }, undefined, undefined, undefined, unknown, Record & typeof baseErrors> + DecoratedProcedure<{ log: true, auth: boolean }, Context, undefined, undefined, unknown, Record & typeof baseErrors> >() }) @@ -57,29 +57,29 @@ describe('AdaptedRouter', () => { })), } - const adapted = {} as AdaptedRouter<{ log: true } | undefined, typeof router, typeof baseErrors> + const adapted = {} as AdaptedRouter<{ log: true }, typeof router, typeof baseErrors> expectTypeOf(adapted.ping).toEqualTypeOf + DecoratedProcedure<{ log: true }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> >>() expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true } | undefined, undefined, undefined, undefined, unknown, Record & typeof baseErrors> + DecoratedProcedure<{ log: true }, Context, undefined, undefined, unknown, Record & typeof baseErrors> >() expectTypeOf(adapted.nested.ping).toEqualTypeOf + DecoratedProcedure<{ log: true }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> >>() expectTypeOf(adapted.nested.pong).toEqualTypeOf & typeof baseErrors> + DecoratedProcedure<{ log: true }, Context, undefined, undefined, unknown, Record & typeof baseErrors> >>() }) it('with procedure', () => { expectTypeOf>().toEqualTypeOf< - DecoratedProcedure<{ log: boolean }, { db: string }, undefined, undefined, unknown, typeof baseErrors> + DecoratedProcedure<{ log: boolean }, { auth: boolean } & { db: string }, undefined, undefined, unknown, typeof baseErrors> >() expectTypeOf, typeof baseErrors>>().toEqualTypeOf< - DecoratedLazy> + DecoratedLazy> >() }) }) @@ -117,21 +117,23 @@ describe('self chainable', () => { return next({}) }) - const mid1 = {} as Middleware<{ auth: boolean }, undefined, unknown, unknown, Record> + const mid1 = {} as Middleware<{ auth: boolean }, Record, unknown, unknown, Record> const mid2 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown, Record> const mid3 = {} as Middleware<{ auth: boolean, db: string }, { dev: string }, unknown, unknown, Record> - expectTypeOf(builder.use(mid1)).toEqualTypeOf() + expectTypeOf(builder.use(mid1)).toEqualTypeOf< + RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string } & Record, typeof baseErrors> + >() expectTypeOf(builder.use(mid2)).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { db: string } & { dev: string }, typeof baseErrors> + RouterBuilder<{ auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof baseErrors> >() expectTypeOf(builder.use(mid3)).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { db: string } & { dev: string }, typeof baseErrors> + RouterBuilder < { auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof baseErrors> >() const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }, Record> const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }, Record> - const mid6 = {} as Middleware<{ auth: 'invalid' }, undefined, any, unknown, Record> + const mid6 = {} as Middleware<{ auth: 'invalid' }, Context, any, unknown, Record> // @ts-expect-error - invalid middleware builder.use(mid4) @@ -143,6 +145,9 @@ describe('self chainable', () => { builder.use(true) // @ts-expect-error - invalid middleware builder.use(() => {}) + + // conflict context but not detected + expectTypeOf(builder.use(({ next }) => next({ context: { auth: undefined } }))).toEqualTypeOf() }) it('errors', () => { @@ -155,7 +160,7 @@ describe('self chainable', () => { const applied = builder.errors(errors) expectTypeOf(applied).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { db: string }, typeof errors & typeof baseErrors> + RouterBuilder < { auth: boolean }, { auth: boolean } & { db: string }, typeof errors & typeof baseErrors> >() // @ts-expect-error - not allow redefine errors @@ -168,9 +173,9 @@ describe('self chainable', () => { describe('to AdaptedRouter', () => { const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors> - const pong = {} as Procedure> + const pong = {} as Procedure> - const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, Record> + const wrongPing = {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, Record> it('router without lazy', () => { const router = { ping, pong, nested: { ping, pong } } @@ -224,9 +229,9 @@ describe('to AdaptedRouter', () => { describe('to Decorated Adapted Lazy', () => { const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors> - const pong = {} as Procedure> + const pong = {} as Procedure> - const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, Record> + const wrongPing = {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, Record> it('router without lazy', () => { const router = { diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 43f5fa01..e92f992a 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,11 +1,9 @@ import type { ContractRouter, ErrorMap, ErrorMapGuard, ErrorMapSuggestions, HTTPPath, StrictErrorMap } from '@orpc/contract' -import type { ContextGuard } from './context' -import type { ORPCErrorConstructorMap } from './error' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { FlattenLazy, Lazy } from './lazy' import type { ANY_MIDDLEWARE, Middleware } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' -import type { Context, MergeContext } from './types' import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' import { flatLazy, isLazy, lazy, unlazy } from './lazy' import { type DecoratedLazy, decorateLazy } from './lazy-decorated' @@ -13,33 +11,39 @@ import { isProcedure } from './procedure' import { DecoratedProcedure } from './procedure-decorated' export type AdaptedRouter< - TContext extends Context, + TInitialContext extends Context, TRouter extends ANY_ROUTER, TErrorMapExtra extends ErrorMap, > = TRouter extends Lazy - ? DecoratedLazy> - : TRouter extends Procedure - ? DecoratedProcedure + ? DecoratedLazy> + : TRouter extends Procedure + ? DecoratedProcedure : { - [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never + [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never } -export type RouterBuilderDef = { +export type RouterBuilderDef< + TInitialContext extends Context, + TCurrentContext extends Context, + TErrorMap extends ErrorMap, +> = { + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext prefix?: HTTPPath tags?: readonly string[] - middlewares: Middleware, Partial | undefined, unknown, any, ORPCErrorConstructorMap>[] + middlewares: Middleware[] errorMap: TErrorMap } export class RouterBuilder< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TErrorMap extends ErrorMap, > { '~type' = 'RouterBuilder' as const - '~orpc': RouterBuilderDef + '~orpc': RouterBuilderDef - constructor(def: RouterBuilderDef) { + constructor(def: RouterBuilderDef) { this['~orpc'] = def if (def.prefix && def.prefix.includes('{')) { @@ -50,21 +54,21 @@ export class RouterBuilder< } } - prefix(prefix: HTTPPath): RouterBuilder { + prefix(prefix: HTTPPath): RouterBuilder { return new RouterBuilder({ ...this['~orpc'], prefix: `${this['~orpc'].prefix ?? ''}${prefix}`, }) } - tag(...tags: string[]): RouterBuilder { + tag(...tags: string[]): RouterBuilder { return new RouterBuilder({ ...this['~orpc'], tags: [...(this['~orpc'].tags ?? []), ...tags], }) } - errors & ErrorMapSuggestions>(errors: U): RouterBuilder { + errors & ErrorMapSuggestions>(errors: U): RouterBuilder { return new RouterBuilder({ ...this['~orpc'], errorMap: { @@ -74,31 +78,30 @@ export class RouterBuilder< }) } - use>>( - middleware: Middleware< - MergeContext, - U, - unknown, - unknown, - Record - >, - ): RouterBuilder, TErrorMap> { - return new RouterBuilder({ - ...this['~orpc'], + use( + middleware: Middleware >, + ): ConflictContextGuard + & RouterBuilder { + const builder = new RouterBuilder({ + tags: this['~orpc'].tags, + prefix: this['~orpc'].prefix, + errorMap: this['~orpc'].errorMap, middlewares: [...this['~orpc'].middlewares, middleware as any], }) + + return builder as typeof builder & ConflictContextGuard } - router, ContractRouter>>>>( + router>>>>( router: U, - ): AdaptedRouter { + ): AdaptedRouter { const adapted = adapt(router, this['~orpc']) return adapted as any } - lazy, ContractRouter>>>>( + lazy>>>>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, TErrorMap> { + ): AdaptedRouter, TErrorMap> { const adapted = adapt(flatLazy(lazy(loader)), this['~orpc']) return adapted as any } diff --git a/packages/server/src/router-client.test-d.ts b/packages/server/src/router-client.test-d.ts index d3ba0f2c..12b537f2 100644 --- a/packages/server/src/router-client.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -1,6 +1,7 @@ import type { Client, NestedClient, ORPCError } from '@orpc/contract' +import type { Context } from './context' import type { Procedure } from './procedure' -import type { Meta, WELL_CONTEXT } from './types' +import type { Meta } from './types' import { z } from 'zod' import { lazy } from './lazy' import { createRouterClient, type RouterClient } from './router-client' @@ -12,8 +13,8 @@ const baseErrors = { }, } -const ping = {} as Procedure -const pong = {} as Procedure<{ auth: boolean }, undefined, undefined, undefined, unknown, Record> +const ping = {} as Procedure +const pong = {} as Procedure<{ auth: boolean }, { auth: boolean }, undefined, undefined, unknown, Record> const router = { ping, @@ -109,9 +110,7 @@ describe('createRouterClient', () => { context: { auth: true }, onSuccess: async ({ output }, context, meta) => { expectTypeOf(output).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf & { - auth: boolean - }>() + expectTypeOf(context).toEqualTypeOf() expectTypeOf(meta).toEqualTypeOf() }, }) diff --git a/packages/server/src/router-implementer.test-d.ts b/packages/server/src/router-implementer.test-d.ts index 218df9e9..68041e6a 100644 --- a/packages/server/src/router-implementer.test-d.ts +++ b/packages/server/src/router-implementer.test-d.ts @@ -58,7 +58,7 @@ const routerWithLazy = { })), } -const implementer = {} as RouterImplementer<{ auth: boolean }, { db: string }, typeof contract> +const implementer = {} as RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string }, typeof contract> describe('self chainable', () => { it('use middleware', () => { @@ -74,21 +74,21 @@ describe('self chainable', () => { return next({}) }) - const mid1 = {} as Middleware<{ auth: boolean }, undefined, unknown, unknown, Record> - const mid2 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown, Record> - const mid3 = {} as Middleware<{ auth: boolean, db: string }, { dev: string }, unknown, unknown, Record> + const mid1 = {} as Middleware<{ auth: boolean }, { auth: boolean }, unknown, unknown, Record> + const mid2 = {} as Middleware<{ auth: boolean }, { auth: boolean } & { dev: string }, unknown, unknown, Record> + const mid3 = {} as Middleware<{ auth: boolean, db: string }, { auth: boolean, db: string } & { dev: string }, unknown, unknown, Record> expectTypeOf(implementer.use(mid1)).toEqualTypeOf() expectTypeOf(implementer.use(mid2)).toEqualTypeOf< - RouterImplementer<{ auth: boolean }, { db: string } & { dev: string }, typeof contract> + RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string } & { dev: string }, typeof contract> >() expectTypeOf(implementer.use(mid3)).toEqualTypeOf< - RouterImplementer<{ auth: boolean }, { db: string } & { dev: string }, typeof contract> + RouterImplementer<{ auth: boolean }, { auth: boolean } & { db: string } & { auth: boolean, db: string } & { dev: string }, typeof contract> >() - const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }, Record> - const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }, Record> - const mid6 = {} as Middleware<{ auth: 'invalid' }, undefined, any, any, Record> + const mid4 = {} as Middleware<{ auth: boolean }, { auth: boolean, dev: string }, unknown, { val: string }, Record> + const mid5 = {} as Middleware<{ auth: boolean }, { auth: boolean, dev: string }, unknown, { val: number }, Record> + const mid6 = {} as Middleware<{ auth: 'invalid' }, { auth: 'invalid' }, any, any, Record> // @ts-expect-error - invalid middleware implementer.use(mid4) @@ -100,6 +100,9 @@ describe('self chainable', () => { implementer.use(true) // @ts-expect-error - invalid middleware implementer.use(() => {}) + + // conflict context but not detected + expectTypeOf(implementer.use(({ next }) => next({ context: { auth: undefined } }))).toEqualTypeOf() }) }) diff --git a/packages/server/src/router-implementer.ts b/packages/server/src/router-implementer.ts index 27c287c3..73407363 100644 --- a/packages/server/src/router-implementer.ts +++ b/packages/server/src/router-implementer.ts @@ -1,52 +1,56 @@ import type { ContractRouter } from '@orpc/contract' -import type { ContextGuard } from './context' +import type { ConflictContextGuard, Context, TypeCurrentContext, TypeInitialContext } from './context' import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context, MergeContext } from './types' import { setRouterContract } from './hidden' import { RouterBuilder } from './router-builder' export interface RouterImplementerDef< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TContract extends ContractRouter, > { - middlewares: Middleware, Partial | undefined, unknown, any, Record>[] + __initialContext?: TypeInitialContext + __currentContext?: TypeCurrentContext + middlewares: Middleware[] contract: TContract } export class RouterImplementer< - TContext extends Context, - TExtraContext extends Context, + TInitialContext extends Context, + TCurrentContext extends Context, TContract extends ContractRouter, > { '~type' = 'RouterImplementer' as const - '~orpc': RouterImplementerDef + '~orpc': RouterImplementerDef - constructor(def: RouterImplementerDef) { + constructor(def: RouterImplementerDef) { this['~orpc'] = def } - use>>( + use( middleware: Middleware< - MergeContext, + TCurrentContext, U, unknown, unknown, Record >, - ): RouterImplementer, TContract> { - return new RouterImplementer({ - ...this['~orpc'], - middlewares: [...(this['~orpc'].middlewares ?? []), middleware as any], + ): ConflictContextGuard + & RouterImplementer { + const builder = new RouterImplementer({ + contract: this['~orpc'].contract, + middlewares: [...this['~orpc'].middlewares, middleware], }) + + return builder as typeof builder & ConflictContextGuard } - router, TContract>>( + router>( router: U, - ): AdaptedRouter> { + ): AdaptedRouter> { const adapted = new RouterBuilder({ ...this['~orpc'], errorMap: {}, @@ -57,9 +61,9 @@ export class RouterImplementer< return contracted } - lazy, TContract>>( + lazy>( loader: () => Promise<{ default: U }>, - ): AdaptedRouter, Record> { + ): AdaptedRouter, Record> { const adapted = new RouterBuilder({ ...this['~orpc'], errorMap: {}, diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index f897afcd..fe5be2a5 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -1,7 +1,7 @@ +import type { Context } from './context' import type { ANY_LAZY, Lazy } from './lazy' import type { Procedure } from './procedure' import type { ANY_ROUTER, InferRouterInputs, InferRouterOutputs, Router } from './router' -import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' import { lazy } from './lazy' @@ -15,8 +15,8 @@ const baseErrors = { }, } as const -const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors> -const pong = {} as Procedure> +const ping = {} as Procedure<{ auth: boolean }, { auth: boolean, db: string }, typeof schema, typeof schema, { val: string }, typeof baseErrors> +const pong = {} as Procedure> const router = { ping: lazy(() => Promise.resolve({ default: ping })), @@ -63,8 +63,8 @@ it('InferRouterOutputs', () => { describe('Router', () => { it('require match context', () => { - const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown, Record> - const pong = {} as Procedure<{ auth: string }, undefined, undefined, undefined, unknown, Record> + const ping = {} as Procedure<{ auth: boolean }, { auth: boolean, db: string }, undefined, undefined, unknown, Record> + const pong = {} as Procedure<{ auth: string }, { auth: string }, undefined, undefined, unknown, Record> const router: Router<{ auth: boolean, userId: string }, any> = { ping, @@ -143,7 +143,7 @@ describe('Router', () => { }) const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown, Record> - const pong = {} as Procedure> + const pong = {} as Procedure> const router1: Router<{ auth: boolean, userId: string }, typeof contract> = { ping, diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index dfb3f66d..9bad3499 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -1,18 +1,18 @@ import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Context } from './context' import type { ANY_LAZY, Lazy, Lazyable } from './lazy' import type { ANY_PROCEDURE, Procedure } from './procedure' -import type { Context } from './types' import { flatLazy, isLazy, lazy, unlazy } from './lazy' import { isProcedure } from './procedure' export type Router< - TContext extends Context, + TInitialContext extends Context, TContract extends ContractRouter, > = Lazyable< TContract extends ContractProcedure - ? Procedure + ? Procedure : { - [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never + [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never } > diff --git a/packages/server/src/types.test-d.ts b/packages/server/src/types.test-d.ts deleted file mode 100644 index d14c1d0e..00000000 --- a/packages/server/src/types.test-d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MergeContext } from './types' - -it('mergeContext', () => { - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf<{ - foo: string - }>() - expectTypeOf>().toEqualTypeOf<{ - foo: string - }>() - expectTypeOf>().toEqualTypeOf<{ - foo: string - }>() - expectTypeOf>().toMatchTypeOf<{ - foo: string - bar: string - }>() -}) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 5da34fef..8467051e 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,14 +1,6 @@ import type { FindGlobalInstanceType } from '@orpc/shared' import type { ANY_PROCEDURE } from './procedure' -export type Context = Record | undefined -export type WELL_CONTEXT = Record | undefined - -export type MergeContext< - TA extends Context, - TB extends Context, -> = TA extends undefined ? TB : TB extends undefined ? TA : TA & TB - export type AbortSignal = FindGlobalInstanceType<'AbortSignal'> export interface WithSignal { diff --git a/packages/server/src/utils.test.ts b/packages/server/src/utils.test.ts deleted file mode 100644 index ac85c6b2..00000000 --- a/packages/server/src/utils.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { mergeContext } from './utils' - -it('mergeContext', () => { - expect(mergeContext(undefined, undefined)).toBe(undefined) - expect(mergeContext(undefined, { foo: 'bar' })).toEqual({ foo: 'bar' }) - expect(mergeContext({ foo: 'bar' }, undefined)).toEqual({ foo: 'bar' }) - expect(mergeContext({ foo: 'bar' }, { foo: 'bar' })).toEqual({ foo: 'bar' }) - expect(mergeContext({ foo: 'bar' }, { bar: 'bar' })).toEqual({ - foo: 'bar', - bar: 'bar', - }) - expect(mergeContext({ foo: 'bar' }, { bar: 'bar', foo: 'bar1' })).toEqual({ - foo: 'bar1', - bar: 'bar', - }) -}) diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts deleted file mode 100644 index e9b4935f..00000000 --- a/packages/server/src/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Context, MergeContext } from './types' - -export function mergeContext( - a: A, - b: B, -): MergeContext { - if (!a) - return b as any - if (!b) - return a as any - - return { - ...a, - ...b, - } as any -}