import {IArticle, IListViewFieldWithOptions, IRestApiResponse, IBaseRestApiResponse} from 'superdesk-api'; import {appConfig} from 'appConfig'; import {DEFAULT_LIST_CONFIG} from 'apps/search/constants'; import {flatMap} from 'lodash'; import {fields} from 'apps/search/components/fields'; import {httpRequestJsonLocal} from './helpers/network'; import {Set, Map} from 'immutable'; import {notNullOrUndefined} from './helpers/typescript-helpers'; import {ignoreAbortError} from './SuperdeskReactComponent'; /** * Holds Maps of entities keyed by IDs. */ export type IRelatedEntities = {[collectionName: string]: Map}; type IEntitiesToFetch = {[collectionName: string]: Set}; function mergeRelatedEntities(a: IRelatedEntities, b: IRelatedEntities): IRelatedEntities { const next: IRelatedEntities = {...a}; Object.keys(b).forEach((entityName) => { if (next[entityName] == null) { next[entityName] = b[entityName]; } else { next[entityName] = next[entityName].merge(b[entityName]); } }); return next; } export function getAndMergeRelatedEntitiesForArticles( items: Array, alreadyFetched: IRelatedEntities, abortSignal: AbortSignal, ): Promise { const listConfig = appConfig.list ?? DEFAULT_LIST_CONFIG; const configuredFields: Array = [] .concat(listConfig.priority ?? []) .concat(listConfig.firstLine ?? []) .concat(listConfig.secondLine ?? []); const relatedEntitiesConfigGetterFunctions = flatMap(configuredFields, (f) => { const field = typeof f === 'string' ? f : f.field; const component = fields[field]; return component?.getRelatedEntities; }).filter(notNullOrUndefined); // ids indexed by collection name const entitiesToFetch: IEntitiesToFetch = {}; items.forEach((item) => { relatedEntitiesConfigGetterFunctions.forEach((fn) => { fn(item).forEach(({collection, id}) => { if (id != null && !alreadyFetched[collection]?.has(id)) { if (entitiesToFetch[collection] == null) { entitiesToFetch[collection] = Set(); } entitiesToFetch[collection] = entitiesToFetch[collection].add(id); } }); }); }); return fetchRelatedEntities(entitiesToFetch, abortSignal) .then((result) => mergeRelatedEntities(alreadyFetched, result)); } export interface IResourceChange { changeType: 'created' | 'updated' | 'deleted'; resource: string; itemId: string; fields?: {[key: string]: 1}; } export function getAndMergeRelatedEntitiesUpdated( currentEntities: IRelatedEntities, changes: Array, abortSignal: AbortSignal, ): Promise { const changesToRelatedEntities = changes.filter( ({changeType, resource, itemId}) => changeType !== 'deleted' && currentEntities[resource]?.get(itemId) != null, ); const entitiesToFetch = changesToRelatedEntities.reduce((acc, change) => { if (acc[change.resource] == null) { acc[change.resource] = Set(); } acc[change.resource] = acc[change.resource].add(change.itemId); return acc; }, {}); if (Object.keys(entitiesToFetch).length > 0) { return fetchRelatedEntities(entitiesToFetch, abortSignal) .then((result) => mergeRelatedEntities(currentEntities, result)); } else { return Promise.resolve(currentEntities); } } export function fetchRelatedEntities( entitiesToFetch: IEntitiesToFetch, abortSignal: AbortSignal, ): Promise { return new Promise((resolve) => { const result: IRelatedEntities = {}; Promise.all( Object.keys(entitiesToFetch).map((collection) => { const ids: Array = entitiesToFetch[collection].toJS(); return ignoreAbortError( httpRequestJsonLocal>({ method: 'GET', path: `/${collection}?where=${JSON.stringify({_id: {$in: ids}})}`, abortSignal, }), ).then((response) => { result[collection] = Map(); response._items.forEach((entity) => { result[collection] = result[collection].set(entity._id, entity); }); }); }), ).then(() => { resolve(result); }); }); }