@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
JavaScript
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 };