UNPKG

@orpc/openapi-client

Version:

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

303 lines (296 loc) • 11.5 kB
import { toStandardHeaders, toHttpPath, getMalformedResponseErrorCode, StandardLink } from '@orpc/client/standard'; import { S as StandardBracketNotationSerializer } from './openapi-client.t9fCAe3x.mjs'; import { isObject, value, get, isAsyncIteratorObject } from '@orpc/shared'; import { isORPCErrorStatus, isORPCErrorJson, createORPCErrorFromJson, mapEventIterator, toORPCError } from '@orpc/client'; import { isContractProcedure, fallbackContractConfig, ORPCError } from '@orpc/contract'; import { mergeStandardHeaders, ErrorEvent } from '@orpc/standard-server'; class StandardOpenAPIJsonSerializer { customSerializers; constructor(options = {}) { this.customSerializers = options.customJsonSerializers ?? []; } serialize(data, hasBlobRef = { value: false }) { for (const custom of this.customSerializers) { if (custom.condition(data)) { const result = this.serialize(custom.serialize(data), hasBlobRef); return result; } } if (data instanceof Blob) { hasBlobRef.value = true; return [data, hasBlobRef.value]; } if (data instanceof Set) { return this.serialize(Array.from(data), hasBlobRef); } if (data instanceof Map) { return this.serialize(Array.from(data.entries()), hasBlobRef); } if (Array.isArray(data)) { const json = data.map((v) => v === void 0 ? null : this.serialize(v, hasBlobRef)[0]); return [json, hasBlobRef.value]; } if (isObject(data)) { const json = {}; for (const k in data) { if (k === "toJSON" && typeof data[k] === "function") { continue; } json[k] = this.serialize(data[k], hasBlobRef)[0]; } return [json, hasBlobRef.value]; } if (typeof data === "bigint" || data instanceof RegExp || data instanceof URL) { return [data.toString(), hasBlobRef.value]; } if (data instanceof Date) { return [Number.isNaN(data.getTime()) ? null : data.toISOString(), hasBlobRef.value]; } if (Number.isNaN(data)) { return [null, hasBlobRef.value]; } return [data, hasBlobRef.value]; } } function standardizeHTTPPath(path) { return `/${path.replace(/\/{2,}/g, "/").replace(/^\/|\/$/g, "")}`; } function getDynamicParams(path) { return path ? standardizeHTTPPath(path).match(/\/\{[^}]+\}/g)?.map((v) => ({ raw: v, name: v.match(/\{\+?([^}]+)\}/)[1] })) : void 0; } class StandardOpenapiLinkCodec { constructor(contract, serializer, options) { this.contract = contract; this.serializer = serializer; this.baseUrl = options.url; this.headers = options.headers ?? {}; this.customErrorResponseBodyDecoder = options.customErrorResponseBodyDecoder; } baseUrl; headers; customErrorResponseBodyDecoder; async encode(path, input, options) { let headers = toStandardHeaders(await value(this.headers, options, path, input)); if (options.lastEventId !== void 0) { headers = mergeStandardHeaders(headers, { "last-event-id": options.lastEventId }); } const baseUrl = await value(this.baseUrl, options, path, input); const procedure = get(this.contract, path); if (!isContractProcedure(procedure)) { throw new Error(`[StandardOpenapiLinkCodec] expect a contract procedure at ${path.join(".")}`); } const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure); return inputStructure === "compact" ? this.#encodeCompact(procedure, path, input, options, baseUrl, headers) : this.#encodeDetailed(procedure, path, input, options, baseUrl, headers); } #encodeCompact(procedure, path, input, options, baseUrl, headers) { let httpPath = standardizeHTTPPath(procedure["~orpc"].route.path ?? toHttpPath(path)); let httpBody = input; const dynamicParams = getDynamicParams(httpPath); if (dynamicParams?.length) { if (!isObject(input)) { throw new TypeError(`[StandardOpenapiLinkCodec] Invalid input shape for "compact" structure when has dynamic params at ${path.join(".")}.`); } const body = { ...input }; for (const param of dynamicParams) { const value2 = input[param.name]; httpPath = httpPath.replace(param.raw, `/${encodeURIComponent(`${this.serializer.serialize(value2)}`)}`); delete body[param.name]; } httpBody = Object.keys(body).length ? body : void 0; } const method = fallbackContractConfig("defaultMethod", procedure["~orpc"].route.method); const url = new URL(baseUrl); url.pathname = `${url.pathname.replace(/\/$/, "")}${httpPath}`; if (method === "GET") { const serialized = this.serializer.serialize(httpBody, { outputFormat: "URLSearchParams" }); for (const [key, value2] of serialized) { url.searchParams.append(key, value2); } return { url, method, headers, body: void 0, signal: options.signal }; } return { url, method, headers, body: this.serializer.serialize(httpBody), signal: options.signal }; } #encodeDetailed(procedure, path, input, options, baseUrl, headers) { let httpPath = standardizeHTTPPath(procedure["~orpc"].route.path ?? toHttpPath(path)); const dynamicParams = getDynamicParams(httpPath); if (!isObject(input) && input !== void 0) { throw new TypeError(`[StandardOpenapiLinkCodec] Invalid input shape for "detailed" structure at ${path.join(".")}.`); } if (dynamicParams?.length) { if (!isObject(input?.params)) { throw new TypeError(`[StandardOpenapiLinkCodec] Invalid input.params shape for "detailed" structure when has dynamic params at ${path.join(".")}.`); } for (const param of dynamicParams) { const value2 = input.params[param.name]; httpPath = httpPath.replace(param.raw, `/${encodeURIComponent(`${this.serializer.serialize(value2)}`)}`); } } let mergedHeaders = headers; if (input?.headers !== void 0) { if (!isObject(input.headers)) { throw new TypeError(`[StandardOpenapiLinkCodec] Invalid input.headers shape for "detailed" structure at ${path.join(".")}.`); } mergedHeaders = mergeStandardHeaders(input.headers, headers); } const method = fallbackContractConfig("defaultMethod", procedure["~orpc"].route.method); const url = new URL(baseUrl); url.pathname = `${url.pathname.replace(/\/$/, "")}${httpPath}`; if (input?.query !== void 0) { const query = this.serializer.serialize(input.query, { outputFormat: "URLSearchParams" }); for (const [key, value2] of query) { url.searchParams.append(key, value2); } } if (method === "GET") { return { url, method, headers: mergedHeaders, body: void 0, signal: options.signal }; } return { url, method, headers: mergedHeaders, body: this.serializer.serialize(input?.body), signal: options.signal }; } async decode(response, _options, path) { const isOk = !isORPCErrorStatus(response.status); const deserialized = await (async () => { let isBodyOk = false; try { const body = await response.body(); isBodyOk = true; return this.serializer.deserialize(body); } catch (error) { if (!isBodyOk) { throw new Error("Cannot parse response body, please check the response body and content-type.", { cause: error }); } throw new Error("Invalid OpenAPI response format.", { cause: error }); } })(); if (!isOk) { const error = this.customErrorResponseBodyDecoder?.(deserialized, response); if (error !== null && error !== void 0) { throw error; } if (isORPCErrorJson(deserialized)) { throw createORPCErrorFromJson(deserialized); } throw new ORPCError(getMalformedResponseErrorCode(response.status), { status: response.status, data: { ...response, body: deserialized } }); } const procedure = get(this.contract, path); if (!isContractProcedure(procedure)) { throw new Error(`[StandardOpenapiLinkCodec] expect a contract procedure at ${path.join(".")}`); } const outputStructure = fallbackContractConfig("defaultOutputStructure", procedure["~orpc"].route.outputStructure); if (outputStructure === "compact") { return deserialized; } return { status: response.status, headers: response.headers, body: deserialized }; } } class StandardOpenAPISerializer { constructor(jsonSerializer, bracketNotation) { this.jsonSerializer = jsonSerializer; this.bracketNotation = bracketNotation; } serialize(data, options = {}) { if (isAsyncIteratorObject(data) && !options.outputFormat) { return mapEventIterator(data, { value: async (value) => this.#serialize(value, { outputFormat: "plain" }), error: async (e) => { return new ErrorEvent({ data: this.#serialize(toORPCError(e).toJSON(), { outputFormat: "plain" }), cause: e }); } }); } return this.#serialize(data, options); } #serialize(data, options) { const [json, hasBlob] = this.jsonSerializer.serialize(data); if (options.outputFormat === "plain") { return json; } if (options.outputFormat === "URLSearchParams") { const params = new URLSearchParams(); for (const [path, value] of this.bracketNotation.serialize(json)) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { params.append(path, value.toString()); } } return params; } if (json instanceof Blob || json === void 0 || !hasBlob) { return json; } const form = new FormData(); for (const [path, value] of this.bracketNotation.serialize(json)) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { form.append(path, value.toString()); } else if (value instanceof Blob) { form.append(path, value); } } return form; } deserialize(data) { if (data instanceof URLSearchParams || data instanceof FormData) { return this.bracketNotation.deserialize(Array.from(data.entries())); } if (isAsyncIteratorObject(data)) { return mapEventIterator(data, { value: async (value) => value, error: async (e) => { if (e instanceof ErrorEvent && isORPCErrorJson(e.data)) { return createORPCErrorFromJson(e.data, { cause: e }); } return e; } }); } return data; } } class StandardOpenAPILink extends StandardLink { constructor(contract, linkClient, options) { const jsonSerializer = new StandardOpenAPIJsonSerializer(options); const bracketNotationSerializer = new StandardBracketNotationSerializer({ maxBracketNotationArrayIndex: 4294967294 }); const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer); const linkCodec = new StandardOpenapiLinkCodec(contract, serializer, options); super(linkCodec, linkClient, options); } } export { StandardOpenAPIJsonSerializer as S, StandardOpenAPILink as a, StandardOpenapiLinkCodec as b, StandardOpenAPISerializer as c, getDynamicParams as g, standardizeHTTPPath as s };