apiful
Version:
Extensible, typed API tooling
177 lines (164 loc) • 6.98 kB
JavaScript
import { defu } from 'defu';
import { pascalCase } from 'scule';
import { C as CODE_HEADER_DIRECTIVES } from './apiful.B_nvMJ_g.mjs';
async function generateDTS(services, openAPITSOptions) {
const resolvedSchemaEntries = await Promise.all(
Object.entries(services).filter(([, service]) => Boolean(service.schema)).map(async ([id, service]) => {
const types = await generateSchemaTypes({ id, service, openAPITSOptions });
return [id, types];
})
);
const resolvedSchemas = Object.fromEntries(resolvedSchemaEntries);
const serviceIds = Object.keys(resolvedSchemas);
const imports = serviceIds.map((id) => ` import { paths as ${pascalCase(id)}Paths, operations as ${pascalCase(id)}Operations } from 'apiful/__${id}__'`).join("\n");
const repositoryEntries = serviceIds.map((id) => ` ${id}: ${pascalCase(id)}Paths`).join("\n");
const typeExports = serviceIds.map((id) => {
return [`
export type ${pascalCase(id)}Response<
T extends keyof ${pascalCase(id)}Operations,
R extends keyof ${pascalCase(id)}Operations[T]['responses'] = 200 extends keyof ${pascalCase(id)}Operations[T]['responses'] ? 200 : never
> = ${pascalCase(id)}Operations[T]['responses'][R] extends { content: { 'application/json': infer U } } ? U : never
export type ${pascalCase(id)}RequestBody<
T extends keyof ${pascalCase(id)}Operations
> = ${pascalCase(id)}Operations[T]['requestBody'] extends { content: { 'application/json': infer U } } ? U : never
export type ${pascalCase(id)}RequestQuery<
T extends keyof ${pascalCase(id)}Operations
> = ${pascalCase(id)}Operations[T]['parameters'] extends { query?: infer U } ? U : never
// Helper type to get the operation from a path entry
export type GetOperation<T, M extends string> =
M extends 'get' ? T extends { get: infer Op } ? Op : never :
M extends 'post' ? T extends { post: infer Op } ? Op : never :
M extends 'put' ? T extends { put: infer Op } ? Op : never :
M extends 'delete' ? T extends { delete: infer Op } ? Op : never :
M extends 'patch' ? T extends { patch: infer Op } ? Op : never :
never
// Direct type that allows accessing path parameters by specifying the HTTP method
export type PathParamsFrom${pascalCase(id)}<
P extends keyof ${pascalCase(id)}Paths,
M extends NonNeverKeysWithoutParams<${pascalCase(id)}Paths[P]>
> = GetOperation<${pascalCase(id)}Paths[P], M> extends infer Op
? Op extends { parameters?: any }
? NonNullable<Op['parameters']>['path'] extends infer Params
? Params extends object
? Params
: Record<string, never>
: Record<string, never>
: Record<string, never>
: Record<string, never>
// Direct type that allows accessing request body by specifying the HTTP method
export type RequestBodyFrom${pascalCase(id)}<
P extends keyof ${pascalCase(id)}Paths,
M extends NonNeverKeysWithoutParams<${pascalCase(id)}Paths[P]>
> = GetOperation<${pascalCase(id)}Paths[P], M> extends infer Op
? Op extends { requestBody?: any }
? NonNullable<Op['requestBody']>['content']['application/json'] extends infer Body
? Body extends object
? Body
: Record<string, never>
: Record<string, never>
: Record<string, never>
: Record<string, never>
// Direct type that allows accessing query parameters by specifying the HTTP method
export type QueryParamsFrom${pascalCase(id)}<
P extends keyof ${pascalCase(id)}Paths,
M extends NonNeverKeysWithoutParams<${pascalCase(id)}Paths[P]>
> = GetOperation<${pascalCase(id)}Paths[P], M> extends infer Op
? Op extends { parameters?: any }
? NonNullable<Op['parameters']>['query'] extends infer Params
? Params extends object
? Params
: Record<string, never>
: Record<string, never>
: Record<string, never>
: Record<string, never>
// Direct type that allows accessing response body by specifying the HTTP method
export type ResponseFrom${pascalCase(id)}<
P extends keyof ${pascalCase(id)}Paths,
M extends NonNeverKeysWithoutParams<${pascalCase(id)}Paths[P]>,
C extends \`\${keyof NonNullable<GetOperation<${pascalCase(id)}Paths[P], M>>['responses']}\` = '200'
> = GetOperation<${pascalCase(id)}Paths[P], M> extends infer Op
? Op extends { responses?: any }
? ParseInt<C> extends keyof Op['responses']
? Op['responses'][ParseInt<C>] extends { content: { 'application/json': infer Body } }
? Body
: Record<string, never>
: Record<string, never>
: Record<string, never>
: Record<string, never>
`.trim()].join("\n");
}).join("\n\n");
const moduleDeclarations = Object.entries(resolvedSchemas).map(([id, types]) => `declare module 'apiful/__${id}__' {
${normalizeIndentation(types).trimEnd()}
}`).join("\n\n");
return `
${CODE_HEADER_DIRECTIVES}
declare module 'apiful/schema' {
${imports}
type NonNeverKeys<T> = {
[K in keyof T]: [T[K]] extends [never]
? never
: [undefined] extends [T[K]]
? [never] extends [Exclude<T[K], undefined>] ? never : K
: K;
}[keyof T];
type NonNeverKeysWithoutParams<T> = Exclude<NonNeverKeys<T>, 'parameters'>
type ParseInt<S extends string> = S extends \`\${infer N extends number}\` ? N : never
interface OpenAPISchemaRepository {
${repositoryEntries}
}
${applyLineIndent(typeExports)}
}
${moduleDeclarations}
`.trimStart();
}
async function generateSchemaTypes(options) {
const { default: openAPITS, astToString } = await import('openapi-typescript').catch(() => {
throw new Error('Missing dependency "openapi-typescript", please install it');
});
const schema = await resolveSchema(options.service);
const resolvedOpenAPITSOptions = defu(options.service.openAPITS, options.openAPITSOptions || {});
try {
const ast = await openAPITS(schema, resolvedOpenAPITSOptions);
return astToString(ast);
} catch (error) {
console.error(`Failed to generate types for ${options.id}`);
console.error(error);
return `
export type paths = Record<string, never>
export type webhooks = Record<string, never>
export interface components {
schemas: never
responses: never
parameters: never
requestBodies: never
headers: never
pathItems: never
}
export type $defs = Record<string, never>
export type operations = Record<string, never>
`.trimStart();
}
}
async function resolveSchema({ schema }) {
if (typeof schema === "function")
return await schema();
if (typeof schema === "string")
return isValidUrl(schema) ? schema : new URL(schema, import.meta.url);
return schema;
}
function isValidUrl(url) {
try {
return Boolean(new URL(url));
} catch {
return false;
}
}
function applyLineIndent(code) {
return code.split("\n").map((line) => line.replace(/^/gm, " ")).join("\n");
}
function normalizeIndentation(code) {
const replacedCode = code.replace(/^( {4})+/gm, (match) => " ".repeat(match.length / 4));
const normalizedCode = replacedCode.replace(/^/gm, " ");
return normalizedCode;
}
export { generateDTS as g };