UNPKG

typed-openapi

Version:
1,070 lines (1,026 loc) 40.1 kB
// 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 };