import { ClarizenApiError } from "../errors.js"; import { ENDPOINTS } from "../endpoints.js"; import type { HttpClient } from "../http.js"; import { filterStandardTypeNames, sanitizeEntityDescription, } from "./filter.js"; import type { DescribeMetadataResult, EntityMetadataEntry, FetchMetadataCatalogOptions, ListEntitiesResult, MetadataCatalog, MetadataFlag, } from "./types.js"; const DEFAULT_FLAGS: MetadataFlag[] = ["fields", "relations"]; const DEFAULT_BATCH_SIZE = 20; const DESCRIBE_METADATA_PATH = ENDPOINTS.find( (e) => e.namespace === "metadata" && e.operation === "DescribeMetadata", )?.path ?? "/V2.0/services/metadata/describeMetadata"; const LIST_ENTITIES_PATH = ENDPOINTS.find( (e) => e.namespace === "metadata" && e.operation === "ListEntities", )?.path ?? "/V2.0/services/metadata/listEntities"; function describeMetadataQuery( typeNames: string[] | undefined, flags: MetadataFlag[], ): Record { const query: Record = { flags, }; if (typeNames !== undefined && typeNames.length > 0) { query.typeNames = typeNames; } return query; } async function listEntities(http: HttpClient): Promise { const result = await http.request(LIST_ENTITIES_PATH, { method: "GET", }); return result.typeNames ?? []; } async function describeMetadata( http: HttpClient, typeNames: string[] | undefined, flags: MetadataFlag[], ): Promise { const result = await http.request( DESCRIBE_METADATA_PATH, { method: "GET", query: describeMetadataQuery(typeNames, flags), }, ); const entries: EntityMetadataEntry[] = []; for (const desc of result.entityDescriptions ?? []) { const entry = sanitizeEntityDescription(desc); if (entry) { entries.push(entry); } } return entries; } function chunk(items: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < items.length; i += size) { chunks.push(items.slice(i, i + size)); } return chunks; } function mergeEntries( batches: EntityMetadataEntry[][], ): EntityMetadataEntry[] { const byType = new Map(); for (const batch of batches) { for (const entry of batch) { byType.set(entry.typeName, entry); } } return [...byType.values()].sort((a, b) => a.typeName.localeCompare(b.typeName), ); } function needsBatchedFetch(error: unknown): boolean { if (error instanceof ClarizenApiError) { return ( error.errorCode === "MissingArgument" || error.errorCode === "InvalidArgument" ); } if (error instanceof Error) { const msg = error.message.toLowerCase(); return msg.includes("missing") || msg.includes("typenames"); } return false; } async function fetchBatched( http: HttpClient, flags: MetadataFlag[], batchSize: number, log: (message: string) => void, ): Promise { log("listEntities + batched describeMetadata..."); const rawNames = await listEntities(http); const typeNames = filterStandardTypeNames(rawNames); log(` ${typeNames.length} standard entities (${rawNames.length} total)`); const batches = chunk(typeNames, batchSize); const batchResults: EntityMetadataEntry[][] = []; for (let i = 0; i < batches.length; i++) { const batch = batches[i]!; log(` batch ${i + 1}/${batches.length} (${batch.length} types)...`); batchResults.push(await describeMetadata(http, batch, flags)); } return mergeEntries(batchResults); } export async function fetchMetadataCatalog( http: HttpClient, options: FetchMetadataCatalogOptions = {}, ): Promise { const flags = options.flags ?? DEFAULT_FLAGS; const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE; const log = options.onProgress ?? (() => {}); let entries: EntityMetadataEntry[]; try { log("describeMetadata (all entities)..."); entries = await describeMetadata(http, undefined, flags); if (entries.length === 0) { entries = await fetchBatched(http, flags, batchSize, log); } } catch (error) { if (needsBatchedFetch(error)) { entries = await fetchBatched(http, flags, batchSize, log); } else { throw error; } } return { fetchedAt: new Date().toISOString(), entities: entries, }; }