@spotable/attio-sdk
Version:
Client for Attio REST API
184 lines (157 loc) • 7.21 kB
text/typescript
import * as commander from "commander";
import logger from "../helpers/logger";
import {
generateAttributeTypes,
generateBaseTypes,
generateCommentTypes,
generateCustomObjectTypes,
generateEntryTypes,
generateListTypes,
generateNoteTypes,
generateObjectTypes,
generateRecordTypes,
generateSdkUtilityTypes,
generateSystemDataTypes,
generateTaskTypes,
generateWebhookTypes,
generateWorkspaceMemberTypes,
} from "../generators/types";
import { ensureDirectoryExists, removeDirectoryRecursive } from "../helpers/fs";
import { fetchAttioSchema } from "../helpers/fetchAttioSchema";
import { join } from "node:path";
import { generateClient } from "../generators/client";
import ora from "ora";
import { generateIndexFile } from "../generators/indexFile";
import { generateNestjsFiles } from "../generators/nestjsFiles";
interface GenerateCommandOptions {
apiKeyEnvVar: string;
output: string;
standardTypes: boolean;
verbose: boolean;
nestjs: boolean;
}
type AttioTypeEntry = {
tsOutputType: string;
tsInputType?: string; // Optional type when the input type differs from the output type
isArray?: boolean;
} & (
| {
needsImport: false;
}
| {
needsImport: true;
importFrom: string;
}
);
const ATTIO_TYPE_MAP: Record<string, AttioTypeEntry> = {
text: { tsOutputType: "string", isArray: false, needsImport: false },
boolean: { tsOutputType: "boolean", isArray: false, needsImport: false },
number: { tsOutputType: "number", isArray: false, needsImport: false },
date: { tsOutputType: "Date", isArray: false, needsImport: false },
email: { tsOutputType: "string", isArray: false, needsImport: false },
phone: { tsOutputType: "string", isArray: false, needsImport: false },
currency: { tsOutputType: "number", isArray: false, needsImport: false },
dropdown: { tsOutputType: "string", isArray: false, needsImport: false },
url: { tsOutputType: "string", isArray: false, needsImport: false },
person: { tsOutputType: "string", isArray: false, needsImport: false },
company: { tsOutputType: "string", isArray: false, needsImport: false },
deal: { tsOutputType: "string", isArray: false, needsImport: false },
user: { tsOutputType: "string", isArray: false, needsImport: false },
list_entry: { tsOutputType: "string", isArray: false, needsImport: false },
object: { tsOutputType: "string", isArray: false, needsImport: false },
status: { tsOutputType: "string", isArray: false, needsImport: false },
"personal-name": { tsOutputType: "string", isArray: false, needsImport: false },
"email-address": { tsOutputType: "string", isArray: false, needsImport: false },
"phone-number": { tsOutputType: "PhoneNumber", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
location: { tsOutputType: "Location", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
domain: { tsOutputType: "Domain", tsInputType: "DomainInput", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
select: { tsOutputType: "SelectOption", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
"record-reference": { tsOutputType: "RecordReference", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
"actor-reference": { tsOutputType: "ActorReference", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
interaction: { tsOutputType: "Interaction", isArray: false, needsImport: true, importFrom: "./systemDataTypes" },
timestamp: { tsOutputType: "string", isArray: false, needsImport: false },
};
async function generateCommandHandler(options: GenerateCommandOptions): Promise<void> {
logger.level = options.verbose ? "debug" : "info";
const apiKey = process.env[options.apiKeyEnvVar];
if (!apiKey) {
logger.error(`API key not found in environment variable: ${options.apiKeyEnvVar}`);
process.exit(1);
}
// Remove existing output directory
removeDirectoryRecursive(options.output);
const clientOutputDir = options.output;
const typesOutputDir = join(options.output, "types");
const generateLoader = ora("Generating standard types").start();
// Recreate client and types directory
ensureDirectoryExists(typesOutputDir);
// Generate standard types if requested
if (options.standardTypes) {
generateBaseTypes(typesOutputDir);
generateAttributeTypes(typesOutputDir);
generateCommentTypes(typesOutputDir);
generateEntryTypes(typesOutputDir);
generateListTypes(typesOutputDir);
generateNoteTypes(typesOutputDir);
generateObjectTypes(typesOutputDir);
generateRecordTypes(typesOutputDir);
generateTaskTypes(typesOutputDir);
generateWebhookTypes(typesOutputDir);
generateWorkspaceMemberTypes(typesOutputDir);
}
const { objects, attributes } = await fetchAttioSchema(apiKey);
// Generate system data and SDK Utility types
generateSystemDataTypes(typesOutputDir);
generateSdkUtilityTypes(objects, options.standardTypes, typesOutputDir);
generateLoader.text = "Generating custom object types";
// Generate TypeScript types
for (const object of objects) {
const imports: Record<
string,
{
file: string;
types: Set<string>;
}
> = {};
const attributesWithTSTypes = (attributes[object.api_slug] || []).map((attribute) => {
const typeInfo = ATTIO_TYPE_MAP[attribute.type] || { tsOutputType: "any", needsImport: false };
if (typeInfo.needsImport) {
if (!imports[typeInfo.importFrom]) {
imports[typeInfo.importFrom] = {
file: typeInfo.importFrom,
types: new Set(),
};
}
imports[typeInfo.importFrom].types.add(typeInfo.tsOutputType);
if (typeInfo.tsInputType) {
imports[typeInfo.importFrom].types.add(typeInfo.tsInputType);
}
}
return {
...attribute,
tsOutputType: typeInfo.tsOutputType,
tsInputType: typeInfo.tsInputType || typeInfo.tsOutputType,
isArray: typeInfo.isArray || attribute.is_multiselect,
};
});
generateCustomObjectTypes(typesOutputDir, { object, attributes: attributesWithTSTypes, imports: Object.values(imports) });
}
generateIndexFile(typesOutputDir);
generateLoader.succeed(`TypeScript types generated successfully in ${typesOutputDir}`);
generateClient(clientOutputDir, objects, options.standardTypes);
if (options.nestjs) {
generateNestjsFiles(options.output);
}
generateIndexFile(clientOutputDir);
}
export default function registerGenerateCommand(program: commander.Command): void {
program
.command("generate")
.description("Generate a TypeScript client from your Attio workspace")
.option("-a, --api-key-env-var <key>", "Environment variable containing the Attio API key", "ATTIO_API_KEY")
.option("-o, --output <dir>", "Output directory for generated types", "./attio")
.option("-s, --standard-types", "Generate types for standard Attio entities (lists, notes, tasks, etc.)", false)
.option("-n, --nestjs", "Generate NestJS-style client", false)
.option("--verbose", "Enable verbose logging", false)
.action(generateCommandHandler);
}