every-plugin
Version:
229 lines (201 loc) • 7.33 kB
text/typescript
import type { AnyContractRouter, AnySchema, InferSchemaOutput } from "@orpc/contract";
import { ORPCError } from "@orpc/contract";
import type { Context, Implementer, Router } from "@orpc/server";
import { implement, onError } from "@orpc/server";
import { Effect, type Scope } from "effect";
import { extractFromFiberFailure, formatORPCError } from "./runtime/errors";
type ContextOutput<T> = T extends AnySchema ? InferSchemaOutput<T> : Record<string, never>;
export type PluginConfigFor<
V extends AnySchema,
S extends AnySchema,
TRequestContext extends AnySchema | undefined,
> = {
variables: V;
secrets: S;
context: TRequestContext;
};
type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
variables: InferSchemaOutput<V>;
secrets: InferSchemaOutput<S>;
};
type PluginDefinition<
V extends AnySchema,
S extends AnySchema,
TContract extends AnyContractRouter,
TRequestContext extends AnySchema | undefined,
TDeps extends Context,
P extends Record<string, unknown>,
> = {
variables: V;
secrets: S;
contract: TContract;
context?: TRequestContext;
initialize?: (
config: PluginInitializeInput<V, S>,
plugins: P,
) => Effect.Effect<TDeps, Error, Scope.Scope>;
createRouter: (
deps: TDeps,
builder: Implementer<TContract, ContextOutput<TRequestContext>, ContextOutput<TRequestContext>>,
) => Router<TContract, any>;
shutdown?: (deps: TDeps) => Effect.Effect<void, Error, never>;
};
/**
* Loaded plugin with static binding property
*/
export interface LoadedPluginWithBinding<
TContract extends AnyContractRouter,
TVariables extends AnySchema,
TSecrets extends AnySchema,
TRequestContext extends AnySchema | undefined,
TDeps extends Context = Record<never, never>,
> {
new (): Plugin<TContract, TVariables, TSecrets, TRequestContext, TDeps>;
binding: {
contract: TContract;
variables: TVariables;
secrets: TSecrets;
context: TRequestContext;
};
}
/**
* Plugin interface
*/
export interface Plugin<
TContract extends AnyContractRouter,
TVariables extends AnySchema,
TSecrets extends AnySchema,
TRequestContext extends AnySchema | undefined,
TDeps extends Context = Record<never, never>,
> {
readonly id: string;
readonly contract: TContract;
readonly configSchema: PluginConfigFor<TVariables, TSecrets, TRequestContext>;
initialize(
config: PluginInitializeInput<TVariables, TSecrets>,
plugins: Record<string, unknown>,
): Effect.Effect<TDeps, unknown, Scope.Scope>;
shutdown(): Effect.Effect<void, never>;
/**
* Creates the strongly-typed oRPC router for this plugin.
* The router's procedure types are inferred directly from the contract.
* @param deps The initialized plugin dependencies
* @returns A router with procedures matching the plugin's contract
*/
createRouter(deps: TDeps): Router<TContract, any>;
}
export interface CreatePluginFn {
<
V extends AnySchema,
S extends AnySchema,
TContract extends AnyContractRouter,
TRequestContext extends AnySchema | undefined = undefined,
TDeps extends Context = Record<never, never>,
P extends Record<string, unknown> = Record<string, never>,
>(
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
): LoadedPluginWithBinding<TContract, V, S, TRequestContext, TDeps>;
withPlugins: <P extends Record<string, unknown>>() => CreatePluginWithPlugins<P>;
}
export const createPlugin: CreatePluginFn = function createPlugin<
V extends AnySchema,
S extends AnySchema,
TContract extends AnyContractRouter,
TRequestContext extends AnySchema | undefined = undefined,
TDeps extends Context = Record<never, never>,
P extends Record<string, unknown> = Record<string, never>,
>(config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>) {
const configSchema: PluginConfigFor<V, S, TRequestContext> = {
variables: config.variables,
secrets: config.secrets,
context: config.context as TRequestContext,
};
class CreatedPlugin implements Plugin<TContract, V, S, TRequestContext, TDeps> {
/** set during instantiation - registry key */
id!: string;
readonly contract = config.contract;
readonly configSchema = configSchema;
private _deps: TDeps | null = null;
initialize(
pluginConfig: PluginInitializeInput<V, S>,
plugins: Record<string, unknown> = {},
): Effect.Effect<TDeps, unknown, Scope.Scope> {
const init = config.initialize ?? (() => Effect.succeed({} as TDeps));
return init(pluginConfig, plugins as P).pipe(
Effect.tap((deps) =>
Effect.sync(() => {
this._deps = deps;
}),
),
Effect.map(() => this._deps as TDeps),
Effect.mapError((error) => error as unknown),
);
}
shutdown(): Effect.Effect<void, never> {
const self = this;
return Effect.gen(function* () {
if (config.shutdown && self._deps) {
yield* config
.shutdown(self._deps)
.pipe(
Effect.catchAll((error) =>
Effect.logWarning(`Plugin shutdown hook failed for ${self.id}`, error),
),
);
}
self._deps = null;
});
}
createRouter(deps: TDeps): Router<TContract, any> {
const base = implement(config.contract).$context<ContextOutput<TRequestContext>>();
const errorMiddleware = onError((error: unknown) => {
const unwrapped = extractFromFiberFailure(error);
if (unwrapped !== error && unwrapped instanceof ORPCError) {
throw unwrapped;
}
formatORPCError(error);
throw error;
}) as any;
const builder = (base as any).use(errorMiddleware);
const router = config.createRouter(deps, builder as any);
return router as Router<TContract, any>;
}
}
const PluginConstructor = CreatedPlugin as unknown as {
new (): Plugin<TContract, V, S, TRequestContext, TDeps>;
binding: {
contract: TContract;
variables: V;
secrets: S;
context: TRequestContext;
};
};
PluginConstructor.binding = {
contract: config.contract,
variables: config.variables,
secrets: config.secrets,
context: config.context as TRequestContext,
};
return PluginConstructor as LoadedPluginWithBinding<TContract, V, S, TRequestContext, TDeps>;
};
export type CreatePluginWithPlugins<P extends Record<string, unknown>> = <
V extends AnySchema,
S extends AnySchema,
TContract extends AnyContractRouter,
TRequestContext extends AnySchema | undefined = undefined,
TDeps extends Context = Record<never, never>,
>(
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
) => LoadedPluginWithBinding<TContract, V, S, TRequestContext, TDeps>;
export function withPlugins<P extends Record<string, unknown>>(): CreatePluginWithPlugins<P> {
return <
V extends AnySchema,
S extends AnySchema,
TContract extends AnyContractRouter,
TRequestContext extends AnySchema | undefined = undefined,
TDeps extends Context = Record<never, never>,
>(
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
) => createPlugin<V, S, TContract, TRequestContext, TDeps, P>(config as any);
}
createPlugin.withPlugins = withPlugins;