platform/packages/presentation/src/pipeline.ts
Andrey Sobolev 1c965209a7
Fix load of applications in tables/lists/kanban (#6231)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2024-08-02 21:14:49 +07:00

373 lines
9.9 KiB
TypeScript

import { Analytics } from '@hcengineering/analytics'
import {
toFindResult,
type Class,
type Client,
type Doc,
type DocumentQuery,
type FindOptions,
type FindResult,
type Hierarchy,
type ModelDb,
type QuerySelector,
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)
// Immidiate response queries, if have some $in with empty list.
for (const [k, v] of Object.entries(fQuery)) {
if (typeof v === 'object' && v != null) {
const vobj = v as QuerySelector<any>
if (vobj.$in != null && vobj.$in.length === 0) {
// Emopty in, will always return []
return toFindResult([], 0)
} else if (vobj.$in != null && vobj.$in.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = vobj.$in[0]
} else if (vobj.$nin != null && vobj.$nin.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = { $ne: vobj.$nin[0] }
}
}
}
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)
}
}