From b1839c6be9191c86ca9fce5674b4494ae74be422 Mon Sep 17 00:00:00 2001 From: Elvin Dzhavadov Date: Mon, 11 May 2026 16:40:26 -0700 Subject: [PATCH] feat(server,contract): narrow TMeta through .meta() chains .meta() now returns a builder whose TMeta reflects the values actually passed in: Omit & U. .meta() takes Partial and constraint is in a new TMetaDef generic - only changed fields must be passed. --- .../contract/src/builder-variants.test-d.ts | 5 + packages/contract/src/builder-variants.ts | 54 ++++---- packages/contract/src/builder.test-d.ts | 2 + packages/contract/src/builder.ts | 27 ++-- packages/contract/src/meta.ts | 8 +- .../server/src/builder-variants.test-d.ts | 7 +- packages/server/src/builder-variants.ts | 117 ++++++++++-------- packages/server/src/builder.test-d.ts | 53 +++++++- packages/server/src/builder.ts | 38 +++--- .../server/src/procedure-decorated.test-d.ts | 3 +- packages/server/src/procedure-decorated.ts | 25 ++-- 11 files changed, 219 insertions(+), 120 deletions(-) diff --git a/packages/contract/src/builder-variants.test-d.ts b/packages/contract/src/builder-variants.test-d.ts index b40d515fd..16915fbf0 100644 --- a/packages/contract/src/builder-variants.test-d.ts +++ b/packages/contract/src/builder-variants.test-d.ts @@ -3,6 +3,7 @@ import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../tests import type { ContractBuilder } from './builder' import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithInputOutput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' import type { MergedErrorMap } from './error' +import type { MergedMeta } from './meta' import type { ContractProcedure } from './procedure' import type { EnhancedContractRouter } from './router-utils' import type { Schema } from './schema' @@ -40,6 +41,7 @@ describe('ContractProcedureBuilder', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -121,6 +123,7 @@ describe('ContractProcedureBuilderWithInput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -188,6 +191,7 @@ describe('ContractProcedureBuilderWithOutput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -255,6 +259,7 @@ it('ContractProcedureBuilderWithInputOutput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() diff --git a/packages/contract/src/builder-variants.ts b/packages/contract/src/builder-variants.ts index 3b8dc7799..454b0243e 100644 --- a/packages/contract/src/builder-variants.ts +++ b/packages/contract/src/builder-variants.ts @@ -1,6 +1,6 @@ import type { HTTPPath } from '@orpc/client' import type { ErrorMap, MergedErrorMap } from './error' -import type { Meta } from './meta' +import type { MergedMeta, Meta } from './meta' import type { ContractProcedure } from './procedure' import type { Route } from './route' import type { ContractRouter } from './router' @@ -12,6 +12,7 @@ export interface ContractProcedureBuilder< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > extends ContractProcedure { /** * Adds type-safe custom errors to the contract. @@ -21,7 +22,7 @@ export interface ContractProcedureBuilder< */ errors( errors: U, - ): ContractProcedureBuilder, TMeta> + ): ContractProcedureBuilder, TMeta, TMetaDef> /** * Sets or updates the metadata for the contract. @@ -29,9 +30,9 @@ export interface ContractProcedureBuilder< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ContractProcedureBuilder + meta>( + meta: U, + ): ContractProcedureBuilder, TMetaDef> /** * Sets or updates the route definition for the contract. @@ -43,7 +44,7 @@ export interface ContractProcedureBuilder< */ route( route: Route, - ): ContractProcedureBuilder + ): ContractProcedureBuilder /** * Defines the input validation schema for the contract. @@ -52,7 +53,7 @@ export interface ContractProcedureBuilder< */ input( schema: U, - ): ContractProcedureBuilderWithInput + ): ContractProcedureBuilderWithInput /** * Defines the output validation schema for the contract. @@ -61,7 +62,7 @@ export interface ContractProcedureBuilder< */ output( schema: U, - ): ContractProcedureBuilderWithOutput + ): ContractProcedureBuilderWithOutput } export interface ContractProcedureBuilderWithInput< @@ -69,6 +70,7 @@ export interface ContractProcedureBuilderWithInput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, >extends ContractProcedure { /** * Adds type-safe custom errors to the contract. @@ -78,7 +80,7 @@ export interface ContractProcedureBuilderWithInput< */ errors( errors: U, - ): ContractProcedureBuilderWithInput, TMeta> + ): ContractProcedureBuilderWithInput, TMeta, TMetaDef> /** * Sets or updates the metadata for the contract. @@ -86,9 +88,9 @@ export interface ContractProcedureBuilderWithInput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ContractProcedureBuilderWithInput + meta>( + meta: U, + ): ContractProcedureBuilderWithInput, TMetaDef> /** * Sets or updates the route definition for the contract. @@ -100,7 +102,7 @@ export interface ContractProcedureBuilderWithInput< */ route( route: Route, - ): ContractProcedureBuilderWithInput + ): ContractProcedureBuilderWithInput /** * Defines the output validation schema for the contract. @@ -109,7 +111,7 @@ export interface ContractProcedureBuilderWithInput< */ output( schema: U, - ): ContractProcedureBuilderWithInputOutput + ): ContractProcedureBuilderWithInputOutput } export interface ContractProcedureBuilderWithOutput< @@ -117,6 +119,7 @@ export interface ContractProcedureBuilderWithOutput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > extends ContractProcedure { /** * Adds type-safe custom errors to the contract. @@ -126,7 +129,7 @@ export interface ContractProcedureBuilderWithOutput< */ errors( errors: U, - ): ContractProcedureBuilderWithOutput, TMeta> + ): ContractProcedureBuilderWithOutput, TMeta, TMetaDef> /** * Sets or updates the metadata for the contract. @@ -134,9 +137,9 @@ export interface ContractProcedureBuilderWithOutput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ContractProcedureBuilderWithOutput + meta>( + meta: U, + ): ContractProcedureBuilderWithOutput, TMetaDef> /** * Sets or updates the route definition for the contract. @@ -148,7 +151,7 @@ export interface ContractProcedureBuilderWithOutput< */ route( route: Route, - ): ContractProcedureBuilderWithOutput + ): ContractProcedureBuilderWithOutput /** * Defines the input validation schema for the contract. @@ -157,7 +160,7 @@ export interface ContractProcedureBuilderWithOutput< */ input( schema: U, - ): ContractProcedureBuilderWithInputOutput + ): ContractProcedureBuilderWithInputOutput } export interface ContractProcedureBuilderWithInputOutput< @@ -165,6 +168,7 @@ export interface ContractProcedureBuilderWithInputOutput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > extends ContractProcedure { /** * Adds type-safe custom errors to the contract. @@ -174,7 +178,7 @@ export interface ContractProcedureBuilderWithInputOutput< */ errors( errors: U, - ): ContractProcedureBuilderWithInputOutput, TMeta> + ): ContractProcedureBuilderWithInputOutput, TMeta, TMetaDef> /** * Sets or updates the metadata for the contract. @@ -182,9 +186,9 @@ export interface ContractProcedureBuilderWithInputOutput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ContractProcedureBuilderWithInputOutput + meta>( + meta: U, + ): ContractProcedureBuilderWithInputOutput, TMetaDef> /** * Sets or updates the route definition for the contract. @@ -196,7 +200,7 @@ export interface ContractProcedureBuilderWithInputOutput< */ route( route: Route, - ): ContractProcedureBuilderWithInputOutput + ): ContractProcedureBuilderWithInputOutput } export interface ContractRouterBuilder< diff --git a/packages/contract/src/builder.test-d.ts b/packages/contract/src/builder.test-d.ts index ed9818e9d..f2cc6fc69 100644 --- a/packages/contract/src/builder.test-d.ts +++ b/packages/contract/src/builder.test-d.ts @@ -2,6 +2,7 @@ import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../tests import type { ContractBuilder } from './builder' import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' import type { MergedErrorMap } from './error' +import type { MergedMeta } from './meta' import type { ContractProcedure } from './procedure' import type { EnhancedContractRouter } from './router-utils' import type { Schema } from './schema' @@ -70,6 +71,7 @@ describe('ContractBuilder', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() diff --git a/packages/contract/src/builder.ts b/packages/contract/src/builder.ts index 4e04711f0..e6cd92c93 100644 --- a/packages/contract/src/builder.ts +++ b/packages/contract/src/builder.ts @@ -1,7 +1,7 @@ import type { HTTPPath } from '@orpc/client' import type { ContractProcedureBuilder, ContractProcedureBuilderWithInput, ContractProcedureBuilderWithOutput, ContractRouterBuilder } from './builder-variants' import type { ErrorMap, MergedErrorMap } from './error' -import type { Meta } from './meta' +import type { MergedMeta, Meta } from './meta' import type { ContractProcedureDef } from './procedure' import type { Route } from './route' import type { ContractRouter } from './router' @@ -26,6 +26,7 @@ export class ContractBuilder< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > extends ContractProcedure { /** * This property holds the defined options for the contract. @@ -46,7 +47,7 @@ export class ContractBuilder< */ $meta( initialMeta: U, - ): ContractBuilder> { + ): ContractBuilder, U> { /** * We need `& Record` to deal with `has no properties in common with type` error */ @@ -66,7 +67,7 @@ export class ContractBuilder< */ $route( initialRoute: Route, - ): ContractBuilder { + ): ContractBuilder { return new ContractBuilder({ ...this['~orpc'], route: initialRoute, @@ -80,7 +81,7 @@ export class ContractBuilder< */ $input( initialInputSchema?: U, - ): ContractBuilder { + ): ContractBuilder { return new ContractBuilder({ ...this['~orpc'], inputSchema: initialInputSchema, @@ -95,7 +96,7 @@ export class ContractBuilder< */ errors( errors: U, - ): ContractBuilder, TMeta> { + ): ContractBuilder, TMeta, TMetaDef> { return new ContractBuilder({ ...this['~orpc'], errorMap: mergeErrorMap(this['~orpc'].errorMap, errors), @@ -108,9 +109,9 @@ export class ContractBuilder< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ContractProcedureBuilder { + meta>( + meta: U, + ): ContractProcedureBuilder, TMetaDef> { return new ContractBuilder({ ...this['~orpc'], meta: mergeMeta(this['~orpc'].meta, meta), @@ -127,7 +128,7 @@ export class ContractBuilder< */ route( route: Route, - ): ContractProcedureBuilder { + ): ContractProcedureBuilder { return new ContractBuilder({ ...this['~orpc'], route: mergeRoute(this['~orpc'].route, route), @@ -141,8 +142,8 @@ export class ContractBuilder< */ input( schema: U, - ): ContractProcedureBuilderWithInput { - return new ContractBuilder({ + ): ContractProcedureBuilderWithInput { + return new ContractBuilder({ ...this['~orpc'], inputSchema: schema, }) @@ -155,8 +156,8 @@ export class ContractBuilder< */ output( schema: U, - ): ContractProcedureBuilderWithOutput { - return new ContractBuilder({ + ): ContractProcedureBuilderWithOutput { + return new ContractBuilder({ ...this['~orpc'], outputSchema: schema, }) diff --git a/packages/contract/src/meta.ts b/packages/contract/src/meta.ts index 2220d191e..9080ea25d 100644 --- a/packages/contract/src/meta.ts +++ b/packages/contract/src/meta.ts @@ -1,5 +1,11 @@ export type Meta = Record -export function mergeMeta(meta1: T, meta2: T): T { +/** + * Merges two meta types with override semantics matching runtime spread: + * keys in `U` replace keys in `T`. + */ +export type MergedMeta = Omit & U + +export function mergeMeta(meta1: T, meta2: U): MergedMeta { return { ...meta1, ...meta2 } } diff --git a/packages/server/src/builder-variants.test-d.ts b/packages/server/src/builder-variants.test-d.ts index 48913d513..93d387eaf 100644 --- a/packages/server/src/builder-variants.test-d.ts +++ b/packages/server/src/builder-variants.test-d.ts @@ -1,4 +1,4 @@ -import type { AnySchema, ContractProcedure, ErrorMap, MergedErrorMap, Schema } from '@orpc/contract' +import type { AnySchema, ContractProcedure, ErrorMap, MergedErrorMap, MergedMeta, Schema } from '@orpc/contract' import type { OmitChainMethodDeep } from '@orpc/shared' import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' import type { CurrentContext, InitialContext } from '../tests/shared' @@ -135,6 +135,7 @@ describe('BuilderWithMiddlewares', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -396,6 +397,7 @@ describe('ProcedureBuilder', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -653,6 +655,7 @@ describe('ProcedureBuilderWithInput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -833,6 +836,7 @@ describe('ProcedureBuilderWithOutput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -1077,6 +1081,7 @@ describe('ProcedureBuilderWithInputOutput', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() diff --git a/packages/server/src/builder-variants.ts b/packages/server/src/builder-variants.ts index 427aa7744..3a93035a1 100644 --- a/packages/server/src/builder-variants.ts +++ b/packages/server/src/builder-variants.ts @@ -1,5 +1,5 @@ import type { HTTPPath } from '@orpc/client' -import type { AnySchema, ContractRouter, ErrorMap, InferSchemaInput, InferSchemaOutput, MergedErrorMap, Meta, Route, Schema } from '@orpc/contract' +import type { AnySchema, ContractRouter, ErrorMap, InferSchemaInput, InferSchemaOutput, MergedErrorMap, MergedMeta, Meta, Route, Schema } from '@orpc/contract' import type { IntersectPick } from '@orpc/shared' import type { BuilderDef } from './builder' import type { Context, MergedCurrentContext, MergedInitialContext } from './context' @@ -18,6 +18,7 @@ export interface BuilderWithMiddlewares< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -38,7 +39,8 @@ export interface BuilderWithMiddlewares< TInputSchema, TOutputSchema, MergedErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -63,7 +65,8 @@ export interface BuilderWithMiddlewares< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -72,9 +75,9 @@ export interface BuilderWithMiddlewares< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - 'meta'( - meta: TMeta, - ): BuilderWithMiddlewares + 'meta'>( + meta: U, + ): BuilderWithMiddlewares, TMetaDef> /** * Sets or updates the route definition. @@ -86,7 +89,7 @@ export interface BuilderWithMiddlewares< */ 'route'( route: Route, - ): ProcedureBuilder + ): ProcedureBuilder /** * Defines the input validation schema. @@ -95,7 +98,7 @@ export interface BuilderWithMiddlewares< */ 'input'( schema: USchema, - ): ProcedureBuilderWithInput + ): ProcedureBuilderWithInput /** * Defines the output validation schema. @@ -104,7 +107,7 @@ export interface BuilderWithMiddlewares< */ 'output'( schema: USchema, - ): ProcedureBuilderWithOutput + ): ProcedureBuilderWithOutput /** * Defines the handler of the procedure. @@ -113,7 +116,7 @@ export interface BuilderWithMiddlewares< */ 'handler'( handler: ProcedureHandler, - ): DecoratedProcedure, TErrorMap, TMeta> + ): DecoratedProcedure, TErrorMap, TMeta, TMetaDef> /** * Prefixes all procedures in the router. @@ -123,7 +126,7 @@ export interface BuilderWithMiddlewares< * * @see {@link https://orpc.dev/docs/openapi/routing#route-prefixes OpenAPI Route Prefixes Docs} */ - 'prefix'(prefix: HTTPPath): RouterBuilder + 'prefix'(prefix: HTTPPath): RouterBuilder /** * Adds tags to all procedures in the router. @@ -131,7 +134,7 @@ export interface BuilderWithMiddlewares< * * @see {@link https://orpc.dev/docs/openapi/openapi-specification#operation-metadata OpenAPI Operation Metadata Docs} */ - 'tag'(...tags: string[]): RouterBuilder + 'tag'(...tags: string[]): RouterBuilder /** * Applies all of the previously defined options to the specified router. @@ -160,6 +163,7 @@ export interface ProcedureBuilder< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -180,7 +184,8 @@ export interface ProcedureBuilder< TInputSchema, TOutputSchema, MergedErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -205,7 +210,8 @@ export interface ProcedureBuilder< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -214,9 +220,9 @@ export interface ProcedureBuilder< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - 'meta'( - meta: TMeta, - ): ProcedureBuilder + 'meta'>( + meta: U, + ): ProcedureBuilder, TMetaDef> /** * Sets or updates the route definition. @@ -228,7 +234,7 @@ export interface ProcedureBuilder< */ 'route'( route: Route, - ): ProcedureBuilder + ): ProcedureBuilder /** * Defines the input validation schema. @@ -237,7 +243,7 @@ export interface ProcedureBuilder< */ 'input'( schema: USchema, - ): ProcedureBuilderWithInput + ): ProcedureBuilderWithInput /** * Defines the output validation schema. @@ -246,7 +252,7 @@ export interface ProcedureBuilder< */ 'output'( schema: USchema, - ): ProcedureBuilderWithOutput + ): ProcedureBuilderWithOutput /** * Defines the handler of the procedure. @@ -255,7 +261,7 @@ export interface ProcedureBuilder< */ 'handler'( handler: ProcedureHandler, - ): DecoratedProcedure, TErrorMap, TMeta> + ): DecoratedProcedure, TErrorMap, TMeta, TMetaDef> } export interface ProcedureBuilderWithInput< @@ -265,6 +271,7 @@ export interface ProcedureBuilderWithInput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -279,7 +286,7 @@ export interface ProcedureBuilderWithInput< */ 'errors'( errors: U, - ): ProcedureBuilderWithInput, TMeta> + ): ProcedureBuilderWithInput, TMeta, TMetaDef> /** * Uses a middleware to modify the context or improve the pipeline. @@ -304,7 +311,8 @@ export interface ProcedureBuilderWithInput< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -331,7 +339,8 @@ export interface ProcedureBuilderWithInput< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -340,9 +349,9 @@ export interface ProcedureBuilderWithInput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - 'meta'( - meta: TMeta, - ): ProcedureBuilderWithInput + 'meta'>( + meta: U, + ): ProcedureBuilderWithInput, TMetaDef> /** * Sets or updates the route definition. @@ -354,7 +363,7 @@ export interface ProcedureBuilderWithInput< */ 'route'( route: Route, - ): ProcedureBuilderWithInput + ): ProcedureBuilderWithInput /** * Defines the output validation schema. @@ -363,7 +372,7 @@ export interface ProcedureBuilderWithInput< */ 'output'( schema: USchema, - ): ProcedureBuilderWithInputOutput + ): ProcedureBuilderWithInputOutput /** * Defines the handler of the procedure. @@ -372,7 +381,7 @@ export interface ProcedureBuilderWithInput< */ 'handler'( handler: ProcedureHandler, UFuncOutput, TErrorMap, TMeta>, - ): DecoratedProcedure, TErrorMap, TMeta> + ): DecoratedProcedure, TErrorMap, TMeta, TMetaDef> } export interface ProcedureBuilderWithOutput< @@ -382,6 +391,7 @@ export interface ProcedureBuilderWithOutput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -402,7 +412,8 @@ export interface ProcedureBuilderWithOutput< TInputSchema, TOutputSchema, MergedErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -427,7 +438,8 @@ export interface ProcedureBuilderWithOutput< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -436,9 +448,9 @@ export interface ProcedureBuilderWithOutput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - 'meta'( - meta: TMeta, - ): ProcedureBuilderWithOutput + 'meta'>( + meta: U, + ): ProcedureBuilderWithOutput, TMetaDef> /** * Sets or updates the route definition. @@ -450,7 +462,7 @@ export interface ProcedureBuilderWithOutput< */ 'route'( route: Route, - ): ProcedureBuilderWithOutput + ): ProcedureBuilderWithOutput /** * Defines the input validation schema. @@ -459,7 +471,7 @@ export interface ProcedureBuilderWithOutput< */ 'input'( schema: USchema, - ): ProcedureBuilderWithInputOutput + ): ProcedureBuilderWithInputOutput /** * Defines the handler of the procedure. @@ -468,7 +480,7 @@ export interface ProcedureBuilderWithOutput< */ 'handler'( handler: ProcedureHandler, TErrorMap, TMeta>, - ): DecoratedProcedure + ): DecoratedProcedure } export interface ProcedureBuilderWithInputOutput< @@ -478,6 +490,7 @@ export interface ProcedureBuilderWithInputOutput< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -492,7 +505,7 @@ export interface ProcedureBuilderWithInputOutput< */ 'errors'( errors: U, - ): ProcedureBuilderWithInputOutput, TMeta> + ): ProcedureBuilderWithInputOutput, TMeta, TMetaDef> /** * Uses a middleware to modify the context or improve the pipeline. @@ -517,7 +530,8 @@ export interface ProcedureBuilderWithInputOutput< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -544,7 +558,8 @@ export interface ProcedureBuilderWithInputOutput< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -553,9 +568,9 @@ export interface ProcedureBuilderWithInputOutput< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - 'meta'( - meta: TMeta, - ): ProcedureBuilderWithInputOutput + 'meta'>( + meta: U, + ): ProcedureBuilderWithInputOutput, TMetaDef> /** * Sets or updates the route definition. @@ -567,7 +582,7 @@ export interface ProcedureBuilderWithInputOutput< */ 'route'( route: Route, - ): ProcedureBuilderWithInputOutput + ): ProcedureBuilderWithInputOutput /** * Defines the handler of the procedure. @@ -576,7 +591,7 @@ export interface ProcedureBuilderWithInputOutput< */ 'handler'( handler: ProcedureHandler, InferSchemaInput, TErrorMap, TMeta>, - ): DecoratedProcedure + ): DecoratedProcedure } export interface RouterBuilder< @@ -584,6 +599,7 @@ export interface RouterBuilder< TCurrentContext extends Context, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -598,7 +614,7 @@ export interface RouterBuilder< */ 'errors'( errors: U, - ): RouterBuilder, TMeta> + ): RouterBuilder, TMeta, TMetaDef> /** * Uses a middleware to modify the context or improve the pipeline. @@ -620,7 +636,8 @@ export interface RouterBuilder< MergedInitialContext, MergedCurrentContext, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -631,7 +648,7 @@ export interface RouterBuilder< * * @see {@link https://orpc.dev/docs/openapi/routing#route-prefixes OpenAPI Route Prefixes Docs} */ - 'prefix'(prefix: HTTPPath): RouterBuilder + 'prefix'(prefix: HTTPPath): RouterBuilder /** * Adds tags to all procedures in the router. @@ -639,7 +656,7 @@ export interface RouterBuilder< * * @see {@link https://orpc.dev/docs/openapi/openapi-specification#operation-metadata OpenAPI Operation Metadata Docs} */ - 'tag'(...tags: string[]): RouterBuilder + 'tag'(...tags: string[]): RouterBuilder /** * Applies all of the previously defined options to the specified router. diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts index b8d8b8eff..fd429680b 100644 --- a/packages/server/src/builder.test-d.ts +++ b/packages/server/src/builder.test-d.ts @@ -1,4 +1,4 @@ -import type { AnySchema, ContractProcedure, ErrorMap, MergedErrorMap, Schema } from '@orpc/contract' +import type { AnySchema, ContractProcedure, ErrorMap, MergedErrorMap, MergedMeta, Schema } from '@orpc/contract' import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' import type { CurrentContext, InitialContext } from '../tests/shared' import type { Builder } from './builder' @@ -275,6 +275,7 @@ describe('Builder', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() @@ -283,6 +284,56 @@ describe('Builder', () => { builder.meta({ log: 'INVALID' }) }) + it('.meta accumulates new keys without re-specifying already-set fields', () => { + expectTypeOf(builder.meta({ log: true }).meta({ mode: 'live' })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + MergedMeta, { readonly mode: 'live' }>, + BaseMeta + > + >() + }) + + it('.meta overrides a previously-narrowed key, matching runtime merge', () => { + expectTypeOf(builder.meta({ log: true }).meta({ log: false })).toEqualTypeOf< + ProcedureBuilder< + InitialContext, + CurrentContext, + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap, + MergedMeta, { readonly log: false }>, + BaseMeta + > + >() + }) + + it('.meta enforces declared schema even after narrowing', () => { + // @ts-expect-error - 'INVALID' not assignable to log: boolean | undefined + builder.meta({ log: true }).meta({ log: 'INVALID' }) + + // @ts-expect-error - 'logg' is not in the declared schema + builder.meta({ logg: true }) + }) + + it('.meta supports helper composition with variable values via $meta', () => { + type TabConfig = { id: string } + const cfgA: TabConfig = { id: 'a' } + const cfgB: TabConfig = { id: 'b' } + + const base = builder.$meta<{ tab?: TabConfig }>({}) + + const result = base.meta({ tab: cfgA }).meta({ tab: cfgB }) + expectTypeOf(result['~orpc'].meta.tab).toEqualTypeOf() + + // @ts-expect-error - foreign key rejected by the declared schema + base.meta({ unknown: 1 }) + }) + it('.route', () => { expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< ProcedureBuilder< diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index 492bec0c0..48d0e50e1 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,5 +1,5 @@ import type { HTTPPath } from '@orpc/client' -import type { AnySchema, ContractProcedureDef, ContractRouter, ErrorMap, MergedErrorMap, Meta, Route, Schema } from '@orpc/contract' +import type { AnySchema, ContractProcedureDef, ContractRouter, ErrorMap, MergedErrorMap, MergedMeta, Meta, Route, Schema } from '@orpc/contract' import type { IntersectPick } from '@orpc/shared' import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInput, ProcedureBuilderWithOutput, RouterBuilder } from './builder-variants' import type { Context, MergedCurrentContext, MergedInitialContext } from './context' @@ -43,6 +43,7 @@ export class Builder< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > { /** * This property holds the defined options. @@ -59,7 +60,7 @@ export class Builder< * @see {@link https://orpc.dev/docs/client/server-side#middlewares-order Middlewares Order Docs} * @see {@link https://orpc.dev/docs/best-practices/dedupe-middleware#configuration Dedupe Middleware Docs} */ - $config(config: BuilderConfig): Builder { + $config(config: BuilderConfig): Builder { const inputValidationCount = this['~orpc'].inputValidationIndex - fallbackConfig('initialInputValidationIndex', this['~orpc'].config.initialInputValidationIndex) const outputValidationCount = this['~orpc'].outputValidationIndex - fallbackConfig('initialOutputValidationIndex', this['~orpc'].config.initialOutputValidationIndex) @@ -77,7 +78,7 @@ export class Builder< * * @see {@link https://orpc.dev/docs/context Context Docs} */ - $context(): Builder, U, TInputSchema, TOutputSchema, TErrorMap, TMeta> { + $context(): Builder, U, TInputSchema, TOutputSchema, TErrorMap, TMeta, TMetaDef> { /** * We need `& Record` to deal with `has no properties in common with type` error */ @@ -97,7 +98,7 @@ export class Builder< */ $meta( initialMeta: U, - ): Builder> { + ): Builder, U & Record> { /** * We need `& Record` to deal with `has no properties in common with type` error */ @@ -117,7 +118,7 @@ export class Builder< */ $route( initialRoute: Route, - ): Builder { + ): Builder { return new Builder({ ...this['~orpc'], route: initialRoute, @@ -131,7 +132,7 @@ export class Builder< */ $input( initialInputSchema?: U, - ): Builder { + ): Builder { return new Builder({ ...this['~orpc'], inputSchema: initialInputSchema, @@ -157,7 +158,7 @@ export class Builder< */ errors( errors: U, - ): Builder, TMeta> { + ): Builder, TMeta, TMetaDef> { return new Builder({ ...this['~orpc'], errorMap: mergeErrorMap(this['~orpc'].errorMap, errors), @@ -186,13 +187,14 @@ export class Builder< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > use( middleware: AnyMiddleware, mapInput?: MapInputMiddleware, - ): BuilderWithMiddlewares { + ): BuilderWithMiddlewares { const mapped = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) : middleware @@ -209,9 +211,9 @@ export class Builder< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): ProcedureBuilder { + meta>( + meta: U, + ): ProcedureBuilder, TMetaDef> { return new Builder({ ...this['~orpc'], meta: mergeMeta(this['~orpc'].meta, meta), @@ -228,7 +230,7 @@ export class Builder< */ route( route: Route, - ): ProcedureBuilder { + ): ProcedureBuilder { return new Builder({ ...this['~orpc'], route: mergeRoute(this['~orpc'].route, route), @@ -242,7 +244,7 @@ export class Builder< */ input( schema: USchema, - ): ProcedureBuilderWithInput { + ): ProcedureBuilderWithInput { return new Builder({ ...this['~orpc'], inputSchema: schema, @@ -257,7 +259,7 @@ export class Builder< */ output( schema: USchema, - ): ProcedureBuilderWithOutput { + ): ProcedureBuilderWithOutput { return new Builder({ ...this['~orpc'], outputSchema: schema, @@ -272,7 +274,7 @@ export class Builder< */ handler( handler: ProcedureHandler, - ): DecoratedProcedure, TErrorMap, TMeta> { + ): DecoratedProcedure, TErrorMap, TMeta, TMetaDef> { return new DecoratedProcedure({ ...this['~orpc'], handler, @@ -289,7 +291,7 @@ export class Builder< */ prefix( prefix: HTTPPath, - ): RouterBuilder { + ): RouterBuilder { return new Builder({ ...this['~orpc'], prefix: mergePrefix(this['~orpc'].prefix, prefix), @@ -302,7 +304,7 @@ export class Builder< * * @see {@link https://orpc.dev/docs/openapi/openapi-specification#operation-metadata OpenAPI Operation Metadata Docs} */ - tag(...tags: string[]): RouterBuilder { + tag(...tags: string[]): RouterBuilder { return new Builder({ ...this['~orpc'], tags: mergeTags(this['~orpc'].tags, tags), diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts index 5fa6a318f..7391640a4 100644 --- a/packages/server/src/procedure-decorated.test-d.ts +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -1,5 +1,5 @@ import type { Client } from '@orpc/client' -import type { AnySchema, ErrorFromErrorMap, ErrorMap, MergedErrorMap } from '@orpc/contract' +import type { AnySchema, ErrorFromErrorMap, ErrorMap, MergedErrorMap, MergedMeta } from '@orpc/contract' import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared' import type { CurrentContext, InitialContext } from '../tests/shared' import type { Context } from './context' @@ -65,6 +65,7 @@ describe('DecoratedProcedure', () => { typeof inputSchema, typeof outputSchema, typeof baseErrorMap, + MergedMeta, BaseMeta > >() diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 2140c9824..07cd2fa2b 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -5,6 +5,7 @@ import type { InferSchemaInput, InferSchemaOutput, MergedErrorMap, + MergedMeta, Meta, Route, } from '@orpc/contract' @@ -28,6 +29,7 @@ export class DecoratedProcedure< TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, TMeta extends Meta, + TMetaDef extends Meta = TMeta, > extends Procedure { /** * Adds type-safe custom errors. @@ -43,7 +45,8 @@ export class DecoratedProcedure< TInputSchema, TOutputSchema, MergedErrorMap, - TMeta + TMeta, + TMetaDef > { return new DecoratedProcedure({ ...this['~orpc'], @@ -57,9 +60,9 @@ export class DecoratedProcedure< * * @see {@link https://orpc.dev/docs/metadata Metadata Docs} */ - meta( - meta: TMeta, - ): DecoratedProcedure { + meta>( + meta: U, + ): DecoratedProcedure, TMetaDef> { return new DecoratedProcedure({ ...this['~orpc'], meta: mergeMeta(this['~orpc'].meta, meta), @@ -76,7 +79,7 @@ export class DecoratedProcedure< */ route( route: Route, - ): DecoratedProcedure { + ): DecoratedProcedure { return new DecoratedProcedure({ ...this['~orpc'], route: mergeRoute(this['~orpc'].route, route), @@ -106,7 +109,8 @@ export class DecoratedProcedure< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > /** @@ -133,10 +137,11 @@ export class DecoratedProcedure< TInputSchema, TOutputSchema, TErrorMap, - TMeta + TMeta, + TMetaDef > - use(middleware: AnyMiddleware, mapInput?: MapInputMiddleware): DecoratedProcedure { + use(middleware: AnyMiddleware, mapInput?: MapInputMiddleware): DecoratedProcedure { const mapped = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) : middleware @@ -162,7 +167,7 @@ export class DecoratedProcedure< TClientContext > > - ): DecoratedProcedure + ): DecoratedProcedure & ProcedureClient { const client: ProcedureClient = createProcedureClient(this, ...rest) @@ -192,7 +197,7 @@ export class DecoratedProcedure< > > ): - & DecoratedProcedure + & DecoratedProcedure & ProcedureActionableClient { const action: ProcedureActionableClient = createActionableClient(createProcedureClient(this, ...rest))