2023-06-06 13:06:32 +03:00
|
|
|
import core, {
|
2023-11-20 13:01:43 +03:00
|
|
|
type Doc,
|
|
|
|
type Ref,
|
|
|
|
type AnyAttribute,
|
|
|
|
type Class,
|
|
|
|
type DocumentQuery,
|
|
|
|
type FindOptions,
|
|
|
|
type Client,
|
|
|
|
type Tx,
|
|
|
|
type TxResult,
|
|
|
|
type FindResult,
|
|
|
|
type Attribute,
|
2023-06-06 13:06:32 +03:00
|
|
|
Hierarchy,
|
2023-11-20 13:01:43 +03:00
|
|
|
type RefTo,
|
2023-06-06 13:06:32 +03:00
|
|
|
generateId
|
|
|
|
} from '@hcengineering/core'
|
2023-11-20 13:01:43 +03:00
|
|
|
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
|
|
|
|
import view, { type AggregationManager } from '@hcengineering/view'
|
2023-06-06 13:06:32 +03:00
|
|
|
import { getResource } from '@hcengineering/platform'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export interface DocSubScriber<T extends Doc = Doc> {
|
|
|
|
attributes: Array<Ref<AnyAttribute>>
|
|
|
|
|
|
|
|
_class: Ref<Class<T>>
|
|
|
|
query: DocumentQuery<T>
|
|
|
|
options?: FindOptions<T>
|
|
|
|
|
|
|
|
refresh: () => void
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
|
|
|
mgrs: Map<Ref<Class<Doc>>, AggregationManager> = new Map<Ref<Class<Doc>>, AggregationManager>()
|
|
|
|
docs: Doc[] | undefined
|
|
|
|
|
2023-06-07 09:22:53 +03:00
|
|
|
subscribers: Map<string, DocSubScriber> = new Map<string, DocSubScriber>()
|
2023-06-06 13:06:32 +03:00
|
|
|
private constructor (client: Client, next?: PresentationMiddleware) {
|
|
|
|
super(client, next)
|
|
|
|
}
|
|
|
|
|
|
|
|
static create (client: Client, next?: PresentationMiddleware): AggregationMiddleware {
|
|
|
|
return new AggregationMiddleware(client, next)
|
|
|
|
}
|
|
|
|
|
|
|
|
async notifyTx (tx: Tx): Promise<void> {
|
|
|
|
const promises: Array<Promise<void>> = []
|
|
|
|
for (const [, value] of this.mgrs) {
|
|
|
|
promises.push(value.notifyTx(tx))
|
|
|
|
}
|
|
|
|
await Promise.all(promises)
|
|
|
|
await this.provideNotifyTx(tx)
|
|
|
|
}
|
|
|
|
|
|
|
|
async close (): Promise<void> {
|
2023-11-20 13:01:43 +03:00
|
|
|
this.mgrs.forEach((mgr) => {
|
|
|
|
mgr.close()
|
|
|
|
})
|
|
|
|
await this.provideClose()
|
2023-06-06 13:06:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async tx (tx: Tx): Promise<TxResult> {
|
|
|
|
return await this.provideTx(tx)
|
|
|
|
}
|
|
|
|
|
|
|
|
private refreshSubscribers (): void {
|
|
|
|
for (const s of this.subscribers.values()) {
|
|
|
|
// TODO: Do something more smart and track if used component field is changed.
|
|
|
|
s.refresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
|
|
|
}> {
|
|
|
|
const ret = await this.provideSubscribe(_class, query, options, refresh)
|
|
|
|
const h = this.client.getHierarchy()
|
|
|
|
|
|
|
|
const id = generateId()
|
|
|
|
const s: DocSubScriber<T> = {
|
|
|
|
_class,
|
|
|
|
query,
|
|
|
|
refresh,
|
|
|
|
options,
|
|
|
|
attributes: []
|
|
|
|
}
|
|
|
|
const statusFields: Array<Attribute<Doc>> = []
|
|
|
|
const allAttrs = h.getAllAttributes(_class)
|
|
|
|
|
2023-08-25 13:15:41 +03:00
|
|
|
const updatedQuery: DocumentQuery<T> = h.clone(ret.query ?? query)
|
|
|
|
const finalOptions = h.clone(ret.options ?? options ?? {})
|
2023-06-06 13:06:32 +03:00
|
|
|
|
|
|
|
await this.updateQueryOptions<T>(allAttrs, h, statusFields, updatedQuery, finalOptions)
|
|
|
|
|
|
|
|
if (statusFields.length > 0) {
|
|
|
|
this.subscribers.set(id, s)
|
|
|
|
return {
|
|
|
|
unsubscribe: () => {
|
|
|
|
ret.unsubscribe()
|
|
|
|
this.subscribers.delete(id)
|
|
|
|
},
|
|
|
|
query: updatedQuery,
|
|
|
|
options: finalOptions
|
|
|
|
}
|
|
|
|
}
|
2023-11-20 13:01:43 +03:00
|
|
|
return { unsubscribe: ret.unsubscribe }
|
2023-06-06 13:06:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getAggregationManager (_class: Ref<Class<Doc>>): Promise<AggregationManager | undefined> {
|
|
|
|
let mgr = this.mgrs.get(_class)
|
|
|
|
|
|
|
|
if (mgr === undefined) {
|
|
|
|
const h = this.client.getHierarchy()
|
|
|
|
const mixin = h.classHierarchyMixin(_class, view.mixin.Aggregation)
|
|
|
|
if (mixin?.createAggregationManager !== undefined) {
|
|
|
|
const f = await getResource(mixin.createAggregationManager)
|
2023-11-20 13:01:43 +03:00
|
|
|
mgr = f(this.client, () => {
|
|
|
|
this.refreshSubscribers()
|
|
|
|
})
|
2023-06-06 13:06:32 +03:00
|
|
|
this.mgrs.set(_class, mgr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mgr
|
|
|
|
}
|
|
|
|
|
|
|
|
async findAll<T extends Doc>(
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T> | undefined
|
|
|
|
): Promise<FindResult<T>> {
|
|
|
|
const docFields: Array<Attribute<Doc>> = []
|
|
|
|
const h = this.client.getHierarchy()
|
|
|
|
const allAttrs = h.getAllAttributes(_class)
|
2023-08-25 13:15:41 +03:00
|
|
|
const finalOptions = h.clone(options ?? {})
|
2023-06-06 13:06:32 +03:00
|
|
|
|
2023-08-25 13:15:41 +03:00
|
|
|
const fquery = h.clone(query ?? {})
|
2023-06-06 13:06:32 +03:00
|
|
|
|
2023-08-25 13:15:41 +03:00
|
|
|
await this.updateQueryOptions<T>(allAttrs, h, docFields, fquery, finalOptions)
|
|
|
|
|
|
|
|
const result = await this.provideFindAll(_class, fquery, finalOptions)
|
2023-06-06 13:06:32 +03:00
|
|
|
// We need to add $
|
|
|
|
if (docFields.length > 0) {
|
|
|
|
// We need to update $lookup for doc fields and provide $doc group fields.
|
|
|
|
for (const attr of docFields) {
|
|
|
|
for (const r of result) {
|
|
|
|
const resultDoc = Hierarchy.toDoc(r)
|
|
|
|
if (resultDoc.$lookup === undefined) {
|
|
|
|
resultDoc.$lookup = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
const mgr = await this.getAggregationManager((attr.type as RefTo<Doc>).to)
|
|
|
|
if (mgr !== undefined) {
|
|
|
|
await mgr.updateLookup(resultDoc, attr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
private async updateQueryOptions<T extends Doc>(
|
|
|
|
allAttrs: Map<string, AnyAttribute>,
|
|
|
|
h: Hierarchy,
|
|
|
|
docFields: Array<Attribute<Doc>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
finalOptions: FindOptions<T>
|
|
|
|
): Promise<void> {
|
|
|
|
for (const attr of allAttrs.values()) {
|
|
|
|
try {
|
|
|
|
if (attr.type._class !== core.class.RefTo) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const mgr = await this.getAggregationManager((attr.type as RefTo<Doc>).to)
|
|
|
|
if (mgr === undefined) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (h.isDerived((attr.type as RefTo<Doc>).to, mgr.getAttrClass())) {
|
|
|
|
let target: Array<Ref<Doc>> = []
|
|
|
|
let targetNin: Array<Ref<Doc>> = []
|
|
|
|
docFields.push(attr)
|
|
|
|
const v = (query as any)[attr.name]
|
|
|
|
|
|
|
|
if (v != null) {
|
|
|
|
// Only add filter if we have filer inside.
|
|
|
|
if (typeof v === 'string') {
|
|
|
|
target.push(v as Ref<Doc>)
|
|
|
|
} else {
|
|
|
|
if (v.$in !== undefined) {
|
|
|
|
target.push(...v.$in)
|
|
|
|
} else if (v.$nin !== undefined) {
|
|
|
|
targetNin.push(...v.$nin)
|
|
|
|
} else if (v.$ne !== undefined) {
|
|
|
|
targetNin.push(v.$ne)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find all similar name statues for same attribute name.
|
|
|
|
target = await mgr.categorize(target, attr)
|
|
|
|
targetNin = await mgr.categorize(targetNin, attr)
|
|
|
|
if (target.length > 0 || targetNin.length > 0) {
|
|
|
|
;(query as any)[attr.name] = {}
|
|
|
|
if (target.length > 0) {
|
|
|
|
;(query as any)[attr.name].$in = target
|
|
|
|
}
|
|
|
|
if (targetNin.length > 0) {
|
|
|
|
;(query as any)[attr.name].$nin = targetNin
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (finalOptions.lookup !== undefined) {
|
|
|
|
// Remove lookups by status field
|
|
|
|
if ((finalOptions.lookup as any)[attr.name] !== undefined) {
|
|
|
|
const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any
|
|
|
|
finalOptions.lookup = newLookup
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update sorting if defined.
|
|
|
|
if (mgr.updateSorting !== undefined) {
|
|
|
|
await mgr.updateSorting(finalOptions, attr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (err: any) {
|
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|