import { Analytics } from '@hcengineering/analytics' import { toFindResult, type Class, type Client, type Doc, type DocumentQuery, type FindOptions, type FindResult, type Hierarchy, type ModelDb, type Ref, type SearchOptions, type SearchQuery, type SearchResult, type Tx, type TxResult, type WithLookup } from '@hcengineering/core' import { setPlatformStatus, unknownError, type Resource } from '@hcengineering/platform' /** * @public */ export interface PresentationMiddleware { next?: PresentationMiddleware tx: (tx: Tx) => Promise<TxResult> notifyTx: (...tx: Tx[]) => Promise<void> findAll: <T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ) => Promise<FindResult<T>> findOne: <T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ) => Promise<WithLookup<T> | undefined> subscribe: <T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options: FindOptions<T> | undefined, refresh: () => void ) => Promise<{ unsubscribe: () => void query?: DocumentQuery<T> options?: FindOptions<T> }> close: () => Promise<void> } /** * @public */ export type PresentationMiddlewareCreator = (client: Client, next?: PresentationMiddleware) => PresentationMiddleware /** * @public */ export interface PresentationPipeline extends Client, Exclude<PresentationMiddleware, 'next'> { close: () => Promise<void> } /** * @public */ export class PresentationPipelineImpl implements PresentationPipeline { private head: PresentationMiddleware | undefined private constructor (readonly client: Client) {} getHierarchy (): Hierarchy { return this.client.getHierarchy() } getModel (): ModelDb { return this.client.getModel() } async notifyTx (...tx: Tx[]): Promise<void> { await this.head?.notifyTx(...tx) } static create (client: Client, constructors: PresentationMiddlewareCreator[]): PresentationPipeline { const pipeline = new PresentationPipelineImpl(client) pipeline.head = pipeline.buildChain(constructors) return pipeline } private buildChain (constructors: PresentationMiddlewareCreator[]): PresentationMiddleware | undefined { let current: PresentationMiddleware | undefined for (let index = constructors.length - 1; index >= 0; index--) { const element = constructors[index] current = element(this.client, current) } return current } async findAll<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<FindResult<T>> { try { return this.head !== undefined ? await this.head.findAll(_class, query, options) : await this.client.findAll(_class, query, options) } catch (err) { Analytics.handleError(err as Error) const status = unknownError(err) await setPlatformStatus(status) return toFindResult([], -1) } } async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> { return await this.client.searchFulltext(query, options) } async findOne<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<WithLookup<T> | undefined> { try { return this.head !== undefined ? await this.head.findOne(_class, query, options) : await this.client.findOne(_class, query, options) } catch (err) { Analytics.handleError(err as Error) const status = unknownError(err) await setPlatformStatus(status) } } async subscribe<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options: FindOptions<T> | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery<T> options?: FindOptions<T> }> { return this.head !== undefined ? await this.head.subscribe(_class, query, options, refresh) : { unsubscribe: () => {} } } async tx (tx: Tx): Promise<TxResult> { try { if (this.head === undefined) { return await this.client.tx(tx) } else { return await this.head.tx(tx) } } catch (err) { Analytics.handleError(err as Error) const status = unknownError(err) await setPlatformStatus(status) return {} } } async close (): Promise<void> { if (this.head !== undefined) { await this.head.close() return } await this.client.close() } } /** * @public */ export abstract class BasePresentationMiddleware { constructor ( protected readonly client: Client, readonly next?: PresentationMiddleware ) {} async provideNotifyTx (...tx: Tx[]): Promise<void> { await this.next?.notifyTx(...tx) } async provideClose (): Promise<void> { if (this.next !== undefined) { await this.next.close() return } await this.client.close() } async findAll<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<FindResult<T>> { return await this.provideFindAll(_class, query, options) } async findOne<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<WithLookup<T> | undefined> { return await this.provideFindOne(_class, query, options) } async subscribe<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options: FindOptions<T> | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery<T> options?: FindOptions<T> }> { return await this.provideSubscribe(_class, query, options, refresh) } protected async provideTx (tx: Tx): Promise<TxResult> { if (this.next !== undefined) { return await this.next.tx(tx) } return await this.client.tx(tx) } protected async provideFindAll<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<FindResult<T>> { if (this.next !== undefined) { return await this.next.findAll(_class, query, options) } return await this.client.findAll(_class, query, options) } protected async provideFindOne<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> ): Promise<WithLookup<T> | undefined> { if (this.next !== undefined) { return await this.next.findOne(_class, query, options) } return await this.client.findOne(_class, query, options) } protected async provideSubscribe<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options: FindOptions<T> | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery<T> options?: FindOptions<T> }> { if (this.next !== undefined) { return await this.next.subscribe(_class, query, options, refresh) } return { unsubscribe: () => {} } } } /** * @public */ export interface PresentationMiddlewareFactory extends Doc { createPresentationMiddleware: Resource<PresentationMiddlewareCreator> } /** * @public */ export class OptimizeQueryMiddleware extends BasePresentationMiddleware implements PresentationMiddleware { private constructor (client: Client, next?: PresentationMiddleware) { super(client, next) } static create (client: Client, next?: PresentationMiddleware): OptimizeQueryMiddleware { return new OptimizeQueryMiddleware(client, next) } async notifyTx (...tx: Tx[]): Promise<void> { await this.provideNotifyTx(...tx) } async close (): Promise<void> { await this.provideClose() } async tx (tx: Tx): Promise<TxResult> { return await this.provideTx(tx) } async subscribe<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options: FindOptions<T> | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery<T> options?: FindOptions<T> }> { return await this.provideSubscribe(_class, query, options, refresh) } async findAll<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> | undefined ): Promise<FindResult<T>> { if (_class == null || typeof query !== 'object' || ('_class' in query && query._class == null)) { console.error('_class must be specified in query', query) return toFindResult([], 0) } const fQuery = { ...query } const fOptions = { ...options } this.optimizeQuery<T>(fQuery, fOptions) return await this.provideFindAll(_class, fQuery, fOptions) } private optimizeQuery<T extends Doc>(fQuery: DocumentQuery<T>, fOptions: FindOptions<T>): void { if (typeof fQuery._id === 'string' && fOptions.sort !== undefined) { delete fOptions.sort } if (fOptions.lookup !== undefined && Object.keys(fOptions.lookup).length === 0) { delete fOptions.lookup } } async findOne<T extends Doc>( _class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T> | undefined ): Promise<WithLookup<T> | undefined> { const fQuery = { ...query } const fOptions = { ...options } this.optimizeQuery<T>(fQuery, fOptions) return await this.provideFindOne(_class, fQuery, fOptions) } }