UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

359 lines (307 loc) 9.44 kB
import { upperCaseFirst } from "../../utils.js"; import { fileBlockEnd, fileBlockStart } from "../file/block.js"; import { fileContextAddLinePrefix, fileContextCreateGeneric, fileContextGetOptional, fileContextRemoveLinePrefix, fileContextSetIndent, } from "../file/context.js"; import { fileWrite } from "../file/write.js"; import { structureResolveReference } from "../processors/structure.js"; import { JavascriptImportCollector } from "../target/javascript.js"; import { apiClientDistilledTargetInfo } from "./generator.js"; /** * Write the global clients to the common directory * * @param {import("../generate").GenerateContext} generateContext */ export function jsFetchGenerateCommonFile(generateContext) { const file = fileContextCreateGeneric( generateContext, "common/api-client.js", { importCollector: new JavascriptImportCollector(), }, ); const importCollector = JavascriptImportCollector.getImportCollector(file); fileWrite( file, `/** * @typedef {(input: string|URL, init?: RequestInit) => Promise<Response>} FetchFn */ `, ); if (generateContext.options.generators.apiClient?.target.globalClient) { fileWrite( file, `/** * @type {FetchFn} */ export let fetchFn = fetch; /** * Override the global fetch function. This can be used to apply defaults to each call. * * @param {FetchFn} newFetchFn */ export function setFetchFn(newFetchFn) { fetchFn = newFetchFn; } `, ); } importCollector.destructure("@compas/stdlib", "AppError"); fileWrite( file, ` /** * Wrap the provided fetch function, adding the baseUrl to each invocation. * * @param {FetchFn} originalFetch * @param {string} baseUrl * @returns {FetchFn} */ export function fetchWithBaseUrl(originalFetch, baseUrl) { return function fetchWithBaseUrl(input, init) { return originalFetch(new URL(input, baseUrl), init); }; } /** * Wrap the provided fetch function, to catch errors and convert where possible to an AppError * * @param {FetchFn} originalFetch * @returns {FetchFn} */ export function fetchCatchErrorAndWrapWithAppError(originalFetch) { return async function fetchCatchErrorAndWrapWithAppError(input, init) { try { const response = await originalFetch(input, init); if (!response.ok) { const body = await response.json(); if (typeof body.key === "string" && !!body.info && typeof body.info === "object") { throw new AppError( body.key, response.status, body.cause ? { info: body.info, cause: body.cause } : body.info, ); } else { throw new AppError("response.error", response.status, { fetch: { request: { input, init }, response: { status: response.status, body, }, }, }); } } return response; } catch (error) { if (AppError.instanceOf(error)) { throw error; } // Unknown error, wrap with a hard '500' since this is most likely unexecpted. throw new AppError("response.error", 500, AppError.format(error)); } }; } `, ); } /** * Write the global clients to the common directory * * @param {import("../generate").GenerateContext} generateContext * @param {import("../generated/common/types").ExperimentalRouteDefinition} route * @returns {import("../file/context").GenerateFile} */ export function jsFetchGetApiClientFile(generateContext, route) { let file = fileContextGetOptional( generateContext, `${route.group}/apiClient.js`, ); if (file) { return file; } file = fileContextCreateGeneric( generateContext, `${route.group}/apiClient.js`, { importCollector: new JavascriptImportCollector(), }, ); const importCollector = JavascriptImportCollector.getImportCollector(file); if (generateContext.options.generators.apiClient?.target.globalClient) { importCollector.destructure(`../common/api-client.js`, "fetchFn"); } importCollector.destructure("@compas/stdlib", "AppError"); return file; } /** * Generate the api client function * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalRouteDefinition>} route * @param {Record<string, string>} contextNames */ export function jsFetchGenerateFunction( generateContext, file, route, contextNames, ) { const distilledTargetInfo = apiClientDistilledTargetInfo(generateContext); const args = []; fileWrite(file, `/**`); fileContextAddLinePrefix(file, " * "); if (route.docString) { fileWrite(file, `${route.docString}\n`); } fileWrite(file, `Tags: ${JSON.stringify(route.tags)}\n`); if (!distilledTargetInfo.useGlobalClients) { args.push("fetchFn"); fileWrite(file, `@param {FetchFn} fetchFn`); } if (route.params) { args.push("params"); fileWrite(file, `@param {${contextNames.paramsTypeName}} params`); } if (route.query) { args.push("query"); fileWrite(file, `@param {${contextNames.queryTypeName}} query`); } if (route.body) { args.push("body"); fileWrite( file, `@param {${contextNames.bodyTypeName}${ route.metadata?.requestBodyType === "form-data" ? "|FormData" : "" }} body`, ); } if (route.files) { args.push("files"); fileWrite(file, `@param {${contextNames.filesTypeName}} files`); } // Allow overwriting any request config args.push("requestConfig"); fileWrite( file, `@param {RequestInit${ route.response ? ` & { skipResponseValidation?: boolean }` : "" }} [requestConfig]`, ); if (route.response) { fileWrite(file, `@returns {Promise<${contextNames.responseTypeName}>}`); } else { fileWrite(file, `@returns {Promise<Response>}`); } fileContextRemoveLinePrefix(file, 3); fileWrite(file, ` */`); fileBlockStart( file, `export async function api${upperCaseFirst(route.group)}${upperCaseFirst( route.name, )}(${args.join(", ")})`, ); if (route.files || route.metadata?.requestBodyType === "form-data") { const parameter = route.body ? "body" : "files"; fileWrite( file, `const data = ${parameter} instanceof FormData ? ${parameter} : new FormData();`, ); fileBlockStart(file, `if (!(${parameter} instanceof FormData))`); /** @type {import("../generated/common/types.js").ExperimentalObjectDefinition} */ // @ts-expect-error const type = structureResolveReference( generateContext.structure, // @ts-expect-error route.body ?? route.files, ); for (const key of Object.keys(type.keys)) { const fieldType = type.keys[key].type === "reference" ? structureResolveReference(generateContext.structure, type.keys[key]) : type.keys[key]; if (fieldType.type === "file") { fileWrite( file, `data.append("${key}", ${parameter}["${key}"].data, ${parameter}["${key}"].name);`, ); } else { fileWrite(file, `data.append("${key}", ${parameter}["${key}"]);`); } } fileBlockEnd(file); } fileWrite(file, `const response = await fetchFn(`); fileContextSetIndent(file, 1); if (route.query) { fileWrite( file, `\`${route.path .split("/") .map((it) => (it.startsWith(":") ? `$\{params.${it.slice(1)}}` : it)) .join("/")}?$\{new URLSearchParams(query).toString()}\`,`, ); } else { fileWrite( file, `\`${route.path .split("/") .map((it) => (it.startsWith(":") ? `$\{params.${it.slice(1)}}` : it)) .join("/")}\`,`, ); } fileWrite(file, `{`); fileContextSetIndent(file, 1); fileWrite(file, `method: "${route.method}",`); if (route.files || route.metadata?.requestBodyType === "form-data") { fileWrite(file, `body: data,`); } if (route.body && route.metadata?.requestBodyType !== "form-data") { fileWrite(file, `body: JSON.stringify(body),`); fileWrite(file, `headers: { "Content-Type": "application/json", },`); } fileWrite(file, `...requestConfig,`); fileContextSetIndent(file, -1); fileWrite(file, `}`); fileContextSetIndent(file, -1); fileWrite(file, `);`); if ( route.response && structureResolveReference(generateContext.structure, route.response) .type === "file" ) { fileWrite(file, `const result = await response.blob();`); } else { fileWrite(file, `const result = await response.json();`); } if (route.response) { fileBlockStart(file, `if (requestConfig?.skipResponseValidation)`); fileWrite(file, `return result;`); fileBlockEnd(file); fileWrite( file, `const { value, error } = ${contextNames.responseValidator}(result);`, ); fileBlockStart(file, `if (error)`); fileWrite( file, `throw AppError.validationError("validator.error", { route: { group: "${route.group}", name: "${route.name}", }, error, });`, ); fileBlockEnd(file); fileBlockStart(file, `else`); fileWrite(file, `return value;`); fileBlockEnd(file); } else { fileWrite(file, `return response;`); } fileBlockEnd(file); fileWrite(file, "\n"); }