UNPKG

@orpc/contract

Version:

<div align="center"> <image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" /> </div>

356 lines (343 loc) • 10.5 kB
import { isORPCErrorStatus, mapEventIterator, ORPCError } from '@orpc/client'; export { ORPCError } from '@orpc/client'; import { isAsyncIteratorObject, get, isTypescriptObject, isPropertyKey } from '@orpc/shared'; class ValidationError extends Error { issues; constructor(options) { super(options.message, options); this.issues = options.issues; } } function mergeErrorMap(errorMap1, errorMap2) { return { ...errorMap1, ...errorMap2 }; } function mergeMeta(meta1, meta2) { return { ...meta1, ...meta2 }; } class ContractProcedure { /** * This property holds the defined options for the contract procedure. */ "~orpc"; constructor(def) { if (def.route?.successStatus && isORPCErrorStatus(def.route.successStatus)) { throw new Error("[ContractProcedure] Invalid successStatus."); } if (Object.values(def.errorMap).some((val) => val && val.status && !isORPCErrorStatus(val.status))) { throw new Error("[ContractProcedure] Invalid error status code."); } this["~orpc"] = def; } } function isContractProcedure(item) { if (item instanceof ContractProcedure) { return true; } return (typeof item === "object" || typeof item === "function") && item !== null && "~orpc" in item && typeof item["~orpc"] === "object" && item["~orpc"] !== null && "errorMap" in item["~orpc"] && "route" in item["~orpc"] && "meta" in item["~orpc"]; } function mergeRoute(a, b) { return { ...a, ...b }; } function prefixRoute(route, prefix) { if (!route.path) { return route; } return { ...route, path: `${prefix}${route.path}` }; } function unshiftTagRoute(route, tags) { return { ...route, tags: [...tags, ...route.tags ?? []] }; } function mergePrefix(a, b) { return a ? `${a}${b}` : b; } function mergeTags(a, b) { return a ? [...a, ...b] : b; } function enhanceRoute(route, options) { let router = route; if (options.prefix) { router = prefixRoute(router, options.prefix); } if (options.tags?.length) { router = unshiftTagRoute(router, options.tags); } return router; } function getContractRouter(router, path) { let current = router; for (let i = 0; i < path.length; i++) { const segment = path[i]; if (!current) { return void 0; } if (isContractProcedure(current)) { return void 0; } current = current[segment]; } return current; } function enhanceContractRouter(router, options) { if (isContractProcedure(router)) { const enhanced2 = new ContractProcedure({ ...router["~orpc"], errorMap: mergeErrorMap(options.errorMap, router["~orpc"].errorMap), route: enhanceRoute(router["~orpc"].route, options) }); return enhanced2; } const enhanced = {}; for (const key in router) { enhanced[key] = enhanceContractRouter(router[key], options); } return enhanced; } function minifyContractRouter(router) { if (isContractProcedure(router)) { const procedure = { "~orpc": { errorMap: {}, meta: router["~orpc"].meta, route: router["~orpc"].route } }; return procedure; } const json = {}; for (const key in router) { json[key] = minifyContractRouter(router[key]); } return json; } class ContractBuilder extends ContractProcedure { constructor(def) { super(def); this["~orpc"].prefix = def.prefix; this["~orpc"].tags = def.tags; } /** * Sets or overrides the initial meta. * * @see {@link https://orpc.unnoq.com/docs/metadata Metadata Docs} */ $meta(initialMeta) { return new ContractBuilder({ ...this["~orpc"], meta: initialMeta }); } /** * Sets or overrides the initial route. * This option is typically relevant when integrating with OpenAPI. * * @see {@link https://orpc.unnoq.com/docs/openapi/routing OpenAPI Routing Docs} * @see {@link https://orpc.unnoq.com/docs/openapi/input-output-structure OpenAPI Input/Output Structure Docs} */ $route(initialRoute) { return new ContractBuilder({ ...this["~orpc"], route: initialRoute }); } /** * Adds type-safe custom errors to the contract. * The provided errors are spared-merged with any existing errors in the contract. * * @see {@link https://orpc.unnoq.com/docs/error-handling#type%E2%80%90safe-error-handling Type-Safe Error Handling Docs} */ errors(errors) { return new ContractBuilder({ ...this["~orpc"], errorMap: mergeErrorMap(this["~orpc"].errorMap, errors) }); } /** * Sets or updates the metadata for the contract. * The provided metadata is spared-merged with any existing metadata in the contract. * * @see {@link https://orpc.unnoq.com/docs/metadata Metadata Docs} */ meta(meta) { return new ContractBuilder({ ...this["~orpc"], meta: mergeMeta(this["~orpc"].meta, meta) }); } /** * Sets or updates the route definition for the contract. * The provided route is spared-merged with any existing route in the contract. * This option is typically relevant when integrating with OpenAPI. * * @see {@link https://orpc.unnoq.com/docs/openapi/routing OpenAPI Routing Docs} * @see {@link https://orpc.unnoq.com/docs/openapi/input-output-structure OpenAPI Input/Output Structure Docs} */ route(route) { return new ContractBuilder({ ...this["~orpc"], route: mergeRoute(this["~orpc"].route, route) }); } /** * Defines the input validation schema for the contract. * * @see {@link https://orpc.unnoq.com/docs/procedure#input-output-validation Input Validation Docs} */ input(schema) { return new ContractBuilder({ ...this["~orpc"], inputSchema: schema }); } /** * Defines the output validation schema for the contract. * * @see {@link https://orpc.unnoq.com/docs/procedure#input-output-validation Output Validation Docs} */ output(schema) { return new ContractBuilder({ ...this["~orpc"], outputSchema: schema }); } /** * Prefixes all procedures in the contract router. * The provided prefix is post-appended to any existing router prefix. * * @note This option does not affect procedures that do not define a path in their route definition. * * @see {@link https://orpc.unnoq.com/docs/openapi/routing#route-prefixes OpenAPI Route Prefixes Docs} */ prefix(prefix) { return new ContractBuilder({ ...this["~orpc"], prefix: mergePrefix(this["~orpc"].prefix, prefix) }); } /** * Adds tags to all procedures in the contract router. * This helpful when you want to group procedures together in the OpenAPI specification. * * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification#operation-metadata OpenAPI Operation Metadata Docs} */ tag(...tags) { return new ContractBuilder({ ...this["~orpc"], tags: mergeTags(this["~orpc"].tags, tags) }); } /** * Applies all of the previously defined options to the specified contract router. * * @see {@link https://orpc.unnoq.com/docs/router#extending-router Extending Router Docs} */ router(router) { return enhanceContractRouter(router, this["~orpc"]); } } const oc = new ContractBuilder({ errorMap: {}, route: {}, meta: {} }); const DEFAULT_CONFIG = { defaultMethod: "POST", defaultSuccessStatus: 200, defaultSuccessDescription: "OK", defaultInputStructure: "compact", defaultOutputStructure: "compact" }; function fallbackContractConfig(key, value) { if (value === void 0) { return DEFAULT_CONFIG[key]; } return value; } const EVENT_ITERATOR_DETAILS_SYMBOL = Symbol("ORPC_EVENT_ITERATOR_DETAILS"); function eventIterator(yields, returns) { return { "~standard": { [EVENT_ITERATOR_DETAILS_SYMBOL]: { yields, returns }, vendor: "orpc", version: 1, validate(iterator) { if (!isAsyncIteratorObject(iterator)) { return { issues: [{ message: "Expect event iterator", path: [] }] }; } const mapped = mapEventIterator(iterator, { async value(value, done) { const schema = done ? returns : yields; if (!schema) { return value; } const result = await schema["~standard"].validate(value); if (result.issues) { throw new ORPCError("EVENT_ITERATOR_VALIDATION_FAILED", { message: "Event iterator validation failed", cause: new ValidationError({ issues: result.issues, message: "Event iterator validation failed" }) }); } return result.value; }, error: async (error) => error }); return { value: mapped }; } } }; } function getEventIteratorSchemaDetails(schema) { if (schema === void 0) { return void 0; } return schema["~standard"][EVENT_ITERATOR_DETAILS_SYMBOL]; } function inferRPCMethodFromContractRouter(contract) { return (_, path) => { const procedure = get(contract, path); if (!isContractProcedure(procedure)) { throw new Error( `[inferRPCMethodFromContractRouter] No valid procedure found at path "${path.join(".")}". This may happen when the contract router is not properly configured.` ); } const method = fallbackContractConfig("defaultMethod", procedure["~orpc"].route.method); return method === "HEAD" ? "GET" : method; }; } function type(...[map]) { return { "~standard": { vendor: "custom", version: 1, async validate(value) { if (map) { return { value: await map(value) }; } return { value }; } } }; } function isSchemaIssue(issue) { if (!isTypescriptObject(issue) || typeof issue.message !== "string") { return false; } if (issue.path !== void 0) { if (!Array.isArray(issue.path)) { return false; } if (!issue.path.every((segment) => isPropertyKey(segment) || isTypescriptObject(segment) && isPropertyKey(segment.key))) { return false; } } return true; } export { ContractBuilder, ContractProcedure, ValidationError, enhanceContractRouter, enhanceRoute, eventIterator, fallbackContractConfig, getContractRouter, getEventIteratorSchemaDetails, inferRPCMethodFromContractRouter, isContractProcedure, isSchemaIssue, mergeErrorMap, mergeMeta, mergePrefix, mergeRoute, mergeTags, minifyContractRouter, oc, prefixRoute, type, unshiftTagRoute };