UBERF-4319: Improve performance (#4501)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-02-02 22:50:22 +07:00 committed by GitHub
parent b114d142d8
commit 857535f5fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 209 additions and 146 deletions

View File

@ -277,6 +277,7 @@ jobs:
env: env:
DOCKER_CLI_HINTS: false DOCKER_CLI_HINTS: false
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.ref == 'refs/heads/main' }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: hardcoreeng username: hardcoreeng

View File

@ -13,21 +13,21 @@
// limitations under the License. // limitations under the License.
// //
import { MeasureContext } from './measurements' import { LoadModelResponse } from '.'
import type { Doc, Class, Ref, Domain, Timestamp } from './classes' import type { Class, Doc, Domain, Ref, Timestamp } from './classes'
import { Hierarchy } from './hierarchy' import { Hierarchy } from './hierarchy'
import { MeasureContext } from './measurements'
import { ModelDb } from './memdb' import { ModelDb } from './memdb'
import type { import type {
DocumentQuery, DocumentQuery,
FindOptions, FindOptions,
FindResult, FindResult,
TxResult,
SearchQuery,
SearchOptions, SearchOptions,
SearchResult SearchQuery,
SearchResult,
TxResult
} from './storage' } from './storage'
import type { Tx } from './tx' import type { Tx } from './tx'
import { LoadModelResponse } from '.'
/** /**
* @public * @public
@ -72,7 +72,9 @@ export interface ServerStorage extends LowLevelStorage {
ctx: MeasureContext, ctx: MeasureContext,
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
}
) => Promise<FindResult<T>> ) => Promise<FindResult<T>>
searchFulltext: (ctx: MeasureContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult> searchFulltext: (ctx: MeasureContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]> tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]>

View File

@ -13,11 +13,10 @@
// limitations under the License. // limitations under the License.
// //
import core, { type PluginConfiguration, SortingOrder } from '@hcengineering/core' import core, { SortingOrder, type PluginConfiguration, type TxUpdateDoc } from '@hcengineering/core'
import { type Plugin, type Resource, getResourcePlugin } from '@hcengineering/platform' import { getResourcePlugin, type Plugin, type Resource } from '@hcengineering/platform'
import { get, writable } from 'svelte/store' import { writable } from 'svelte/store'
import { createQuery } from '.' import { addTxListener, createQuery } from '.'
import { location as platformLocation } from '@hcengineering/ui'
/** /**
* @public * @public
@ -47,9 +46,17 @@ export const configurationStore = writable<ConfigurationManager>(configuration)
const configQuery = createQuery(true) const configQuery = createQuery(true)
let hashString = '' addTxListener((tx) => {
let workspaceId: string = '' if (tx._class === core.class.TxUpdateDoc) {
const cud = tx as TxUpdateDoc<PluginConfiguration>
if (cud.objectClass === core.class.PluginConfiguration) {
if (cud.operations.enabled !== undefined) {
// Plugin enabled/disabled we need to refresh
location.reload()
}
}
}
})
/** /**
* @public * @public
*/ */
@ -61,17 +68,8 @@ configQuery.query(
core.class.PluginConfiguration, core.class.PluginConfiguration,
{}, {},
(res) => { (res) => {
const newHash = res.map((it) => `${it.pluginId}=${it.enabled ? '+' : '-'}`).join('&')
const wsId = get(platformLocation).path[1]
if (hashString !== '' && hashString !== newHash && workspaceId !== '' && workspaceId === wsId) {
// Configuration is changed for same workspace.
location.reload()
}
workspaceId = wsId
hashString = newHash
configuration = new ConfigurationManager(res, new Map(res.map((it) => [it.pluginId, it]))) configuration = new ConfigurationManager(res, new Map(res.map((it) => [it.pluginId, it])))
configurationStore.set(configuration) configurationStore.set(configuration)
}, },
{ sort: { label: SortingOrder.Ascending } } { sort: { pluginId: SortingOrder.Ascending } }
) )

