// // Copyright © 2020, 2021 Anticrm Platform Contributors. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import { Ref, Class, Doc, Tx, DocumentQuery, TxCreateDoc, TxRemoveDoc, Client, FindOptions, TxUpdateDoc, _getOperator, TxProcessor, resultSort, SortingQuery, FindResult, Hierarchy, Refs, WithLookup, LookupData } from '@anticrm/core' interface Query { _class: Ref> query: DocumentQuery result: Doc[] | Promise options?: FindOptions callback: (result: FindResult) => void } /** * @public */ export class LiveQuery extends TxProcessor implements Client { private readonly client: Client private readonly queries: Query[] = [] constructor (client: Client) { super() this.client = client } getHierarchy (): Hierarchy { return this.client.getHierarchy() } private match (q: Query, doc: Doc): boolean { if (!this.getHierarchy().isDerived(doc._class, q._class)) { return false } for (const key in q.query) { const value = (q.query as any)[key] if ((doc as any)[key] !== value) { return false } } return true } async findAll(_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise> { return await this.client.findAll(_class, query, options) } async findOne(_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise | undefined> { return (await this.findAll(_class, query, options))[0] } query(_class: Ref>, query: DocumentQuery, callback: (result: T[]) => void, options?: FindOptions): () => void { const result = this.client.findAll(_class, query, options) const q: Query = { _class, query, result, options, callback: callback as (result: Doc[]) => void } this.queries.push(q) result .then((result) => { q.callback(result) }) .catch((err) => { console.log('failed to update Live Query: ', err) }) return () => { this.queries.splice(this.queries.indexOf(q), 1) } } protected async txUpdateDoc (tx: TxUpdateDoc): Promise { for (const q of this.queries) { if (q.result instanceof Promise) { q.result = await q.result } const updatedDoc = q.result.find(p => p._id === tx.objectId) if (updatedDoc !== undefined) { await this.__updateDoc(q, updatedDoc, tx) this.sort(q, tx) await this.callback(updatedDoc, q) } } } private async lookup (doc: Doc, lookup: Refs): Promise { const result: LookupData = {} for (const key in lookup) { const _class = (lookup as any)[key] as Ref> const _id = (doc as any)[key] as Ref (result as any)[key] = (await this.client.findAll(_class, { _id }))[0] } (doc as WithLookup).$lookup = result } protected async txCreateDoc (tx: TxCreateDoc): Promise { console.log('query tx', tx) for (const q of this.queries) { const doc = TxProcessor.createDoc2Doc(tx) if (this.match(q, doc)) { if (q.result instanceof Promise) { q.result = await q.result } if (q.options?.lookup !== undefined) await this.lookup(doc, q.options.lookup) q.result.push(doc) if (q.options?.sort !== undefined) resultSort(q.result, q.options?.sort) if (q.options?.limit !== undefined && q.result.length > q.options.limit) { if (q.result.pop()?._id !== doc._id) { q.callback(q.result) } } else { q.callback(q.result) } } } } protected async txRemoveDoc (tx: TxRemoveDoc): Promise { for (const q of this.queries) { if (q.result instanceof Promise) { q.result = await q.result } const index = q.result.findIndex(p => p._id === tx.objectId) if (index > -1) { q.result.splice(index, 1) q.callback(q.result) } } } async tx (tx: Tx): Promise { // await this.client.tx(tx) await super.tx(tx) } // why this is separate from txUpdateDoc? private async __updateDoc (q: Query, updatedDoc: WithLookup, tx: TxUpdateDoc): Promise { const ops = tx.operations as any for (const key in ops) { if (key.startsWith('$')) { const operator = _getOperator(key) operator(updatedDoc, ops[key]) } else { (updatedDoc as any)[key] = ops[key] if (q.options !== undefined) { const lookup = (q.options.lookup as any)?.[key] if (lookup !== undefined) { (updatedDoc.$lookup as any)[key] = await this.client.findOne(lookup, { _id: ops[key] }) } } } } updatedDoc.modifiedBy = tx.modifiedBy updatedDoc.modifiedOn = tx.modifiedOn } private sort (q: Query, tx: TxUpdateDoc): void { const sort = q.options?.sort if (sort === undefined) return let needSort = sort.modifiedBy !== undefined || sort.modifiedOn !== undefined if (!needSort) needSort = this.checkNeedSort(sort, tx) if (needSort) resultSort(q.result as Doc[], sort) } private checkNeedSort (sort: SortingQuery, tx: TxUpdateDoc): boolean { const ops = tx.operations as any for (const key in ops) { if (key.startsWith('$')) { for (const opKey in ops[key]) { if (opKey in sort) return true } } else { if (key in sort) return true } } return false } private async callback (updatedDoc: Doc, q: Query): Promise { q.result = q.result as Doc[] if (q.options?.limit !== undefined && q.result.length > q.options.limit) { if (q.result[q.options?.limit]._id === updatedDoc._id) { const res = await this.findAll(q._class, q.query, q.options) q.result = res q.callback(res) return } if (q.result.pop()?._id !== updatedDoc._id) q.callback(q.result) } else { q.callback(q.result) } } }