fix(itglue): list flexible assets per type to satisfy API 422 requirement

IT Glue's /flexible_assets endpoint refuses requests without a
filter[flexible-asset-type-id] (returns 422 "Cannot index flexible
assets without providing a flexible asset type ID filter"). The
analyzer's Stage 2 search was caught and tolerated, but never returned
docs.

Added getFlexibleAssetsForOrganization(orgId) on ITGlueClient. It
fetches the type list once per process (memoized), then fans out
per-type fetches with Promise.allSettled so a permission-restricted
type doesn't poison the whole org. Wired into itglue-search and
aggregate-persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-04-29 14:48:53 -04:00
parent 9acf48e78a
commit a0a6e7f192
3 changed files with 48 additions and 2 deletions

View file

@ -237,7 +237,7 @@ async function fetchITGlueDocTitles(
try {
const org = await client.findOrganizationByName(name);
if (!org) continue;
const docs = await client.getFlexibleAssets({ organizationId: org.id });
const docs = await client.getFlexibleAssetsForOrganization(org.id);
const titles = docs
.map((d) => (d as { name?: string }).name)
.filter((t): t is string => typeof t === 'string')

View file

@ -153,7 +153,7 @@ export async function itglueSearch(
// Flexible assets — runbooks, integrations, app-specific docs.
try {
const flex = await client.getFlexibleAssets({ organizationId: resolved.org_id });
const flex = await client.getFlexibleAssetsForOrganization(resolved.org_id);
for (const a of flex) {
if (docs.length >= MAX_DOCS_RETURNED) break;
const dedupeKey = `flex:${a.id}`;

View file

@ -286,6 +286,52 @@ export class ITGlueClient {
}));
}
/**
* Cached, per-instance fetch of enabled flexible asset type ids. IT Glue's
* /flexible_assets endpoint requires a flexibleAssetTypeId filter (otherwise
* 422). This caches the type list so callers don't pay the lookup on every
* call. Cache lives for the life of the process types rarely change.
*/
private flexibleAssetTypesCache: Promise<ITGlueFlexibleAssetType[]> | null = null;
private async cachedFlexibleAssetTypes(): Promise<ITGlueFlexibleAssetType[]> {
if (!this.flexibleAssetTypesCache) {
this.flexibleAssetTypesCache = this.getFlexibleAssetTypes().catch((err) => {
// Re-throw next call so a transient failure isn't sticky.
this.flexibleAssetTypesCache = null;
throw err;
});
}
return this.flexibleAssetTypesCache;
}
/**
* Get every flexible asset for a given organization across all enabled types.
* IT Glue's /flexible_assets endpoint requires a per-type filter (the
* `getFlexibleAssets` raw call returns 422 otherwise), so this helper
* enumerates types and fans out per-type requests in parallel. Per-type
* failures are tolerated so a single bad type doesn't poison the whole org.
*/
async getFlexibleAssetsForOrganization(
organizationId: number | string
): Promise<ITGlueFlexibleAsset[]> {
const types = await this.cachedFlexibleAssetTypes();
const enabled = types.filter((t) => t.enabled);
const results = await Promise.allSettled(
enabled.map((t) =>
this.getFlexibleAssets({
organizationId,
flexibleAssetTypeId: t.id,
})
)
);
const out: ITGlueFlexibleAsset[] = [];
for (const r of results) {
if (r.status === 'fulfilled') out.push(...r.value);
// Tolerate per-type failures — common for permission-restricted types.
}
return out;
}
// ─── Configurations ───────────────────────────────────────────────────────
private mapConfiguration(item: any): ITGlueConfiguration {