View File

@ -17,6 +17,7 @@ import clientPlugin from '@hcengineering/client'
import core, { import core, {
AccountClient, AccountClient,
ClientConnectEvent, ClientConnectEvent,
LoadModelResponse,
TxHandler, TxHandler,
TxPersistenceStore, TxPersistenceStore,
TxWorkspaceEvent, TxWorkspaceEvent,
@ -79,31 +80,62 @@ export default async () => {
} }
} }
function createModelPersistence (token: string): TxPersistenceStore | undefined { function createModelPersistence (token: string): TxPersistenceStore | undefined {
let dbRequest: IDBOpenDBRequest | undefined
let dbPromise: Promise<IDBDatabase | undefined> = Promise.resolve(undefined)
if (typeof localStorage !== 'undefined') {
dbPromise = new Promise<IDBDatabase>((resolve) => {
dbRequest = indexedDB.open('model.db.persistence', 2)
dbRequest.onupgradeneeded = function () {
const db = (dbRequest as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains('model')) {
db.createObjectStore('model', { keyPath: 'id' })
}
}
dbRequest.onsuccess = function () {
const db = (dbRequest as IDBOpenDBRequest).result
resolve(db)
}
})
}
return { return {
load: async () => { load: async () => {
if (typeof localStorage !== 'undefined') { const db = await dbPromise
const storedValue = localStorage.getItem('platform.model') ?? null if (db !== undefined) {
try { const transaction = db.transaction('model', 'readwrite') // (1)
const model = storedValue != null ? JSON.parse(storedValue) : undefined const models = transaction.objectStore('model') // (2)
if (token !== model?.token) { const model = await new Promise<{ id: string, model: LoadModelResponse } | undefined>((resolve) => {
return { const storedValue: IDBRequest<{ id: string, model: LoadModelResponse }> = models.get(token)
full: false, storedValue.onsuccess = function () {
transactions: [], resolve(storedValue.result)
hash: []
}
} }
return model.model storedValue.onerror = function () {
} catch {} resolve(undefined)
}
})
if (model == null) {
return {
full: false,
transactions: [],
hash: ''
}
}
return model.model
} }
return { return {
full: true, full: true,
transactions: [], transactions: [],
hash: [] hash: ''
} }
}, },
store: async (model) => { store: async (model) => {
if (typeof localStorage !== 'undefined') { const db = await dbPromise
localStorage.setItem('platform.model', JSON.stringify({ token, model })) if (db !== undefined) {
const transaction = db.transaction('model', 'readwrite') // (1)
const models = transaction.objectStore('model') // (2)
models.put({ id: token, model })
} }
} }
} }

View File

@ -442,11 +442,11 @@
issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue)) issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue))
} }
) )
console.log('createIssue measure', await doneOp())
draftController.remove() draftController.remove()
descriptionBox?.removeDraft(false) descriptionBox?.removeDraft(false)
isAssigneeTouched = false isAssigneeTouched = false
console.log('createIssue measure', doneOp())
} catch (err: any) { } catch (err: any) {
console.error(err) console.error(err)
await doneOp() // Complete in case of error await doneOp() // Complete in case of error

View File

@ -49,7 +49,9 @@ export interface DbAdapter {
findAll: <T extends Doc>( findAll: <T extends Doc>(
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
}
) => Promise<FindResult<T>> ) => Promise<FindResult<T>>
tx: (...tx: Tx[]) => Promise<TxResult> tx: (...tx: Tx[]) => Promise<TxResult>

View File

@ -372,9 +372,11 @@ class TServerStorage implements ServerStorage {
ctx: MeasureContext, ctx: MeasureContext,
clazz: Ref<Class<T>>, clazz: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
}
): Promise<FindResult<T>> { ): Promise<FindResult<T>> {
const domain = this.hierarchy.getDomain(clazz) const domain = options?.domain ?? this.hierarchy.getDomain(clazz)
if (query?.$search !== undefined) { if (query?.$search !== undefined) {
return await ctx.with('client-fulltext-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options)) return await ctx.with('client-fulltext-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options))
} }

