typed-openapi
Version:
1,070 lines (1,026 loc) • 40.1 kB
JavaScript
// src/asserts.ts
var isPrimitiveType = (type2) => primitiveTypeList.includes(type2);
var primitiveTypeList = ["string", "number", "integer", "boolean", "null"];
// src/is-reference-object.ts
function isReferenceObject(obj) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, "$ref");
}
// src/string-utils.ts
import { capitalize, kebabToCamel } from "pastable/server";
function normalizeString(text) {
const prefixed = prefixStringStartingWithNumberIfNeeded(text);
return prefixed.normalize("NFKD").trim().replace(/\s+/g, "_").replace(/-+/g, "_").replace(/[^\w\-]+/g, "_").replace(/--+/g, "-");
}
var onlyWordRegex = /^\w+$/;
var wrapWithQuotesIfNeeded = (str) => {
if (str[0] === '"' && str[str.length - 1] === '"') return str;
if (onlyWordRegex.test(str)) {
return str;
}
return `"${str}"`;
};
var prefixStringStartingWithNumberIfNeeded = (str) => {
const firstAsNumber = Number(str[0]);
if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) {
return "_" + str;
}
return str;
};
var pathParamWithBracketsRegex = /({\w+})/g;
var wordPrecededByNonWordCharacter = /[^\w\-]+/g;
var pathToVariableName = (path) => capitalize(kebabToCamel(path).replaceAll("/", "_")).replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))).replace(wordPrecededByNonWordCharacter, "_");
// src/openapi-schema-to-ts.ts
var openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }) => {
const meta = {};
if (!schema) {
throw new Error("Schema is required");
}
const t = createBoxFactory(schema, ctx);
const getTs = () => {
if (isReferenceObject(schema)) {
const refInfo = ctx.refs.getInfosByRef(schema.$ref);
return t.reference(refInfo.normalized);
}
if (Array.isArray(schema.type)) {
if (schema.type.length === 1) {
return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0] }, ctx, meta });
}
return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta })));
}
if (schema.type === "null") {
return t.literal("null");
}
if (schema.oneOf) {
if (schema.oneOf.length === 1) {
return openApiSchemaToTs({ schema: schema.oneOf[0], ctx, meta });
}
return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
}
if (schema.anyOf) {
if (schema.anyOf.length === 1) {
return openApiSchemaToTs({ schema: schema.anyOf[0], ctx, meta });
}
return t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
}
if (schema.allOf) {
const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
const { allOf, externalDocs, example, examples, description, title, ...rest } = schema;
if (Object.keys(rest).length > 0) {
types.push(openApiSchemaToTs({ schema: rest, ctx, meta }));
}
return t.intersection(types);
}
const schemaType = schema.type ? schema.type.toLowerCase() : void 0;
if (schemaType && isPrimitiveType(schemaType)) {
if (schema.enum) {
if (schema.enum.length === 1) {
const value = schema.enum[0];
if (value === null) {
return t.literal("null");
} else if (value === true) {
return t.literal("true");
} else if (value === false) {
return t.literal("false");
} else if (typeof value === "number") {
return t.literal(`${value}`);
} else {
return t.literal(`"${value}"`);
}
}
if (schemaType === "string") {
return t.union(schema.enum.map((value) => t.literal(`"${value}"`)));
}
if (schema.enum.some((e) => typeof e === "string")) {
return t.never();
}
return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value)));
}
if (schemaType === "string") return t.string();
if (schemaType === "boolean") return t.boolean();
if (schemaType === "number" || schemaType === "integer") return t.number();
if (schemaType === "null") return t.literal("null");
}
if (!schemaType && schema.enum) {
return t.union(
schema.enum.map((value) => {
if (typeof value === "string") {
return t.literal(`"${value}"`);
}
if (value === null) {
return t.literal("null");
}
return t.literal(value);
})
);
}
if (schemaType === "array") {
if (schema.items) {
let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta });
if (typeof arrayOfType === "string") {
arrayOfType = t.reference(arrayOfType);
}
return t.array(arrayOfType);
}
return t.array(t.any());
}
if (schemaType === "object" || schema.properties || schema.additionalProperties) {
if (!schema.properties) {
if (schema.additionalProperties && !isReferenceObject(schema.additionalProperties) && typeof schema.additionalProperties !== "boolean" && schema.additionalProperties.type) {
const valueSchema = openApiSchemaToTs({ schema: schema.additionalProperties, ctx, meta });
return t.literal(`Record<string, ${valueSchema.value}>`);
}
return t.literal("Record<string, unknown>");
}
let additionalProperties;
if (schema.additionalProperties) {
let additionalPropertiesType;
if (typeof schema.additionalProperties === "boolean" && schema.additionalProperties || typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0) {
additionalPropertiesType = t.any();
} else if (typeof schema.additionalProperties === "object") {
additionalPropertiesType = openApiSchemaToTs({
schema: schema.additionalProperties,
ctx,
meta
});
}
additionalProperties = t.object({ [t.string().value]: additionalPropertiesType });
}
const hasRequiredArray = schema.required && schema.required.length > 0;
const isPartial = !schema.required?.length;
const props = Object.fromEntries(
Object.entries(schema.properties).map(([prop, propSchema]) => {
let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta });
if (typeof propType === "string") {
propType = t.reference(propType);
}
const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false);
const isOptional = !isPartial && !isRequired;
return [`${wrapWithQuotesIfNeeded(prop)}`, isOptional ? t.optional(propType) : propType];
})
);
const objectType = additionalProperties ? t.intersection([t.object(props), additionalProperties]) : t.object(props);
return isPartial ? t.reference("Partial", [objectType]) : objectType;
}
if (!schemaType) return t.unknown();
throw new Error(`Unsupported schema type: ${schemaType}`);
};
let output = getTs();
if (!isReferenceObject(schema)) {
if (schema.nullable) {
output = t.union([output, t.literal("null")]);
}
}
return output;
};
// src/box.ts
var Box = class _Box {
constructor(definition) {
this.definition = definition;
this.definition = definition;
this.type = definition.type;
this.value = definition.value;
this.params = definition.params;
this.schema = definition.schema;
this.ctx = definition.ctx;
}
type;
value;
params;
schema;
ctx;
toJSON() {
return { type: this.type, value: this.value };
}
toString() {
return JSON.stringify(this.toJSON(), null, 2);
}
recompute(callback) {
return openApiSchemaToTs({ schema: this.schema, ctx: { ...this.ctx, onBox: callback } });
}
static fromJSON(json) {
return new _Box(JSON.parse(json));
}
static isBox(box) {
return box instanceof _Box;
}
static isUnion(box) {
return box.type === "union";
}
static isIntersection(box) {
return box.type === "intersection";
}
static isArray(box) {
return box.type === "array";
}
static isOptional(box) {
return box.type === "optional";
}
static isReference(box) {
return box.type === "ref";
}
static isKeyword(box) {
return box.type === "keyword";
}
static isObject(box) {
return box.type === "object";
}
static isLiteral(box) {
return box.type === "literal";
}
};
// src/box-factory.ts
var unwrap = (param) => typeof param === "string" ? param : param.value;
var createFactory = (f) => f;
var createBoxFactory = (schema, ctx) => {
const f = typeof ctx.factory === "function" ? ctx.factory(schema, ctx) : ctx.factory;
const callback = (box2) => {
if (f.callback) {
box2 = f.callback(box2);
}
if (ctx?.onBox) {
box2 = ctx.onBox?.(box2);
}
return box2;
};
const box = {
union: (types) => callback(new Box({ ctx, schema, type: "union", params: { types }, value: f.union(types) })),
intersection: (types) => callback(new Box({ ctx, schema, type: "intersection", params: { types }, value: f.intersection(types) })),
array: (type2) => callback(new Box({ ctx, schema, type: "array", params: { type: type2 }, value: f.array(type2) })),
optional: (type2) => callback(new Box({ ctx, schema, type: "optional", params: { type: type2 }, value: f.optional(type2) })),
reference: (name, generics) => callback(
new Box({
ctx,
schema,
type: "ref",
params: generics ? { name, generics } : { name },
value: f.reference(name, generics)
})
),
literal: (value) => callback(new Box({ ctx, schema, type: "literal", params: {}, value: f.literal(value) })),
string: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "string" }, value: f.string() })),
number: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "number" }, value: f.number() })),
boolean: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "boolean" }, value: f.boolean() })),
unknown: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "unknown" }, value: f.unknown() })),
any: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "any" }, value: f.any() })),
never: () => callback(new Box({ ctx, schema, type: "keyword", params: { name: "never" }, value: f.never() })),
object: (props) => callback(new Box({ ctx, schema, type: "object", params: { props }, value: f.object(props) }))
};
return box;
};
// src/generator.ts
import { capitalize as capitalize2, groupBy } from "pastable/server";
import * as Codegen from "@sinclair/typebox-codegen";
import { match } from "ts-pattern";
import { type } from "arktype";
var allowedRuntimes = type("'none' | 'arktype' | 'io-ts' | 'typebox' | 'valibot' | 'yup' | 'zod'");
var runtimeValidationGenerator = {
arktype: Codegen.ModelToArkType.Generate,
"io-ts": Codegen.ModelToIoTs.Generate,
typebox: Codegen.ModelToTypeBox.Generate,
valibot: Codegen.ModelToValibot.Generate,
yup: Codegen.ModelToYup.Generate,
zod: Codegen.ModelToZod.Generate
};
var inferByRuntime = {
none: (input) => input,
arktype: (input) => `${input}["infer"]`,
"io-ts": (input) => `t.TypeOf<${input}>`,
typebox: (input) => `Static<${input}>`,
valibot: (input) => `v.InferOutput<${input}>`,
yup: (input) => `y.InferType<${input}>`,
zod: (input) => `z.infer<${input}>`
};
var methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
var methodsRegex = new RegExp(`(?:${methods.join("|")})_`);
var endpointExport = new RegExp(`export (?:type|const) (?:${methodsRegex.source})`);
var replacerByRuntime = {
yup: (line) => line.replace(/y\.InferType<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(y\.object)(\()/).source, "g"),
"$1$2("
),
zod: (line) => line.replace(/z\.infer<\s*?typeof (.*?)\s*?>/g, "typeof $1").replace(
new RegExp(`(${endpointExport.source})` + new RegExp(/([\s\S]*? )(z\.object)(\()/).source, "g"),
"$1$2("
)
};
var generateFile = (options) => {
const ctx = { ...options, runtime: options.runtime ?? "none" };
const schemaList = generateSchemaList(ctx);
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
const apiClient = options.schemasOnly ? "" : generateApiClient(ctx);
const transform = ctx.runtime === "none" ? (file2) => file2 : (file2) => {
const model = Codegen.TypeScriptToModel.Generate(file2);
const transformer = runtimeValidationGenerator[ctx.runtime];
const generated = ctx.runtime === "typebox" ? Codegen.TypeScriptToTypeBox.Generate(file2) : transformer(model);
let converted = "";
const match3 = generated.match(/(const __ENDPOINTS_START__ =)([\s\S]*?)(export type __ENDPOINTS_END__)/);
const content = match3?.[2];
if (content && ctx.runtime in replacerByRuntime) {
const before = generated.slice(0, generated.indexOf("export type __ENDPOINTS_START"));
converted = before + replacerByRuntime[ctx.runtime](
content.slice(content.indexOf("export"))
);
} else {
converted = generated;
}
return converted;
};
const file = `
${transform(schemaList + endpointSchemaList)}
${apiClient}
`;
return file;
};
var generateSchemaList = ({ refs, runtime }) => {
let file = `
${runtime === "none" ? "export namespace Schemas {" : ""}
// <Schemas>
`;
refs.getOrderedSchemas().forEach(([schema, infos]) => {
if (!infos?.name) return;
if (infos.kind !== "schemas") return;
file += `export type ${infos.normalized} = ${schema.value}
`;
});
return file + `
// </Schemas>
${runtime === "none" ? "}" : ""}
`;
};
var parameterObjectToString = (parameters) => {
if (parameters instanceof Box) return parameters.value;
let str = "{";
for (const [key, box] of Object.entries(parameters)) {
str += `${wrapWithQuotesIfNeeded(key)}${box.type === "optional" ? "?" : ""}: ${box.value},
`;
}
return str + "}";
};
var responseHeadersObjectToString = (responseHeaders, ctx) => {
let str = "{";
for (const [key, responseHeader] of Object.entries(responseHeaders)) {
const value = ctx.runtime === "none" ? responseHeader.recompute((box) => {
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
box.value = `Schemas.${box.value}`;
}
return box;
}).value : responseHeader.value;
str += `${wrapWithQuotesIfNeeded(key.toLowerCase())}: ${value},
`;
}
return str + "}";
};
var generateEndpointSchemaList = (ctx) => {
let file = `
${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
// <Endpoints>
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_START__ = {}"}
`;
ctx.endpointList.map((endpoint) => {
const parameters = endpoint.parameters ?? {};
file += `export type ${endpoint.meta.alias} = {
method: "${endpoint.method.toUpperCase()}",
path: "${endpoint.path}",
requestFormat: "${endpoint.requestFormat}",
${endpoint.meta.hasParameters ? `parameters: {
${parameters.query ? `query: ${parameterObjectToString(parameters.query)},` : ""}
${parameters.path ? `path: ${parameterObjectToString(parameters.path)},` : ""}
${parameters.header ? `header: ${parameterObjectToString(parameters.header)},` : ""}
${parameters.body ? `body: ${parameterObjectToString(
ctx.runtime === "none" ? parameters.body.recompute((box) => {
if (Box.isReference(box) && !box.params.generics) {
box.value = `Schemas.${box.value}`;
}
return box;
}) : parameters.body
)},` : ""}
}` : "parameters: never,"}
response: ${ctx.runtime === "none" ? endpoint.response.recompute((box) => {
if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
box.value = `Schemas.${box.value}`;
}
return box;
}).value : endpoint.response.value},
${endpoint.responseHeaders ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},` : ""}
}
`;
});
return file + `
// </Endpoints>
${ctx.runtime === "none" ? "}" : ""}
${ctx.runtime === "none" ? "" : "type __ENDPOINTS_END__ = {}"}
`;
};
var generateEndpointByMethod = (ctx) => {
const { endpointList } = ctx;
const byMethods = groupBy(endpointList, "method");
const endpointByMethod = `
// <EndpointByMethod>
export ${ctx.runtime === "none" ? "type" : "const"} EndpointByMethod = {
${Object.entries(byMethods).map(([method, list]) => {
return `${method}: {
${list.map(
(endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`
)}
}`;
}).join(",\n")}
}
${ctx.runtime === "none" ? "" : "export type EndpointByMethod = typeof EndpointByMethod;"}
// </EndpointByMethod>
`;
const shorthands = `
// <EndpointByMethod.Shorthands>
${Object.keys(byMethods).map((method) => `export type ${capitalize2(method)}Endpoints = EndpointByMethod["${method}"]`).join("\n")}
// </EndpointByMethod.Shorthands>
`;
return endpointByMethod + shorthands;
};
var generateApiClient = (ctx) => {
const { endpointList } = ctx;
const byMethods = groupBy(endpointList, "method");
const endpointSchemaList = generateEndpointByMethod(ctx);
const apiClientTypes = `
// <ApiClientTypes>
export type EndpointParameters = {
body?: unknown;
query?: Record<string, unknown>;
header?: Record<string, unknown>;
path?: Record<string, unknown>;
};
export type MutationMethod = "post" | "put" | "patch" | "delete";
export type Method = "get" | "head" | "options" | MutationMethod;
type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
export type DefaultEndpoint = {
parameters?: EndpointParameters | undefined;
response: unknown;
responseHeaders?: Record<string, unknown>;
};
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
operationId: string;
method: Method;
path: string;
requestFormat: RequestFormat;
parameters?: TConfig["parameters"];
meta: {
alias: string;
hasParameters: boolean;
areParametersRequired: boolean;
};
response: TConfig["response"];
responseHeaders?: TConfig["responseHeaders"]
};
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
type RequiredKeys<T> = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
// </ApiClientTypes>
`;
const apiClient = `
// <ApiClient>
export class ApiClient {
baseUrl: string = "";
constructor(public fetcher: Fetcher) {}
setBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl;
return this;
}
parseResponse = async <T>(response: Response): Promise<T> => {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return response.json();
}
return response.text() as unknown as T;
}
${Object.entries(byMethods).map(([method, endpointByMethod]) => {
const capitalizedMethod = capitalize2(method);
const infer = inferByRuntime[ctx.runtime];
return endpointByMethod.length ? `// <ApiClient.${method}>
${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["parameters"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`).otherwise(() => `TEndpoint["parameters"]`)}>
): Promise<${match(ctx.runtime).with("zod", "yup", () => infer(`TEndpoint["response"]`)).with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`).otherwise(() => `TEndpoint["response"]`)}> {
return this.fetcher("${method}", this.baseUrl + path, params[0])
.then(response => this.parseResponse(response))${match(ctx.runtime).with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`).with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`).otherwise(() => `as Promise<TEndpoint["response"]>`)};
}
// </ApiClient.${method}>
` : "";
}).join("\n")}
// <ApiClient.request>
/**
* Generic request method with full type-safety for any endpoint
*/
request<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath]
>(
method: TMethod,
path: TPath,
...params: MaybeOptionalArg<${match(ctx.runtime).with(
"zod",
"yup",
() => inferByRuntime[ctx.runtime](`TEndpoint extends { parameters: infer Params } ? Params : never`)
).with(
"arktype",
"io-ts",
"typebox",
"valibot",
() => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`
).otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
: Promise<Omit<Response, "json"> & {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
}> {
return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
}
// </ApiClient.request>
}
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
}
/**
Example usage:
const api = createApiClient((method, url, params) =>
fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
);
api.get("/users").then((users) => console.log(users));
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
*/
// </ApiClient
`;
return endpointSchemaList + apiClientTypes + apiClient;
};
// src/ref-resolver.ts
import { get } from "pastable/server";
// src/topological-sort.ts
function topologicalSort(graph) {
const sorted = [], visited = {};
function visit(name, ancestors) {
if (!Array.isArray(ancestors)) ancestors = [];
ancestors.push(name);
visited[name] = true;
const deps = graph.get(name);
if (deps) {
deps.forEach((dep) => {
if (ancestors.includes(dep)) {
return;
}
if (visited[dep]) return;
visit(dep, ancestors.slice(0));
});
}
if (!sorted.includes(name)) sorted.push(name);
}
graph.forEach((_, name) => visit(name, []));
return sorted;
}
// src/ref-resolver.ts
var autocorrectRef = (ref) => ref[1] === "/" ? ref : "#/" + ref.slice(1);
var componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
var createRefResolver = (doc, factory2) => {
const nameByRef = /* @__PURE__ */ new Map();
const refByName = /* @__PURE__ */ new Map();
const byRef = /* @__PURE__ */ new Map();
const byNormalized = /* @__PURE__ */ new Map();
const boxByRef = /* @__PURE__ */ new Map();
const getSchemaByRef = (ref) => {
const correctRef = autocorrectRef(ref);
const split = correctRef.split("/");
const path = split.slice(1, -1).join("/");
const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
const map = get(doc, normalizedPath) ?? {};
const name = split[split.length - 1];
const normalized = normalizeString(name);
nameByRef.set(correctRef, normalized);
refByName.set(normalized, correctRef);
const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] };
byRef.set(infos.ref, infos);
byNormalized.set(infos.normalized, infos);
const schema = map[name];
if (!schema) {
throw new Error(`Unresolved ref "${name}" not found in "${path}"`);
}
return schema;
};
const getInfosByRef = (ref) => byRef.get(autocorrectRef(ref));
const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key));
schemaEntries.forEach(([key, component]) => {
Object.keys(component).map((name) => {
const ref = `#/components/${key}/${name}`;
getSchemaByRef(ref);
});
});
const directDependencies = /* @__PURE__ */ new Map();
schemaEntries.forEach(([key, component]) => {
Object.keys(component).map((name) => {
const ref = `#/components/${key}/${name}`;
const schema = getSchemaByRef(ref);
boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory: factory2, refs: { getInfosByRef } } }));
if (!directDependencies.has(ref)) {
directDependencies.set(ref, /* @__PURE__ */ new Set());
}
setSchemaDependencies(schema, directDependencies.get(ref));
});
});
const transitiveDependencies = getTransitiveDependencies(directDependencies);
return {
get: getSchemaByRef,
unwrap: (component) => {
return isReferenceObject(component) ? getSchemaByRef(component.$ref) : component;
},
getInfosByRef,
infos: byRef,
/**
* Get the schemas in the order they should be generated, depending on their dependencies
* so that a schema is generated before the ones that depend on it
*/
getOrderedSchemas: () => {
const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => {
const infos = getInfosByRef(ref);
return [boxByRef.get(infos.ref), infos];
});
return schemaOrderedByDependencies;
},
directDependencies,
transitiveDependencies
};
};
var setSchemaDependencies = (schema, deps) => {
const visit = (schema2) => {
if (!schema2) return;
if (isReferenceObject(schema2)) {
deps.add(schema2.$ref);
return;
}
if (schema2.allOf) {
for (const allOf of schema2.allOf) {
visit(allOf);
}
return;
}
if (schema2.oneOf) {
for (const oneOf of schema2.oneOf) {
visit(oneOf);
}
return;
}
if (schema2.anyOf) {
for (const anyOf of schema2.anyOf) {
visit(anyOf);
}
return;
}
if (schema2.type === "array") {
if (!schema2.items) return;
return void visit(schema2.items);
}
if (schema2.type === "object" || schema2.properties || schema2.additionalProperties) {
if (schema2.properties) {
for (const property in schema2.properties) {
visit(schema2.properties[property]);
}
}
if (schema2.additionalProperties && typeof schema2.additionalProperties === "object") {
visit(schema2.additionalProperties);
}
}
};
visit(schema);
};
var getTransitiveDependencies = (directDependencies) => {
const transitiveDependencies = /* @__PURE__ */ new Map();
const visitedsDeepRefs = /* @__PURE__ */ new Set();
directDependencies.forEach((deps, ref) => {
if (!transitiveDependencies.has(ref)) {
transitiveDependencies.set(ref, /* @__PURE__ */ new Set());
}
const visit = (depRef) => {
transitiveDependencies.get(ref).add(depRef);
const deps2 = directDependencies.get(depRef);
if (deps2 && ref !== depRef) {
deps2.forEach((transitive) => {
const key = ref + "__" + transitive;
if (visitedsDeepRefs.has(key)) return;
visitedsDeepRefs.add(key);
visit(transitive);
});
}
};
deps.forEach((dep) => visit(dep));
});
return transitiveDependencies;
};
// src/ts-factory.ts
var tsFactory = createFactory({
union: (types) => `(${types.map(unwrap).join(" | ")})`,
intersection: (types) => `(${types.map(unwrap).join(" & ")})`,
array: (type2) => `Array<${unwrap(type2)}>`,
optional: (type2) => `${unwrap(type2)} | undefined`,
reference: (name, typeArgs) => `${name}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`,
literal: (value) => value.toString(),
string: () => "string",
number: () => "number",
boolean: () => "boolean",
unknown: () => "unknown",
any: () => "any",
never: () => "never",
object: (props) => {
const propsString = Object.entries(props).map(
([prop, type2]) => `${wrapWithQuotesIfNeeded(prop)}${typeof type2 !== "string" && Box.isOptional(type2) ? "?" : ""}: ${unwrap(
type2
)}`
).join(", ");
return `{ ${propsString} }`;
}
});
// src/map-openapi-endpoints.ts
import { capitalize as capitalize3, pick } from "pastable/server";
import { match as match2, P } from "ts-pattern";
var factory = tsFactory;
var mapOpenApiEndpoints = (doc) => {
const refs = createRefResolver(doc, factory);
const ctx = { refs, factory };
const endpointList = [];
Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => {
const pathItem = pick(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
Object.entries(pathItem).forEach(([method, operation]) => {
if (operation.deprecated) return;
const endpoint = {
operation,
method,
path,
requestFormat: "json",
response: openApiSchemaToTs({ schema: {}, ctx }),
meta: {
alias: getAlias({ path, method, operation }),
areParametersRequired: false,
hasParameters: false
}
};
const lists = { query: [], path: [], header: [] };
const paramObjects = [...pathItemObj.parameters ?? [], ...operation.parameters ?? []].reduce(
(acc, paramOrRef) => {
const param = refs.unwrap(paramOrRef);
const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx });
if (param.required) endpoint.meta.areParametersRequired = true;
endpoint.meta.hasParameters = true;
if (param.in === "query") {
lists.query.push(param);
acc.query[param.name] = schema;
}
if (param.in === "path") {
lists.path.push(param);
acc.path[param.name] = schema;
}
if (param.in === "header") {
lists.header.push(param);
acc.header[param.name] = schema;
}
return acc;
},
{ query: {}, path: {}, header: {} }
);
const params = Object.entries(paramObjects).reduce(
(acc, [key, value]) => {
if (Object.keys(value).length) {
acc[key] = value;
}
return acc;
},
{}
);
if (operation.requestBody) {
endpoint.meta.hasParameters = true;
const requestBody = refs.unwrap(operation.requestBody ?? {});
const content2 = requestBody.content;
const matchingMediaType = Object.keys(content2).find(isAllowedParamMediaTypes);
if (matchingMediaType && content2[matchingMediaType]) {
params.body = openApiSchemaToTs({
schema: content2[matchingMediaType]?.schema ?? {} ?? {},
ctx
});
}
endpoint.requestFormat = match2(matchingMediaType).with("application/octet-stream", () => "binary").with("multipart/form-data", () => "form-data").with("application/x-www-form-urlencoded", () => "form-url").with(P.string.includes("json"), () => "json").otherwise(() => "text");
}
if (params) {
const t = createBoxFactory({}, ctx);
const filtered_params = ["query", "path", "header"];
for (const k of filtered_params) {
if (params[k] && lists[k].length) {
if (lists[k].every((param) => !param.required)) {
params[k] = t.reference("Partial", [t.object(params[k])]);
} else {
for (const p of lists[k]) {
if (!p.required) {
params[k][p.name] = t.optional(params[k][p.name]);
}
}
}
}
}
endpoint.parameters = Object.keys(params).length ? params : void 0;
}
let responseObject;
Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
const statusCode = Number(status);
if (statusCode >= 200 && statusCode < 300) {
responseObject = refs.unwrap(responseOrRef);
}
});
if (!responseObject && operation.responses?.default) {
responseObject = refs.unwrap(operation.responses.default);
}
const content = responseObject?.content;
if (content) {
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
if (matchingMediaType && content[matchingMediaType]) {
endpoint.response = openApiSchemaToTs({
schema: content[matchingMediaType]?.schema ?? {} ?? {},
ctx
});
}
}
const headers = responseObject?.headers;
if (headers) {
endpoint.responseHeaders = Object.entries(headers).reduce(
(acc, [name, headerOrRef]) => {
const header = refs.unwrap(headerOrRef);
acc[name] = openApiSchemaToTs({ schema: header.schema ?? {}, ctx });
return acc;
},
{}
);
}
endpointList.push(endpoint);
});
});
return { doc, refs, endpointList, factory };
};
var allowedParamMediaTypes = [
"application/octet-stream",
"multipart/form-data",
"application/x-www-form-urlencoded",
"*/*"
];
var isAllowedParamMediaTypes = (mediaType) => mediaType.includes("application/") && mediaType.includes("json") || allowedParamMediaTypes.includes(mediaType) || mediaType.includes("text/");
var isResponseMediaType = (mediaType) => mediaType === "application/json";
var getAlias = ({ path, method, operation }) => (method + "_" + capitalize3(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__");
// src/format.ts
import prettier from "prettier";
import parserTypescript from "prettier/parser-typescript";
function maybePretty(input, options) {
try {
return prettier.format(input, {
parser: "typescript",
plugins: [parserTypescript],
...options
});
} catch (err) {
console.warn("Failed to format code");
console.warn(err);
return input;
}
}
var prettify = (str, options) => maybePretty(str, { printWidth: 120, trailingComma: "all", ...options });
// src/tanstack-query.generator.ts
import { capitalize as capitalize4 } from "pastable/server";
var generateTanstackQueryFile = async (ctx) => {
const endpointMethods = new Set(ctx.endpointList.map((endpoint) => endpoint.method.toLowerCase()));
const file = `
import { queryOptions } from "@tanstack/react-query"
import type { EndpointByMethod, ApiClient } from "${ctx.relativeApiClientPath}"
type EndpointQueryKey<TOptions extends EndpointParameters> = [
TOptions & {
_id: string;
_infinite?: boolean;
}
];
const createQueryKey = <TOptions extends EndpointParameters>(id: string, options?: TOptions, infinite?: boolean): [
EndpointQueryKey<TOptions>[0]
] => {
const params: EndpointQueryKey<TOptions>[0] = { _id: id, } as EndpointQueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (options?.body) {
params.body = options.body;
}
if (options?.header) {
params.header = options.header;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [
params
];
};
// <EndpointByMethod.Shorthands>
${Array.from(endpointMethods).map((method) => `export type ${capitalize4(method)}Endpoints = EndpointByMethod["${method}"];`).join("\n")}
// </EndpointByMethod.Shorthands>
// <ApiClientTypes>
export type EndpointParameters = {
body?: unknown;
query?: Record<string, unknown>;
header?: Record<string, unknown>;
path?: Record<string, unknown>;
};
type RequiredKeys<T> = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [config: T];
// </ApiClientTypes>
// <ApiClient>
export class TanstackQueryApiClient {
constructor(public client: ApiClient) { }
${Array.from(endpointMethods).map(
(method) => `
// <ApiClient.${method}>
${method}<Path extends keyof ${capitalize4(method)}Endpoints, TEndpoint extends ${capitalize4(method)}Endpoints[Path]>(
path: Path,
...params: MaybeOptionalArg<TEndpoint["parameters"]>
) {
const queryKey = createQueryKey(path, params[0]);
const query = {
/** type-only property if you need easy access to the endpoint params */
"~endpoint": {} as TEndpoint,
queryKey,
queryOptions: queryOptions({
queryFn: async ({ queryKey, signal, }) => {
const res = await this.client.${method}(path, {
...params,
...queryKey[0],
signal,
});
return res as TEndpoint["response"];
},
queryKey: queryKey
}),
mutationOptions: {
mutationKey: queryKey,
mutationFn: async (localOptions: TEndpoint extends { parameters: infer Parameters} ? Parameters: never) => {
const res = await this.client.${method}(path, {
...params,
...queryKey[0],
...localOptions,
});
return res as TEndpoint["response"];
}
}
};
return query
}
// </ApiClient.${method}>
`
).join("\n")}
// <ApiClient.request>
/**
* Generic mutation method with full type-safety for any endpoint that doesnt require parameters to be passed initially
*/
mutation<
TMethod extends keyof EndpointByMethod,
TPath extends keyof EndpointByMethod[TMethod],
TEndpoint extends EndpointByMethod[TMethod][TPath],
TSelection,
>(method: TMethod, path: TPath, selectFn?: (res: Omit<Response, "json"> & {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
}) => TSelection) {
const mutationKey = [{ method, path }] as const;
return {
/** type-only property if you need easy access to the endpoint params */
"~endpoint": {} as TEndpoint,
mutationKey: mutationKey,
mutationOptions: {
mutationKey: mutationKey,
mutationFn: async (params: TEndpoint extends { parameters: infer Parameters } ? Parameters : never) => {
const response = await this.client.request(method, path, params);
const res = selectFn ? selectFn(response) : response
return res as unknown extends TSelection ? typeof response : Awaited<TSelection>
},
},
};
}
// </ApiClient.request>
}
`;
return prettify(file);
};
export {
unwrap,
createFactory,
createBoxFactory,
openApiSchemaToTs,
allowedRuntimes,
generateFile,
createRefResolver,
tsFactory,
mapOpenApiEndpoints,
prettify,
generateTanstackQueryFile
};