Files
clarizen-lib/src/metadata/catalog.ts
T

161 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-06-04 12:09:45 -03:00
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<string, string | string[] | undefined> {
const query: Record<string, string | string[] | undefined> = {
flags,
};
if (typeNames !== undefined && typeNames.length > 0) {
query.typeNames = typeNames;
}
return query;
}
async function listEntities(http: HttpClient): Promise<string[]> {
const result = await http.request<ListEntitiesResult>(LIST_ENTITIES_PATH, {
method: "GET",
});
return result.typeNames ?? [];
}
async function describeMetadata(
http: HttpClient,
typeNames: string[] | undefined,
flags: MetadataFlag[],
): Promise<EntityMetadataEntry[]> {
const result = await http.request<DescribeMetadataResult>(
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<T>(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<string, EntityMetadataEntry>();
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<EntityMetadataEntry[]> {
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<MetadataCatalog> {
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,
};
}