mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
UBERF-4319: Improve performance (#4501)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
b114d142d8
commit
857535f5fa
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@ -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
|
||||||
|
@ -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[]]>
|
||||||
|
@ -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 } }
|
||||||
)
|
)
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user