first commit
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user