@trpc/server
Version:
692 lines (656 loc) • 17.7 kB
text/typescript
import type { inferObservableValue, Observable } from '../observable';
import { getTRPCErrorFromUnknown, TRPCError } from './error/TRPCError';
import type {
AnyMiddlewareFunction,
MiddlewareBuilder,
MiddlewareFunction,
MiddlewareResult,
} from './middleware';
import {
createInputMiddleware,
createOutputMiddleware,
middlewareMarker,
} from './middleware';
import type { inferParser, Parser } from './parser';
import { getParseFn } from './parser';
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
LegacyObservableSubscriptionProcedure,
MutationProcedure,
ProcedureType,
QueryProcedure,
SubscriptionProcedure,
} from './procedure';
import type { inferTrackedOutput } from './stream/tracked';
import type {
GetRawInputFn,
MaybePromise,
Overwrite,
Simplify,
TypeError,
} from './types';
import type { UnsetMarker } from './utils';
import { mergeWithoutOverrides } from './utils';
type IntersectIfDefined<TType, TWith> = TType extends UnsetMarker
? TWith
: TWith extends UnsetMarker
? TType
: Simplify<TType & TWith>;
type DefaultValue<TValue, TFallback> = TValue extends UnsetMarker
? TFallback
: TValue;
type inferAsyncIterable<TOutput> =
TOutput extends AsyncIterable<infer $Yield, infer $Return, infer $Next>
? {
yield: $Yield;
return: $Return;
next: $Next;
}
: never;
type inferSubscriptionOutput<TOutput> =
TOutput extends AsyncIterable<any>
? AsyncIterable<
inferTrackedOutput<inferAsyncIterable<TOutput>['yield']>,
inferAsyncIterable<TOutput>['return'],
inferAsyncIterable<TOutput>['next']
>
: TypeError<'Subscription output could not be inferred'>;
export type CallerOverride<TContext> = (opts: {
args: unknown[];
invoke: (opts: ProcedureCallOptions<TContext>) => Promise<unknown>;
_def: AnyProcedure['_def'];
}) => Promise<unknown>;
type ProcedureBuilderDef<TMeta> = {
procedure: true;
inputs: Parser[];
output?: Parser;
meta?: TMeta;
resolver?: ProcedureBuilderResolver;
middlewares: AnyMiddlewareFunction[];
/**
* @deprecated use `type` instead
*/
mutation?: boolean;
/**
* @deprecated use `type` instead
*/
query?: boolean;
/**
* @deprecated use `type` instead
*/
subscription?: boolean;
type?: ProcedureType;
caller?: CallerOverride<unknown>;
};
type AnyProcedureBuilderDef = ProcedureBuilderDef<any>;
/**
* Procedure resolver options (what the `.query()`, `.mutation()`, and `.subscription()` functions receive)
* @internal
*/
export interface ProcedureResolverOptions<
TContext,
_TMeta,
TContextOverridesIn,
TInputOut,
> {
ctx: Simplify<Overwrite<TContext, TContextOverridesIn>>;
input: TInputOut extends UnsetMarker ? undefined : TInputOut;
/**
* The AbortSignal of the request
*/
signal: AbortSignal | undefined;
}
/**
* A procedure resolver
*/
type ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputParserIn,
$Output,
> = (
opts: ProcedureResolverOptions<TContext, TMeta, TContextOverrides, TInputOut>,
) => MaybePromise<
// If an output parser is defined, we need to return what the parser expects, otherwise we return the inferred type
DefaultValue<TOutputParserIn, $Output>
>;
type AnyResolver = ProcedureResolver<any, any, any, any, any, any>;
export type AnyProcedureBuilder = ProcedureBuilder<
any,
any,
any,
any,
any,
any,
any,
any
>;
/**
* Infer the context type from a procedure builder
* Useful to create common helper functions for different procedures
*/
export type inferProcedureBuilderResolverOptions<
TProcedureBuilder extends AnyProcedureBuilder,
> =
TProcedureBuilder extends ProcedureBuilder<
infer TContext,
infer TMeta,
infer TContextOverrides,
infer _TInputIn,
infer TInputOut,
infer _TOutputIn,
infer _TOutputOut,
infer _TCaller
>
? ProcedureResolverOptions<
TContext,
TMeta,
TContextOverrides,
TInputOut extends UnsetMarker
? // if input is not set, we don't want to infer it as `undefined` since a procedure further down the chain might have set an input
unknown
: TInputOut extends object
? Simplify<
TInputOut & {
/**
* Extra input params might have been added by a `.input()` further down the chain
*/
[keyAddedByInputCallFurtherDown: string]: unknown;
}
>
: TInputOut
>
: never;
export interface ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller extends boolean,
> {
/**
* Add an input parser to the procedure.
* @see https://trpc.io/docs/v11/server/validators
*/
input<$Parser extends Parser>(
schema: TInputOut extends UnsetMarker
? $Parser
: inferParser<$Parser>['out'] extends Record<string, unknown> | undefined
? TInputOut extends Record<string, unknown> | undefined
? undefined extends inferParser<$Parser>['out'] // if current is optional the previous must be too
? undefined extends TInputOut
? $Parser
: TypeError<'Cannot chain an optional parser to a required parser'>
: $Parser
: TypeError<'All input parsers did not resolve to an object'>
: TypeError<'All input parsers did not resolve to an object'>,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
IntersectIfDefined<TInputIn, inferParser<$Parser>['in']>,
IntersectIfDefined<TInputOut, inferParser<$Parser>['out']>,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* Add an output parser to the procedure.
* @see https://trpc.io/docs/v11/server/validators
*/
output<$Parser extends Parser>(
schema: $Parser,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
IntersectIfDefined<TOutputIn, inferParser<$Parser>['in']>,
IntersectIfDefined<TOutputOut, inferParser<$Parser>['out']>,
TCaller
>;
/**
* Add a meta data to the procedure.
* @see https://trpc.io/docs/v11/server/metadata
*/
meta(
meta: TMeta,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* Add a middleware to the procedure.
* @see https://trpc.io/docs/v11/server/middlewares
*/
use<$ContextOverridesOut>(
fn:
| MiddlewareBuilder<
Overwrite<TContext, TContextOverrides>,
TMeta,
$ContextOverridesOut,
TInputOut
>
| MiddlewareFunction<
TContext,
TMeta,
TContextOverrides,
$ContextOverridesOut,
TInputOut
>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverridesOut>,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>;
/**
* @deprecated use {@link concat} instead
*/
unstable_concat<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
>(
builder: Overwrite<TContext, TContextOverrides> extends $Context
? TMeta extends $Meta
? ProcedureBuilder<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
TCaller
>
: TypeError<'Meta mismatch'>
: TypeError<'Context mismatch'>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverrides>,
IntersectIfDefined<TInputIn, $InputIn>,
IntersectIfDefined<TInputOut, $InputOut>,
IntersectIfDefined<TOutputIn, $OutputIn>,
IntersectIfDefined<TOutputOut, $OutputOut>,
TCaller
>;
/**
* Combine two procedure builders
*/
concat<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
>(
builder: Overwrite<TContext, TContextOverrides> extends $Context
? TMeta extends $Meta
? ProcedureBuilder<
$Context,
$Meta,
$ContextOverrides,
$InputIn,
$InputOut,
$OutputIn,
$OutputOut,
TCaller
>
: TypeError<'Meta mismatch'>
: TypeError<'Context mismatch'>,
): ProcedureBuilder<
TContext,
TMeta,
Overwrite<TContextOverrides, $ContextOverrides>,
IntersectIfDefined<TInputIn, $InputIn>,
IntersectIfDefined<TInputOut, $InputOut>,
IntersectIfDefined<TOutputIn, $OutputIn>,
IntersectIfDefined<TOutputOut, $OutputOut>,
TCaller
>;
/**
* Query procedure
* @see https://trpc.io/docs/v11/concepts#vocabulary
*/
query<$Output>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? (
input: DefaultValue<TInputIn, void>,
) => Promise<DefaultValue<TOutputOut, $Output>>
: QueryProcedure<{
input: DefaultValue<TInputIn, void>;
output: DefaultValue<TOutputOut, $Output>;
meta: TMeta;
}>;
/**
* Mutation procedure
* @see https://trpc.io/docs/v11/concepts#vocabulary
*/
mutation<$Output>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? (
input: DefaultValue<TInputIn, void>,
) => Promise<DefaultValue<TOutputOut, $Output>>
: MutationProcedure<{
input: DefaultValue<TInputIn, void>;
output: DefaultValue<TOutputOut, $Output>;
meta: TMeta;
}>;
/**
* Subscription procedure
* @see https://trpc.io/docs/v11/server/subscriptions
*/
subscription<$Output extends AsyncIterable<any, void, any>>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? TypeError<'Not implemented'>
: SubscriptionProcedure<{
input: DefaultValue<TInputIn, void>;
output: inferSubscriptionOutput<DefaultValue<TOutputOut, $Output>>;
meta: TMeta;
}>;
/**
* @deprecated Using subscriptions with an observable is deprecated. Use an async generator instead.
* This feature will be removed in v12 of tRPC.
* @see https://trpc.io/docs/v11/server/subscriptions
*/
subscription<$Output extends Observable<any, any>>(
resolver: ProcedureResolver<
TContext,
TMeta,
TContextOverrides,
TInputOut,
TOutputIn,
$Output
>,
): TCaller extends true
? TypeError<'Not implemented'>
: LegacyObservableSubscriptionProcedure<{
input: DefaultValue<TInputIn, void>;
output: inferObservableValue<DefaultValue<TOutputOut, $Output>>;
meta: TMeta;
}>;
/**
* Overrides the way a procedure is invoked
* Do not use this unless you know what you're doing - this is an experimental API
*/
experimental_caller(
caller: CallerOverride<TContext>,
): ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
true
>;
/**
* @internal
*/
_def: ProcedureBuilderDef<TMeta>;
}
type ProcedureBuilderResolver = (
opts: ProcedureResolverOptions<any, any, any, any>,
) => Promise<unknown>;
function createNewBuilder(
def1: AnyProcedureBuilderDef,
def2: Partial<AnyProcedureBuilderDef>,
): AnyProcedureBuilder {
const { middlewares = [], inputs, meta, ...rest } = def2;
// TODO: maybe have a fn here to warn about calls
return createBuilder({
...mergeWithoutOverrides(def1, rest),
inputs: [...def1.inputs, ...(inputs ?? [])],
middlewares: [...def1.middlewares, ...middlewares],
meta: def1.meta && meta ? { ...def1.meta, ...meta } : (meta ?? def1.meta),
});
}
export function createBuilder<TContext, TMeta>(
initDef: Partial<AnyProcedureBuilderDef> = {},
): ProcedureBuilder<
TContext,
TMeta,
object,
UnsetMarker,
UnsetMarker,
UnsetMarker,
UnsetMarker,
false
> {
const _def: AnyProcedureBuilderDef = {
procedure: true,
inputs: [],
middlewares: [],
...initDef,
};
const builder: AnyProcedureBuilder = {
_def,
input(input) {
const parser = getParseFn(input as Parser);
return createNewBuilder(_def, {
inputs: [input as Parser],
middlewares: [createInputMiddleware(parser)],
});
},
output(output: Parser) {
const parser = getParseFn(output);
return createNewBuilder(_def, {
output,
middlewares: [createOutputMiddleware(parser)],
});
},
meta(meta) {
return createNewBuilder(_def, {
meta,
});
},
use(middlewareBuilderOrFn) {
// Distinguish between a middleware builder and a middleware function
const middlewares =
'_middlewares' in middlewareBuilderOrFn
? middlewareBuilderOrFn._middlewares
: [middlewareBuilderOrFn];
return createNewBuilder(_def, {
middlewares: middlewares,
});
},
unstable_concat(builder) {
return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
},
concat(builder) {
return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
},
query(resolver) {
return createResolver(
{ ..._def, type: 'query' },
resolver,
) as AnyQueryProcedure;
},
mutation(resolver) {
return createResolver(
{ ..._def, type: 'mutation' },
resolver,
) as AnyMutationProcedure;
},
subscription(resolver: ProcedureResolver<any, any, any, any, any, any>) {
return createResolver({ ..._def, type: 'subscription' }, resolver) as any;
},
experimental_caller(caller) {
return createNewBuilder(_def, {
caller,
}) as any;
},
};
return builder;
}
function createResolver(
_defIn: AnyProcedureBuilderDef & { type: ProcedureType },
resolver: AnyResolver,
) {
const finalBuilder = createNewBuilder(_defIn, {
resolver,
middlewares: [
async function resolveMiddleware(opts) {
const data = await resolver(opts);
return {
marker: middlewareMarker,
ok: true,
data,
ctx: opts.ctx,
} as const;
},
],
});
const _def: AnyProcedure['_def'] = {
...finalBuilder._def,
type: _defIn.type,
experimental_caller: Boolean(finalBuilder._def.caller),
meta: finalBuilder._def.meta,
$types: null as any,
};
const invoke = createProcedureCaller(finalBuilder._def);
const callerOverride = finalBuilder._def.caller;
if (!callerOverride) {
return invoke;
}
const callerWrapper = async (...args: unknown[]) => {
return await callerOverride({
args,
invoke,
_def: _def,
});
};
callerWrapper._def = _def;
return callerWrapper;
}
/**
* @internal
*/
export interface ProcedureCallOptions<TContext> {
ctx: TContext;
getRawInput: GetRawInputFn;
input?: unknown;
path: string;
type: ProcedureType;
signal: AbortSignal | undefined;
}
const codeblock = `
This is a client-only function.
If you want to call this function on the server, see https://trpc.io/docs/v11/server/server-side-calls
`.trim();
// run the middlewares recursively with the resolver as the last one
async function callRecursive(
index: number,
_def: AnyProcedureBuilderDef,
opts: ProcedureCallOptions<any>,
): Promise<MiddlewareResult<any>> {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const middleware = _def.middlewares[index]!;
const result = await middleware({
...opts,
meta: _def.meta,
input: opts.input,
next(_nextOpts?: any) {
const nextOpts = _nextOpts as
| {
ctx?: Record<string, unknown>;
input?: unknown;
getRawInput?: GetRawInputFn;
}
| undefined;
return callRecursive(index + 1, _def, {
...opts,
ctx: nextOpts?.ctx ? { ...opts.ctx, ...nextOpts.ctx } : opts.ctx,
input: nextOpts && 'input' in nextOpts ? nextOpts.input : opts.input,
getRawInput: nextOpts?.getRawInput ?? opts.getRawInput,
});
},
});
return result;
} catch (cause) {
return {
ok: false,
error: getTRPCErrorFromUnknown(cause),
marker: middlewareMarker,
};
}
}
function createProcedureCaller(_def: AnyProcedureBuilderDef): AnyProcedure {
async function procedure(opts: ProcedureCallOptions<unknown>) {
// is direct server-side call
if (!opts || !('getRawInput' in opts)) {
throw new Error(codeblock);
}
// there's always at least one "next" since we wrap this.resolver in a middleware
const result = await callRecursive(0, _def, opts);
if (!result) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message:
'No result from middlewares - did you forget to `return next()`?',
});
}
if (!result.ok) {
// re-throw original error
throw result.error;
}
return result.data;
}
procedure._def = _def;
procedure.procedure = true;
procedure.meta = _def.meta;
// FIXME typecast shouldn't be needed - fixittt
return procedure as unknown as AnyProcedure;
}