UNPKG

microcms_sdk_generator

Version:
371 lines (357 loc) 11.7 kB
import { match } from "ts-pattern"; import { format } from "prettier"; import camelCase from "../deps/deno.land/x/case@2.1.1/camelCase.js"; import { defTypeSchemaName, inputTypeName, outputTypeSchemaName, } from "./helper.js"; export async function printClientImpl(apiDefinitions) { return await format(` export type RequestOptions = RequestInit & { customFetcher?: typeof fetch; }; export type QueryForObjectApi = { draftKey?: string; fields?: string[]; depth?: 1 | 2 | 3; }; export type QueryForListApi = QueryForObjectApi & { limit: number; offset?: number; orders?: string[]; q?: string; ids?: string[]; filters?: string; }; export class RequestError extends Error { public readonly ok = false; public readonly isRequestError = true; static { this.prototype.name = "RequestError"; } constructor( public readonly statusCode: number, public readonly responseBody: string, public readonly responseHeaders: Headers, ) { super(\`Request failed with status code \${statusCode}: \${responseBody}\`); } } export class ParseResponseError extends Error { public readonly ok = false; public readonly isParseResponseError = true; static { this.prototype.name = "ParseResponseError"; } constructor( public readonly statusCode: number, public readonly data: unknown, public readonly parseError: z.ZodError, public readonly responseHeaders: Headers, ) { super( \`Failed to parse response with status code \${statusCode}: \${parseError.message}: \${JSON.stringify( data )}\` ); } } export function createClient({ serviceDomain, apiKey, requestBaseOptions, }: { serviceDomain: string; apiKey: string; requestBaseOptions?: RequestOptions; }) { const headers = { "Content-Type": "application/json", "X-MICROCMS-API-KEY": apiKey, }; function contentUrl(path: string) { return new URL( path, \`https://\${serviceDomain}.microcms.io/api/v1/\` ).toString(); } function managementUrl(path: string) { return new URL( path, \`https://\${serviceDomain}.microcms-management.io/api/v1/\` ).toString(); } async function request<OkResponseSchema extends z.AnyZodObject>( url: string, okResponseSchema: OkResponseSchema, options: RequestOptions ): Promise< | { ok: true; statusCode: number; data: z.infer<OkResponseSchema>; responseHeaders: Headers; } | RequestError | ParseResponseError > { const opts = { ...requestBaseOptions, ...options, headers: { ...headers, ...(requestBaseOptions?.headers ?? {}), ...(options?.headers ?? {}), }, }; const res = await (opts.customFetcher ?? fetch)(url, opts); if (!res.ok) { return new RequestError(res.status, await res.text(), res.headers); } let respBody: unknown; if (res.status === 202) { await res.text(); respBody = {}; } else { respBody = await res.json(); } const parser = okResponseSchema.safeParse(respBody); if (!parser.success) { return new ParseResponseError( res.status, respBody, parser.error, res.headers ); } return { ok: true, statusCode: res.status, data: parser.data, responseHeaders: res.headers, }; } function requestGet<OkResponseSchema extends z.AnyZodObject>( url: string, okResponseSchema: OkResponseSchema, query?: QueryForObjectApi | QueryForListApi, options?: RequestOptions, ): Promise< | { ok: true; statusCode: number; data: z.infer<OkResponseSchema>; responseHeaders: Headers; } | RequestError | ParseResponseError > { const searchParams = new URLSearchParams( Object.fromEntries( Object.entries(query ?? {}).map(([k, v]) => [ k, Array.isArray(v) ? v.join(",") : v.toString(), ]), ), ); return request( url + (searchParams.size > 0 ? \`?\${searchParams.toString()}\` : ""), okResponseSchema, { ...options, method: "GET" }, ); } function requestWrite<OkResponseSchema extends z.AnyZodObject>( method: "POST" | "PUT" | "PATCH" | "DELETE", url: string, okResponseSchema: OkResponseSchema, content?: unknown, options?: RequestOptions, ): Promise< | { ok: true; statusCode: number; data: z.infer<OkResponseSchema>; responseHeaders: Headers; } | RequestError | ParseResponseError > { return request(url, okResponseSchema, { ...options, method, body: content ? JSON.stringify(content) : null, }); } return {${apiDefinitions.map((apiDefinition) => { const endpointName = apiDefinition.endpointName; const camelCaseEndpointName = camelCase(endpointName); return `${camelCaseEndpointName}: { ${match(apiDefinition.apiType) .with("list", () => { return [ `list: ({ options, ...query }: QueryForListApi & {options?: RequestOptions}) => requestGet( contentUrl(\`${endpointName}\`), makeListResponseSchema(${defTypeSchemaName(endpointName)}), query, options, ),`, `get: ({ id, options, ...query }: QueryForObjectApi & { id: string; options?: RequestOptions; }) => requestGet( contentUrl(\`${endpointName}/\${id}\`), ${defTypeSchemaName(endpointName)}.merge(MicroCmsListContentFieldsSchema), query, options, ),`, `post: ({content, status, options}: { content: ${inputTypeName(endpointName)}; status?: "draft"; options?: RequestOptions; }) => requestWrite( "POST", contentUrl(\`${endpointName}\${status ? \`?status=\${status}\` : ""}\`), OnlyIdSchema, content, options, ),`, `put: ({id, content, status, options}: { id: string; content: ${inputTypeName(endpointName)}; status?: "draft"; options?: RequestOptions; }) => requestWrite( "PUT", contentUrl(\`${endpointName}/\${id}\${status ? \`?status=\${status}\` : ""}\`), OnlyIdSchema, content, options, ),`, `patch: ({id, content, options}: { id: string; content: Partial<${inputTypeName(endpointName)}>; options?: RequestOptions; }) => requestWrite( "PATCH", contentUrl(\`${endpointName}/\${id}\`), OnlyIdSchema, content, options, ),`, `delete: ({id, options}: { id: string; options?: RequestOptions; }) => requestWrite( "DELETE", contentUrl(\`${endpointName}/\${id}\`), z.object({}), undefined, options, ),`, `listMetadata: ({ options, ...query }: Pick<QueryForListApi, "limit" | "offset"> & {options?: RequestOptions}) => requestGet( managementUrl(\`contents/${endpointName}\`), ${outputTypeSchemaName(endpointName)}, query, options, ),`, `getMetadata: ({ id, options, }: QueryForObjectApi & { id: string; options?: RequestOptions; }) => requestGet( managementUrl(\`contents/${endpointName}/\${id}\`), ListContentMetadataSchema, undefined, options, ),`, `patchStatus: ({id, status, options}: { id: string; status: "PUBLISH" | "DRAFT"; options?: RequestOptions; }) => requestWrite( "PATCH", managementUrl(\`contents/${endpointName}/\${id}/status\`), OnlyIdSchema, {status: [status]}, options, ),`, ]; }) .with("object", () => { return [ // NOTE: list is not supported for object type `get: (param?: QueryForObjectApi & { options?: RequestOptions; }) => { const { options, ...query } = param ?? {}; return requestGet( contentUrl(\`${endpointName}\`), ${defTypeSchemaName(endpointName)}.merge(MicroCmsObjectContentFieldsSchema), query, options, ); },`, // NOTE: post is not supported for object type // NOTE: put is not supported for object type `patch: ({content, options}: { content: Partial<${inputTypeName(endpointName)}>; options?: RequestOptions; }) => requestWrite( "PATCH", contentUrl(\`${endpointName}\`), OnlyIdSchema, content, options, ),`, `getMetadata: (params?: { options?: RequestOptions; }) => { const { options } = params ?? {}; return requestGet( managementUrl(\`contents/${endpointName}\`), ObjectContentMetadataSchema, undefined, options, ); },`, // NOTE: may be not supported (tried but got 403 error response). // `patchStatus: ({status, options}: { // status: "PUBLISH" | "DRAFT"; // options?: RequestOptions; // }) => { // return requestWrite( // "PATCH", // managementUrl(\`contents/${endpointName}/status\`), // // FIXME: fix schema // OnlyIdSchema, // {status: [status]}, // options, // ); // },`, ]; }) .exhaustive() .join("\n")} }`; })}}; };`, { parser: "typescript" }); }