diff --git a/lib/services/analyzer/aggregate-persistence.ts b/lib/services/analyzer/aggregate-persistence.ts index 725c56f..ad803b7 100644 --- a/lib/services/analyzer/aggregate-persistence.ts +++ b/lib/services/analyzer/aggregate-persistence.ts @@ -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') diff --git a/lib/services/analyzer/itglue-search.ts b/lib/services/analyzer/itglue-search.ts index 3ce2b83..fdfb405 100644 --- a/lib/services/analyzer/itglue-search.ts +++ b/lib/services/analyzer/itglue-search.ts @@ -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}`; diff --git a/lib/services/itglue-client.ts b/lib/services/itglue-client.ts index 6cb1667..13e507a 100644 --- a/lib/services/itglue-client.ts +++ b/lib/services/itglue-client.ts @@ -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 | null = null; + private async cachedFlexibleAssetTypes(): Promise { + 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 { + 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 {