import { type Class, type Doc, type DocIndexState, docKey, type Hierarchy, type Ref, type RefTo, type SearchResultDoc } from '@hcengineering/core' import { getResource, type Resource } from '@hcengineering/platform' import plugin from './plugin' import { type ClassSearchConfigProps, type IndexedDoc, type SearchPresenter, type SearchPresenterFunc, type SearchScoring } from './types' interface IndexedReader { get: (attribute: string) => any getDoc: (attribute: string) => IndexedReader | undefined } // TODO: Rework to use mongo function createIndexedReader ( _class: Ref>, hierarchy: Hierarchy, doc: DocIndexState, otherDocs?: Record, refAttribute?: string ): IndexedReader { return { get: (attr: string) => { const realAttr = hierarchy.findAttribute(_class, attr) if (realAttr !== undefined) { return doc.attributes[docKey(attr, { refAttribute, _class: realAttr.attributeOf })] } return undefined }, getDoc: (attr: string) => { const realAttr = hierarchy.findAttribute(_class, attr) if (realAttr !== undefined) { const anotherDoc = otherDocs?.[attr] if (anotherDoc !== undefined) { const refAtrr = realAttr.type as RefTo return createIndexedReader(refAtrr.to, hierarchy, anotherDoc, otherDocs, docKey(attr, { _class })) } } return undefined } } } async function readAndMapProps ( reader: IndexedReader, props: ClassSearchConfigProps[], searchProvider?: { hierarchy: Hierarchy providers: SearchPresenterFunc } ): Promise> { const res: Record = {} for (const prop of props) { if (typeof prop === 'string') { res[prop] = reader.get(prop) } else { for (const [propName, rest] of Object.entries(prop)) { if (rest.length > 1) { const val = reader.getDoc(rest[0])?.get(rest[1]) const v = Array.isArray(val) ? val[0] : val if (searchProvider !== undefined) { const func = searchProvider.providers !== undefined && Object.keys(searchProvider.providers).includes(propName) ? ((await getResource(searchProvider.providers[propName])) as any) : undefined if (func !== undefined) { res[propName] = func(searchProvider.hierarchy, { _class: res?._class, [propName]: v }) continue } } res[propName] = v } } } } return res } function findSearchPresenter (hierarchy: Hierarchy, _class: Ref>): SearchPresenter | undefined { const ancestors = hierarchy.getAncestors(_class).reverse() for (const _class of ancestors) { const searchMixin = hierarchy.classHierarchyMixin(_class, plugin.mixin.SearchPresenter) if (searchMixin !== undefined) { return searchMixin } } return undefined } /** * @public */ export async function updateDocWithPresenter ( hierarchy: Hierarchy, doc: DocIndexState, elasticDoc: IndexedDoc, refDocs: { parentDoc: DocIndexState | undefined spaceDoc: DocIndexState | undefined } ): Promise { const searchPresenter = findSearchPresenter(hierarchy, elasticDoc._class) if (searchPresenter === undefined) { return } const reader = createIndexedReader(elasticDoc._class, hierarchy, doc, { space: refDocs.spaceDoc, attachedTo: refDocs.parentDoc }) const props = [ { name: 'searchTitle', config: searchPresenter.searchConfig.title, provider: searchPresenter.getSearchTitle } ] as any[] if (searchPresenter.searchConfig.shortTitle !== undefined) { props.push({ name: 'searchShortTitle', config: searchPresenter.searchConfig.shortTitle, provider: searchPresenter.getSearchShortTitle }) } if (searchPresenter.searchConfig.iconConfig !== undefined) { props.push({ name: 'searchIcon', config: searchPresenter.searchConfig.iconConfig }) } for (const prop of props) { let value if (prop.config.tmpl !== undefined) { const tmpl = prop.config.tmpl const renderProps = await readAndMapProps(reader, prop.config.props, { hierarchy, providers: prop.provider }) value = fillTemplate(tmpl, renderProps) } else if (typeof prop.config === 'string') { value = reader.get(prop.config) } else if (prop.provider !== undefined) { const func = await getResource(Object.values(prop.provider)[0] as Resource) const renderProps = await readAndMapProps(reader, prop.config.props) value = func(hierarchy, { _class: elasticDoc._class, ...renderProps }) } else if (prop.name === 'searchIcon') { value = await readAndMapProps(reader, prop.config.props) } elasticDoc[prop.name] = value } } export function getScoringConfig (hierarchy: Hierarchy, classes: Ref>[]): SearchScoring[] { let results: SearchScoring[] = [] for (const _class of classes) { const searchPresenter = findSearchPresenter(hierarchy, _class) if (searchPresenter?.searchConfig.scoring !== undefined) { results = results.concat(searchPresenter?.searchConfig.scoring) } } return results } /** * @public */ export function mapSearchResultDoc (hierarchy: Hierarchy, raw: IndexedDoc): SearchResultDoc { const doc: SearchResultDoc = { id: raw.id, title: raw.searchTitle, shortTitle: raw.searchShortTitle, iconProps: raw.searchIcon, doc: { _id: raw.id, _class: raw._class }, score: raw._score } const searchPresenter = findSearchPresenter(hierarchy, doc.doc._class) if (searchPresenter?.searchConfig.icon !== undefined) { doc.icon = searchPresenter.searchConfig.icon } if (searchPresenter?.searchConfig.iconConfig !== undefined) { doc.iconComponent = searchPresenter.searchConfig.iconConfig.component } return doc } function fillTemplate (tmpl: string, props: Record): string { return tmpl.replace(/{(.*?)}/g, (_, key: string) => props[key]) }