2021-08-03 16:55:52 +00:00
|
|
|
//
|
|
|
|
// Copyright © 2020 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.
|
|
|
|
//
|
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
import { Plugin } from '@anticrm/platform'
|
|
|
|
import type { Class, Doc, PluginConfiguration, Ref } from './classes'
|
2021-08-03 16:55:52 +00:00
|
|
|
import { DOMAIN_MODEL } from './classes'
|
|
|
|
import core from './component'
|
2022-03-03 09:01:39 +00:00
|
|
|
import { Hierarchy } from './hierarchy'
|
|
|
|
import { ModelDb } from './memdb'
|
|
|
|
import type { DocumentQuery, FindOptions, FindResult, Storage, TxResult, WithLookup } from './storage'
|
|
|
|
import { SortingOrder } from './storage'
|
2022-04-01 05:57:22 +00:00
|
|
|
import { Tx, TxCreateDoc, TxProcessor, TxUpdateDoc } from './tx'
|
2022-04-08 03:06:38 +00:00
|
|
|
import { toFindResult } from './utils'
|
2021-08-03 16:55:52 +00:00
|
|
|
|
2021-08-04 20:24:30 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export type TxHander = (tx: Tx) => void
|
2021-08-03 16:55:52 +00:00
|
|
|
|
2021-08-04 20:24:30 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
2021-08-07 17:21:52 +00:00
|
|
|
export interface Client extends Storage {
|
2021-08-08 21:04:39 +00:00
|
|
|
notify?: (tx: Tx) => void
|
2021-08-03 16:55:52 +00:00
|
|
|
getHierarchy: () => Hierarchy
|
2021-09-27 09:06:55 +00:00
|
|
|
getModel: () => ModelDb
|
2021-08-24 19:00:56 +00:00
|
|
|
findOne: <T extends Doc>(
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T>
|
|
|
|
) => Promise<WithLookup<T> | undefined>
|
2021-12-06 16:57:35 +00:00
|
|
|
close: () => Promise<void>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
export interface ClientConnection extends Storage {
|
|
|
|
close: () => Promise<void>
|
2021-08-03 16:55:52 +00:00
|
|
|
}
|
|
|
|
|
2021-08-08 21:04:39 +00:00
|
|
|
class ClientImpl implements Client {
|
|
|
|
notify?: (tx: Tx) => void
|
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
constructor (
|
|
|
|
private readonly hierarchy: Hierarchy,
|
|
|
|
private readonly model: ModelDb,
|
|
|
|
private readonly conn: ClientConnection
|
|
|
|
) {}
|
2021-08-03 16:55:52 +00:00
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
getHierarchy (): Hierarchy {
|
|
|
|
return this.hierarchy
|
|
|
|
}
|
2021-08-07 17:21:52 +00:00
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
getModel (): ModelDb {
|
|
|
|
return this.model
|
|
|
|
}
|
2021-09-27 09:06:55 +00:00
|
|
|
|
2021-08-03 16:55:52 +00:00
|
|
|
async findAll<T extends Doc>(
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T>
|
|
|
|
): Promise<FindResult<T>> {
|
2021-08-07 18:29:35 +00:00
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
2022-04-08 03:06:38 +00:00
|
|
|
const data =
|
2022-04-01 05:57:22 +00:00
|
|
|
domain === DOMAIN_MODEL
|
|
|
|
? await this.model.findAll(_class, query, options)
|
|
|
|
: await this.conn.findAll(_class, query, options)
|
2021-12-30 09:04:32 +00:00
|
|
|
|
|
|
|
// In case of mixin we need to create mixin proxies.
|
2022-03-03 09:01:39 +00:00
|
|
|
|
|
|
|
// Update mixins & lookups
|
2022-04-08 03:06:38 +00:00
|
|
|
const result = data.map((v) => {
|
2022-03-03 09:01:39 +00:00
|
|
|
return this.hierarchy.updateLookupMixin(_class, v, options)
|
|
|
|
})
|
2022-04-08 03:06:38 +00:00
|
|
|
return toFindResult(result, data.total)
|
2021-08-03 16:55:52 +00:00
|
|
|
}
|
|
|
|
|
2021-08-24 19:00:56 +00:00
|
|
|
async findOne<T extends Doc>(
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T>
|
|
|
|
): Promise<WithLookup<T> | undefined> {
|
2022-01-21 09:05:26 +00:00
|
|
|
return (await this.findAll(_class, query, { ...options, limit: 1 }))[0]
|
2021-08-24 19:00:56 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
async tx (tx: Tx): Promise<TxResult> {
|
2021-08-08 21:04:39 +00:00
|
|
|
if (tx.objectSpace === core.space.Model) {
|
|
|
|
this.hierarchy.tx(tx)
|
2021-08-10 10:26:48 +00:00
|
|
|
await this.model.tx(tx)
|
2021-08-08 21:04:39 +00:00
|
|
|
}
|
2021-12-30 09:04:32 +00:00
|
|
|
|
|
|
|
// We need to handle it on server, before performing local live query updates.
|
|
|
|
const result = await this.conn.tx(tx)
|
2021-08-08 21:04:39 +00:00
|
|
|
this.notify?.(tx)
|
2021-12-30 09:04:32 +00:00
|
|
|
return result
|
2021-08-03 16:55:52 +00:00
|
|
|
}
|
2021-08-09 06:59:50 +00:00
|
|
|
|
|
|
|
async updateFromRemote (tx: Tx): Promise<void> {
|
|
|
|
if (tx.objectSpace === core.space.Model) {
|
|
|
|
this.hierarchy.tx(tx)
|
|
|
|
await this.model.tx(tx)
|
|
|
|
}
|
|
|
|
this.notify?.(tx)
|
|
|
|
}
|
2021-12-06 16:57:35 +00:00
|
|
|
|
|
|
|
async close (): Promise<void> {
|
|
|
|
await this.conn.close()
|
|
|
|
}
|
2021-08-03 16:55:52 +00:00
|
|
|
}
|
|
|
|
|
2021-08-04 20:24:30 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
2021-08-03 16:55:52 +00:00
|
|
|
export async function createClient (
|
2022-04-01 05:57:22 +00:00
|
|
|
connect: (txHandler: TxHander) => Promise<ClientConnection>,
|
|
|
|
// If set will build model with only allowed plugins.
|
|
|
|
allowedPlugins?: Plugin[]
|
2021-08-03 16:55:52 +00:00
|
|
|
): Promise<Client> {
|
2021-08-09 06:59:50 +00:00
|
|
|
let client: ClientImpl | null = null
|
2021-08-03 16:55:52 +00:00
|
|
|
let txBuffer: Tx[] | undefined = []
|
|
|
|
|
|
|
|
const hierarchy = new Hierarchy()
|
|
|
|
const model = new ModelDb(hierarchy)
|
|
|
|
|
|
|
|
function txHander (tx: Tx): void {
|
|
|
|
if (client === null) {
|
|
|
|
txBuffer?.push(tx)
|
|
|
|
} else {
|
2021-08-09 06:59:50 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
client.updateFromRemote(tx)
|
2021-08-03 16:55:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const conn = await connect(txHander)
|
2022-04-01 05:57:22 +00:00
|
|
|
const atxes = await conn.findAll(
|
|
|
|
core.class.Tx,
|
|
|
|
{ objectSpace: core.space.Model },
|
|
|
|
{ sort: { _id: SortingOrder.Ascending } }
|
|
|
|
)
|
2022-02-03 09:02:54 +00:00
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
let systemTx: Tx[] = []
|
2022-02-03 09:02:54 +00:00
|
|
|
const userTx: Tx[] = []
|
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
atxes.forEach((tx) => (tx.modifiedBy === core.account.System ? systemTx : userTx).push(tx))
|
|
|
|
|
|
|
|
if (allowedPlugins !== undefined) {
|
|
|
|
// Filter system transactions
|
|
|
|
const configs = new Map<Ref<PluginConfiguration>, PluginConfiguration>()
|
|
|
|
for (const t of systemTx) {
|
|
|
|
if (t._class === core.class.TxCreateDoc) {
|
|
|
|
const ct = t as TxCreateDoc<Doc>
|
|
|
|
if (ct.objectClass === core.class.PluginConfiguration) {
|
2022-04-08 03:06:38 +00:00
|
|
|
configs.set(ct.objectId as Ref<PluginConfiguration>, TxProcessor.createDoc2Doc(ct) as PluginConfiguration)
|
2022-04-01 05:57:22 +00:00
|
|
|
}
|
|
|
|
} else if (t._class === core.class.TxUpdateDoc) {
|
|
|
|
const ut = t as TxUpdateDoc<Doc>
|
|
|
|
if (ut.objectClass === core.class.PluginConfiguration) {
|
|
|
|
const c = configs.get(ut.objectId as Ref<PluginConfiguration>)
|
|
|
|
if (c !== undefined) {
|
|
|
|
TxProcessor.updateDoc2Doc(c, ut)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
const excludedPlugins = Array.from(configs.values()).filter((it) => !allowedPlugins.includes(it.pluginId as Plugin))
|
2022-04-01 05:57:22 +00:00
|
|
|
|
|
|
|
for (const a of excludedPlugins) {
|
|
|
|
for (const c of configs.values()) {
|
|
|
|
if (a.pluginId === c.pluginId) {
|
|
|
|
const excluded = new Set<Ref<Tx>>()
|
|
|
|
for (const id of c.transactions) {
|
|
|
|
excluded.add(id as Ref<Tx>)
|
|
|
|
}
|
2022-04-08 03:06:38 +00:00
|
|
|
const exclude = systemTx.filter((t) => excluded.has(t._id))
|
2022-04-01 05:57:22 +00:00
|
|
|
console.log('exclude plugin', c.pluginId, exclude.length)
|
2022-04-08 03:06:38 +00:00
|
|
|
systemTx = systemTx.filter((t) => !excluded.has(t._id))
|
2022-04-01 05:57:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-03 09:02:54 +00:00
|
|
|
|
2022-04-01 05:57:22 +00:00
|
|
|
const txes = systemTx.concat(userTx)
|
2021-08-03 16:55:52 +00:00
|
|
|
|
|
|
|
const txMap = new Map<Ref<Tx>, Ref<Tx>>()
|
|
|
|
for (const tx of txes) txMap.set(tx._id, tx._id)
|
2022-03-04 14:42:02 +00:00
|
|
|
for (const tx of txes) {
|
|
|
|
try {
|
|
|
|
hierarchy.tx(tx)
|
|
|
|
} catch (err: any) {
|
|
|
|
console.error('failed to apply model transaction, skipping', JSON.stringify(tx), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const tx of txes) {
|
|
|
|
try {
|
|
|
|
await model.tx(tx)
|
|
|
|
} catch (err: any) {
|
|
|
|
console.error('failed to apply model transaction, skipping', JSON.stringify(tx), err)
|
|
|
|
}
|
|
|
|
}
|
2021-08-03 16:55:52 +00:00
|
|
|
|
|
|
|
txBuffer = txBuffer.filter((tx) => txMap.get(tx._id) === undefined)
|
|
|
|
|
|
|
|
client = new ClientImpl(hierarchy, model, conn)
|
|
|
|
|
|
|
|
for (const tx of txBuffer) txHander(tx)
|
|
|
|
txBuffer = undefined
|
|
|
|
|
|
|
|
return client
|
|
|
|
}
|