UNPKG

every-plugin

Version:
249 lines (218 loc) 8.23 kB
import type { AnyContractRouter, AnySchema, InferSchemaOutput } from "@orpc/contract"; import { ORPCError } from "@orpc/contract"; import type { Implementer, Router } from "@orpc/server"; import { implement, onError } from "@orpc/server"; import { Context, Effect, type Context as EffectContext, type Layer, 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>; }; /** * Tools provided to plugin initialize/services for building long-lived scoped resources. */ type ServiceOf<T> = T extends EffectContext.Tag<any, infer S> ? S : never; export type PluginServicesTools = { buildService: <T>(tag: T, layer: Layer.Layer<any, any, any>) => Effect.Effect<ServiceOf<T>>; }; export class PluginIdTag extends Context.Tag("PluginId")<PluginIdTag, string>() {} type PluginDefinition< V extends AnySchema, S extends AnySchema, TContract extends AnyContractRouter, TRequestContext extends AnySchema | undefined, TDeps extends Record<string, any>, P extends Record<string, unknown>, > = { variables: V; secrets: S; contract: TContract; context?: TRequestContext; /** * Initialize the plugin and build dependencies. * A third argument `tools` is provided for building long-lived scoped resources * (e.g. database pools) via `tools.buildService(tag, layer)`. */ initialize?: ( config: PluginInitializeInput<V, S>, plugins: P, tools: PluginServicesTools, ) => 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 Record<string, any> = 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 Record<string, any> = Record<never, never>, > { readonly id: string; readonly contract: TContract; readonly configSchema: PluginConfigFor<TVariables, TSecrets, TRequestContext>; initialize( config: PluginInitializeInput<TVariables, TSecrets>, plugins: Record<string, unknown>, tools: PluginServicesTools, ): 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 Record<string, any> = 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 Record<string, any> = 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> = {}, tools: PluginServicesTools, ): Effect.Effect<TDeps, unknown, Scope.Scope> { const init = config.initialize ?? (() => Effect.succeed({} as TDeps)); return init(pluginConfig, plugins as P, tools).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; } const formatted = formatORPCError(error); if (formatted) console.error(formatted); 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 Record<string, any> = 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 Record<string, any> = Record<never, never>, >( config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>, ) => createPlugin<V, S, TContract, TRequestContext, TDeps, P>(config as any); } createPlugin.withPlugins = withPlugins;