import { Analytics } from '@hcengineering/analytics' import { toFindResult, type Class, type Client, type Doc, type DocumentQuery, type FindOptions, type FindResult, type Hierarchy, type MeasureClient, type MeasureDoneOperation, 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 notifyTx: (...tx: Tx[]) => Promise findAll: ( _class: Ref>, query: DocumentQuery, options?: FindOptions ) => Promise> findOne: ( _class: Ref>, query: DocumentQuery, options?: FindOptions ) => Promise | undefined> subscribe: ( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ) => Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> close: () => Promise } /** * @public */ export type PresentationMiddlewareCreator = (client: Client, next?: PresentationMiddleware) => PresentationMiddleware /** * @public */ export interface PresentationPipeline extends MeasureClient, Exclude { close: () => Promise } /** * @public */ export class PresentationPipelineImpl implements PresentationPipeline { private head: PresentationMiddleware | undefined private constructor (readonly client: MeasureClient) {} getHierarchy (): Hierarchy { return this.client.getHierarchy() } getModel (): ModelDb { return this.client.getModel() } async notifyTx (...tx: Tx[]): Promise { await this.head?.notifyTx(...tx) } async measure (operationName: string): Promise { return await this.client.measure(operationName) } static create (client: MeasureClient, 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( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { 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 { return await this.client.searchFulltext(query, options) } async findOne( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise | 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( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> { return this.head !== undefined ? await this.head.subscribe(_class, query, options, refresh) : { unsubscribe: () => {} } } async tx (tx: Tx): Promise { 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 { 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 { await this.next?.notifyTx(...tx) } async provideClose (): Promise { if (this.next !== undefined) { await this.next.close() return } await this.client.close() } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { return await this.provideFindAll(_class, query, options) } async findOne( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise | undefined> { return await this.provideFindOne(_class, query, options) } async subscribe( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> { return await this.provideSubscribe(_class, query, options, refresh) } protected async provideTx (tx: Tx): Promise { if (this.next !== undefined) { return await this.next.tx(tx) } return await this.client.tx(tx) } protected async provideFindAll( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { if (this.next !== undefined) { return await this.next.findAll(_class, query, options) } return await this.client.findAll(_class, query, options) } protected async provideFindOne( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise | undefined> { if (this.next !== undefined) { return await this.next.findOne(_class, query, options) } return await this.client.findOne(_class, query, options) } protected async provideSubscribe( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> { if (this.next !== undefined) { return await this.next.subscribe(_class, query, options, refresh) } return { unsubscribe: () => {} } } } /** * @public */ export interface PresentationMiddlewareFactory extends Doc { createPresentationMiddleware: Resource } /** * @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 { await this.provideNotifyTx(...tx) } async close (): Promise { await this.provideClose() } async tx (tx: Tx): Promise { return await this.provideTx(tx) } async subscribe( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> { return await this.provideSubscribe(_class, query, options, refresh) } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions | undefined ): Promise> { 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(fQuery, fOptions) return await this.provideFindAll(_class, fQuery, fOptions) } private optimizeQuery(fQuery: DocumentQuery, fOptions: FindOptions): 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( _class: Ref>, query: DocumentQuery, options?: FindOptions | undefined ): Promise | undefined> { const fQuery = { ...query } const fOptions = { ...options } this.optimizeQuery(fQuery, fOptions) return await this.provideFindOne(_class, fQuery, fOptions) } }