View File

@ -18,6 +18,7 @@ import core, {
Class, Class,
Doc, Doc,
DocumentQuery, DocumentQuery,
Domain,
FindOptions, FindOptions,
FindResult, FindResult,
LookupData, LookupData,
@ -47,14 +48,19 @@ import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@
import { BaseMiddleware } from './base' import { BaseMiddleware } from './base'
import { getUser, isOwner, isSystem, mergeTargets } from './utils' import { getUser, isOwner, isSystem, mergeTargets } from './utils'
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private'>
/** /**
* @public * @public
*/ */
export class SpaceSecurityMiddleware extends BaseMiddleware implements Middleware { export class SpaceSecurityMiddleware extends BaseMiddleware implements Middleware {
private allowedSpaces: Record<Ref<Account>, Ref<Space>[]> = {} private allowedSpaces: Record<Ref<Account>, Ref<Space>[]> = {}
private privateSpaces: Record<Ref<Space>, Space | undefined> = {} private privateSpaces: Record<Ref<Space>, SpaceWithMembers | undefined> = {}
private domainSpaces: Record<string, Set<Ref<Space>>> = {} private readonly _domainSpaces = new Map<string, Set<Ref<Space>> | Promise<Set<Ref<Space>>>>()
private publicSpaces: Ref<Space>[] = [] private publicSpaces: Ref<Space>[] = []
private spaceMeasureCtx!: MeasureContext
private readonly systemSpaces = [ private readonly systemSpaces = [
core.space.Configuration, core.space.Configuration,
core.space.DerivedTx, core.space.DerivedTx,
@ -78,9 +84,8 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
next?: Middleware next?: Middleware
): Promise<SpaceSecurityMiddleware> { ): Promise<SpaceSecurityMiddleware> {
const res = new SpaceSecurityMiddleware(broadcast, storage, next) const res = new SpaceSecurityMiddleware(broadcast, storage, next)
await ctx.with('space chain', {}, async (ctx) => { res.spaceMeasureCtx = ctx.newChild('space chain', {})
await res.init(ctx) await res.init(res.spaceMeasureCtx)
})
return res return res
} }
@ -90,7 +95,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
this.allowedSpaces[member] = arr this.allowedSpaces[member] = arr
} }
private addSpace (space: Space): void { private addSpace (space: SpaceWithMembers): void {
this.privateSpaces[space._id] = space this.privateSpaces[space._id] = space
for (const member of space.members) { for (const member of space.members) {
this.addMemberSpace(member, space._id) this.addMemberSpace(member, space._id)
@ -98,49 +103,24 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
} }
private async init (ctx: MeasureContext): Promise<void> { private async init (ctx: MeasureContext): Promise<void> {
const spaces = await this.storage.findAll(ctx, core.class.Space, { private: true }) const spaces: SpaceWithMembers[] = await this.storage.findAll(
for (const space of spaces) { ctx,
this.addSpace(space) core.class.Space,
} {},
this.publicSpaces = (await this.storage.findAll(ctx, core.class.Space, { private: false })).map((p) => p._id) {
await this.initDomains(ctx) projection: {
} private: 1,
_id: 1,
private async initDomains (ctx: MeasureContext): Promise<void> { members: 1
const classesPerDomain: Record<string, Ref<Class<Doc>>[]> = {}
const classes = this.storage.hierarchy.getDescendants(core.class.Doc)
for (const _class of classes) {
const clazz = this.storage.hierarchy.getClass(_class)
if (clazz.domain === undefined) continue
const domain = clazz.domain
classesPerDomain[domain] = classesPerDomain[domain] ?? []
classesPerDomain[domain].push(_class)
}
for (const domain in classesPerDomain) {
for (const _class of classesPerDomain[domain]) {
const field = this.getKey(_class)
const map = this.domainSpaces[domain] ?? new Set()
this.domainSpaces[domain] = map
while (true) {
const spaces = await this.storage.findAll(
ctx,
_class,
{
[field]: { $nin: Array.from(map.values()) }
},
{
projection: { [field]: 1 },
limit: 1000
}
)
if (spaces.length === 0) {
break
}
spaces.forEach((p) => map.add((p as any)[field] as Ref<Space>))
} }
} }
)
for (const space of spaces) {
if (space.private) {
this.addSpace(space)
}
} }
this.publicSpaces = spaces.filter((it) => !it.private).map((p) => p._id)
} }
private removeMemberSpace (member: Ref<Account>, space: Ref<Space>): void { private removeMemberSpace (member: Ref<Account>, space: Ref<Space>): void {
@ -210,7 +190,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
} }
} }
private async syncMembers (members: Ref<Account>[], space: Space): Promise<void> { private async syncMembers (members: Ref<Account>[], space: SpaceWithMembers): Promise<void> {
const oldMembers = new Set(space.members) const oldMembers = new Set(space.members)
const newMembers = new Set(members) const newMembers = new Set(members)
const changed: Ref<Account>[] = [] const changed: Ref<Account>[] = []
@ -253,7 +233,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
this.broadcast([tx], targets) this.broadcast([tx], targets)
} }
private async broadcastNonMembers (space: Space | undefined): Promise<void> { private async broadcastNonMembers (space: SpaceWithMembers | undefined): Promise<void> {
const users = await this.storage.modelDb.findAll(core.class.Account, { _id: { $nin: space?.members } }) const users = await this.storage.modelDb.findAll(core.class.Account, { _id: { $nin: space?.members } })
await this.brodcastEvent(users.map((p) => p._id)) await this.brodcastEvent(users.map((p) => p._id))
} }
@ -291,7 +271,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (updateDoc.operations.$pull?.members !== undefined) { if (updateDoc.operations.$pull?.members !== undefined) {
await this.pullMembersHandle(updateDoc.operations.$pull.members, space._id) await this.pullMembersHandle(updateDoc.operations.$pull.members, space._id)
} }
space = TxProcessor.updateDoc2Doc(space, updateDoc) space = TxProcessor.updateDoc2Doc(space as any, updateDoc)
} }
} }
@ -349,7 +329,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
return targets return targets
} }
private processTxSpaceDomain (tx: TxCUD<Doc>): void { private async processTxSpaceDomain (tx: TxCUD<Doc>): Promise<void> {
const actualTx = TxProcessor.extractTx(tx) const actualTx = TxProcessor.extractTx(tx)
if (actualTx._class === core.class.TxCreateDoc) { if (actualTx._class === core.class.TxCreateDoc) {
const ctx = actualTx as TxCreateDoc<Doc> const ctx = actualTx as TxCreateDoc<Doc>
@ -358,21 +338,19 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
const space = (doc as any)[key] const space = (doc as any)[key]
if (space === undefined) return if (space === undefined) return
const domain = this.storage.hierarchy.getDomain(ctx.objectClass) const domain = this.storage.hierarchy.getDomain(ctx.objectClass)
this.domainSpaces[domain] = this.domainSpaces[domain] ?? new Set() ;(await this.getDomainSpaces(domain)).add(space)
this.domainSpaces[domain].add(space)
} else if (actualTx._class === core.class.TxUpdateDoc) { } else if (actualTx._class === core.class.TxUpdateDoc) {
const updTx = actualTx as TxUpdateDoc<Doc> const updTx = actualTx as TxUpdateDoc<Doc>
const key = this.getKey(updTx.objectClass) const key = this.getKey(updTx.objectClass)
const space = (updTx.operations as any)[key] const space = (updTx.operations as any)[key]
if (space !== undefined) { if (space !== undefined) {
const domain = this.storage.hierarchy.getDomain(updTx.objectClass) const domain = this.storage.hierarchy.getDomain(updTx.objectClass)
this.domainSpaces[domain] = this.domainSpaces[domain] ?? new Set() ;(await this.getDomainSpaces(domain)).add(space)
this.domainSpaces[domain].add(space)
} }
} }
} }
private async proccessTx (ctx: SessionContext, tx: Tx): Promise<void> { private async processTx (ctx: SessionContext, tx: Tx): Promise<void> {
const h = this.storage.hierarchy const h = this.storage.hierarchy
if (h.isDerived(tx._class, core.class.TxCUD)) { if (h.isDerived(tx._class, core.class.TxCUD)) {
const cudTx = tx as TxCUD<Doc> const cudTx = tx as TxCUD<Doc>
@ -380,7 +358,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (isSpace) { if (isSpace) {
await this.handleTx(ctx, cudTx as TxCUD<Space>) await this.handleTx(ctx, cudTx as TxCUD<Space>)
} }
this.processTxSpaceDomain(tx as TxCUD<Doc>) await this.processTxSpaceDomain(tx as TxCUD<Doc>)
if (h.isDerived(cudTx.objectClass, core.class.Account) && cudTx._class === core.class.TxUpdateDoc) { if (h.isDerived(cudTx.objectClass, core.class.Account) && cudTx._class === core.class.TxUpdateDoc) {
const ctx = cudTx as TxUpdateDoc<Account> const ctx = cudTx as TxUpdateDoc<Account>
if (ctx.operations.role !== undefined) { if (ctx.operations.role !== undefined) {
@ -391,21 +369,24 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
} }
async tx (ctx: SessionContext, tx: Tx): Promise<TxMiddlewareResult> { async tx (ctx: SessionContext, tx: Tx): Promise<TxMiddlewareResult> {
await this.proccessTx(ctx, tx) await this.processTx(ctx, tx)
const targets = await this.getTxTargets(ctx, tx) const targets = await this.getTxTargets(ctx, tx)
const res = await this.provideTx(ctx, tx) const res = await this.provideTx(ctx, tx)
for (const tx of res[1]) { for (const tx of res[1]) {
await this.proccessTx(ctx, tx) await this.processTx(ctx, tx)
} }
return [res[0], res[1], mergeTargets(targets, res[2])] return [res[0], res[1], mergeTargets(targets, res[2])]
} }
handleBroadcast (tx: Tx[], targets?: string[]): Tx[] { handleBroadcast (tx: Tx[], targets?: string[]): Tx[] {
for (const t of tx) { const process = async (): Promise<void> => {
if (this.storage.hierarchy.isDerived(t._class, core.class.TxCUD)) { for (const t of tx) {
this.processTxSpaceDomain(t as TxCUD<Doc>) if (this.storage.hierarchy.isDerived(t._class, core.class.TxCUD)) {
await this.processTxSpaceDomain(t as TxCUD<Doc>)
}
} }
} }
void process()
return this.provideHandleBroadcast(tx, targets) return this.provideHandleBroadcast(tx, targets)
} }
@ -419,18 +400,52 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
} }
} }
private filterByDomain (domain: string, spaces: Ref<Space>[]): Ref<Space>[] { async loadDomainSpaces (ctx: MeasureContext, domain: Domain): Promise<Set<Ref<Space>>> {
const domainSpaces = this.domainSpaces[domain] const map = new Set<Ref<Space>>()
if (domainSpaces === undefined) return [] const field = this.getKey(domain)
while (true) {
const spaces = await this.storage.findAll(
ctx,
core.class.Doc,
{
[field]: { $nin: Array.from(map.values()) }
},
{
projection: { [field]: 1 },
limit: 1000,
domain
}
)
if (spaces.length === 0) {
break
}
spaces.forEach((p) => map.add((p as any)[field] as Ref<Space>))
}
return map
}
async getDomainSpaces (domain: Domain): Promise<Set<Ref<Space>>> {
let domainSpaces = this._domainSpaces.get(domain)
if (domainSpaces === undefined) {
const p = this.loadDomainSpaces(this.spaceMeasureCtx, domain)
this._domainSpaces.set(domain, p)
domainSpaces = await p
this._domainSpaces.set(domain, domainSpaces)
}
return domainSpaces instanceof Promise ? await domainSpaces : domainSpaces
}
private async filterByDomain (domain: Domain, spaces: Ref<Space>[]): Promise<Ref<Space>[]> {
const domainSpaces = await this.getDomainSpaces(domain)
return spaces.filter((p) => domainSpaces.has(p)) return spaces.filter((p) => domainSpaces.has(p))
} }
private mergeQuery<T extends Doc>( private async mergeQuery<T extends Doc>(
account: Account, account: Account,
query: ObjQueryType<T['space']>, query: ObjQueryType<T['space']>,
domain: string domain: Domain
): ObjQueryType<T['space']> { ): Promise<ObjQueryType<T['space']>> {
const spaces = this.filterByDomain(domain, this.getAllAllowedSpaces(account)) const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account))
if (query == null) { if (query == null) {
return { $in: spaces } return { $in: spaces }
} }
@ -446,12 +461,8 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
return query return query
} }
private getKey<T extends Doc>(_class: Ref<Class<T>>): string { private getKey (domain: string): string {
return this.storage.hierarchy.isDerived(_class, core.class.Tx) return domain === 'tx' ? 'objectSpace' : domain === 'space' ? '_id' : 'space'
? 'objectSpace'
: this.storage.hierarchy.isDerived(_class, core.class.Space)
? '_id'
: 'space'
} }
override async findAll<T extends Doc>( override async findAll<T extends Doc>(
@ -468,9 +479,9 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (!isSystem(account)) { if (!isSystem(account)) {
if (!isOwner(account) || !this.storage.hierarchy.isDerived(_class, core.class.Space)) { if (!isOwner(account) || !this.storage.hierarchy.isDerived(_class, core.class.Space)) {
if (query[field] !== undefined) { if (query[field] !== undefined) {
;(newQuery as any)[field] = this.mergeQuery(account, query[field], domain) ;(newQuery as any)[field] = await this.mergeQuery(account, query[field], domain)
} else { } else {
const spaces = this.filterByDomain(domain, this.getAllAllowedSpaces(account)) const spaces = await this.filterByDomain(domain, this.getAllAllowedSpaces(account))
;(newQuery as any)[field] = { $in: spaces } ;(newQuery as any)[field] = { $in: spaces }
} }
} }

View File

@ -168,34 +168,41 @@ abstract class MongoAdapterBase implements DbAdapter {
translated[tkey] = value translated[tkey] = value
} }
const baseClass = this.hierarchy.getBaseClass(clazz) const baseClass = this.hierarchy.getBaseClass(clazz)
const classes = this.hierarchy.getDescendants(baseClass) if (baseClass !== core.class.Doc) {
const classes = this.hierarchy.getDescendants(baseClass)
// Only replace if not specified // Only replace if not specified
if (translated._class === undefined) { if (translated._class === undefined) {
translated._class = { $in: classes }
} else if (typeof translated._class === 'string') {
if (!classes.includes(translated._class)) {
translated._class = { $in: classes } translated._class = { $in: classes }
} } else if (typeof translated._class === 'string') {
} else if (typeof translated._class === 'object' && translated._class !== null) { if (!classes.includes(translated._class)) {
let descendants: Ref<Class<Doc>>[] = classes translated._class = { $in: classes }
}
} else if (typeof translated._class === 'object' && translated._class !== null) {
let descendants: Ref<Class<Doc>>[] = classes
if (Array.isArray(translated._class.$in)) { if (Array.isArray(translated._class.$in)) {
const classesIds = new Set(classes) const classesIds = new Set(classes)
descendants = translated._class.$in.filter((c: Ref<Class<Doc>>) => classesIds.has(c)) descendants = translated._class.$in.filter((c: Ref<Class<Doc>>) => classesIds.has(c))
}
if (translated._class != null && Array.isArray(translated._class.$nin)) {
const excludedClassesIds = new Set<Ref<Class<Doc>>>(translated._class.$nin)
descendants = descendants.filter((c) => !excludedClassesIds.has(c))
}
translated._class = { $in: descendants }
} }
if (translated._class != null && Array.isArray(translated._class.$nin)) { if (baseClass !== clazz) {
const excludedClassesIds = new Set<Ref<Class<Doc>>>(translated._class.$nin) // Add an mixin to be exists flag
descendants = descendants.filter((c) => !excludedClassesIds.has(c)) translated[clazz] = { $exists: true }
}
} else {
// No need to pass _class in case of fixed domain search.
if ('_class' in translated) {
delete translated._class
} }
translated._class = { $in: descendants }
}
if (baseClass !== clazz) {
// Add an mixin to be exists flag
translated[clazz] = { $exists: true }
} }
return translated return translated
} }
@ -416,7 +423,9 @@ abstract class MongoAdapterBase implements DbAdapter {
private async findWithPipeline<T extends Doc>( private async findWithPipeline<T extends Doc>(
clazz: Ref<Class<T>>, clazz: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
}
): Promise<FindResult<T>> { ): Promise<FindResult<T>> {
const pipeline = [] const pipeline = []
const match = { $match: this.translateQuery(clazz, query) } const match = { $match: this.translateQuery(clazz, query) }
@ -454,7 +463,8 @@ abstract class MongoAdapterBase implements DbAdapter {
...(options?.total === true ? { totalCount: [{ $count: 'count' }] } : {}) ...(options?.total === true ? { totalCount: [{ $count: 'count' }] } : {})
} }
}) })
const domain = this.hierarchy.getDomain(clazz) // const domain = this.hierarchy.getDomain(clazz)
const domain = options?.domain ?? this.hierarchy.getDomain(clazz)
const cursor = this.db.collection(domain).aggregate(pipeline, { const cursor = this.db.collection(domain).aggregate(pipeline, {
checkKeys: false, checkKeys: false,
enableUtf8Validation: false enableUtf8Validation: false
@ -562,13 +572,15 @@ abstract class MongoAdapterBase implements DbAdapter {
async findAll<T extends Doc>( async findAll<T extends Doc>(
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
}
): Promise<FindResult<T>> { ): Promise<FindResult<T>> {
// TODO: rework this // TODO: rework this
if (options != null && (options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))) { if (options != null && (options?.lookup != null || this.isEnumSort(_class, options) || this.isRulesSort(options))) {
return await this.findWithPipeline(_class, query, options) return await this.findWithPipeline(_class, query, options)
} }
const domain = this.hierarchy.getDomain(_class) const domain = options?.domain ?? this.hierarchy.getDomain(_class)
const coll = this.db.collection(domain) const coll = this.db.collection(domain)
const mongoQuery = this.translateQuery(_class, query) const mongoQuery = this.translateQuery(_class, query)
let cursor = coll.find<T>(mongoQuery, { let cursor = coll.find<T>(mongoQuery, {
@ -719,6 +731,9 @@ abstract class MongoAdapterBase implements DbAdapter {
} }
async load (domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> { async load (domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
if (docs.length === 0) {
return []
}
const cursor = this.db.collection<Doc>(domain).find<Doc>({ _id: { $in: docs } }) const cursor = this.db.collection<Doc>(domain).find<Doc>({ _id: { $in: docs } })
const result = await this.toArray(cursor) const result = await this.toArray(cursor)
return this.stripHash(this.stripHash(result)) return this.stripHash(this.stripHash(result))