mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
UBERF-7543: Add low level groupBy api and improve security space lookup (#6126)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
c919a14fa5
commit
66c20106ae
@ -67,6 +67,9 @@ export interface LowLevelStorage {
|
|||||||
|
|
||||||
// Remove a list of documents.
|
// Remove a list of documents.
|
||||||
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
|
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
|
||||||
|
|
||||||
|
// Low level direct group API
|
||||||
|
groupBy: <T>(ctx: MeasureContext, domain: Domain, field: string) => Promise<Set<T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Branding {
|
export interface Branding {
|
||||||
|
@ -112,6 +112,8 @@ export interface DbAdapter {
|
|||||||
upload: (ctx: MeasureContext, domain: Domain, docs: Doc[]) => Promise<void>
|
upload: (ctx: MeasureContext, domain: Domain, docs: Doc[]) => Promise<void>
|
||||||
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
|
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
|
||||||
|
|
||||||
|
groupBy: <T>(ctx: MeasureContext, domain: Domain, field: string) => Promise<Set<T>>
|
||||||
|
|
||||||
// Bulk update operations
|
// Bulk update operations
|
||||||
update: (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>) => Promise<void>
|
update: (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>) => Promise<void>
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,10 @@ export class DummyDbAdapter implements DbAdapter {
|
|||||||
async clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {}
|
async clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {}
|
||||||
|
|
||||||
async update (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {}
|
async update (ctx: MeasureContext, domain: Domain, operations: Map<Ref<Doc>, DocumentUpdate<Doc>>): Promise<void> {}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InMemoryAdapter extends DummyDbAdapter implements DbAdapter {
|
class InMemoryAdapter extends DummyDbAdapter implements DbAdapter {
|
||||||
|
@ -117,6 +117,12 @@ class PipelineImpl implements Pipeline {
|
|||||||
: await this.storage.findAll(ctx.ctx, _class, query, options)
|
: await this.storage.findAll(ctx.ctx, _class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return this.head !== undefined
|
||||||
|
? await this.head.groupBy(ctx, domain, field)
|
||||||
|
: await this.storage.groupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
|
||||||
async searchFulltext (ctx: SessionContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
async searchFulltext (ctx: SessionContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||||
return this.head !== undefined
|
return this.head !== undefined
|
||||||
? await this.head.searchFulltext(ctx, query, options)
|
? await this.head.searchFulltext(ctx, query, options)
|
||||||
|
@ -440,6 +440,10 @@ export class TServerStorage implements ServerStorage {
|
|||||||
return this.model.filter((it) => it.modifiedOn > lastModelTx)
|
return this.model.filter((it) => it.modifiedOn > lastModelTx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return await this.getAdapter(domain, false).groupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
|
||||||
async findAll<T extends Doc>(
|
async findAll<T extends Doc>(
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
clazz: Ref<Class<T>>,
|
clazz: Ref<Class<T>>,
|
||||||
|
@ -97,6 +97,8 @@ export interface Middleware {
|
|||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
) => Promise<FindResult<T>>
|
) => Promise<FindResult<T>>
|
||||||
|
|
||||||
|
groupBy: <T>(ctx: MeasureContext, domain: Domain, field: string) => Promise<Set<T>>
|
||||||
handleBroadcast: HandleBroadcastFunc
|
handleBroadcast: HandleBroadcastFunc
|
||||||
searchFulltext: (ctx: SessionContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
|
searchFulltext: (ctx: SessionContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,10 @@ class ElasticDataAdapter implements DbAdapter {
|
|||||||
this.getDocId = (fulltext) => fulltext.slice(0, -1 * (this.workspaceString.length + 1)) as Ref<Doc>
|
this.getDocId = (fulltext) => fulltext.slice(0, -1 * (this.workspaceString.length + 1)) as Ref<Doc>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
|
||||||
async findAll<T extends Doc>(
|
async findAll<T extends Doc>(
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
|
@ -23,7 +23,9 @@ import {
|
|||||||
SearchOptions,
|
SearchOptions,
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
Tx
|
Tx,
|
||||||
|
type Domain,
|
||||||
|
type MeasureContext
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||||
|
|
||||||
@ -45,6 +47,17 @@ export abstract class BaseMiddleware {
|
|||||||
return await this.provideFindAll(ctx, _class, query, options)
|
return await this.provideFindAll(ctx, _class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async providerGroupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
if (this.next !== undefined) {
|
||||||
|
return await this.next.groupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
return await this.storage.groupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return await this.providerGroupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
|
||||||
async searchFulltext (ctx: SessionContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
async searchFulltext (ctx: SessionContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||||
return await this.provideSearchFulltext(ctx, query, options)
|
return await this.provideSearchFulltext(ctx, query, options)
|
||||||
}
|
}
|
||||||
|
@ -450,30 +450,8 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadDomainSpaces (ctx: MeasureContext, domain: Domain): Promise<Set<Ref<Space>>> {
|
async loadDomainSpaces (ctx: MeasureContext, domain: Domain): Promise<Set<Ref<Space>>> {
|
||||||
const map = new Set<Ref<Space>>()
|
|
||||||
const field = this.getKey(domain)
|
const field = this.getKey(domain)
|
||||||
while (true) {
|
return await this.storage.groupBy<Ref<Space>>(ctx, domain, field)
|
||||||
const nin = Array.from(map.values())
|
|
||||||
const spaces = await this.storage.findAll(
|
|
||||||
ctx,
|
|
||||||
core.class.Doc,
|
|
||||||
nin.length > 0
|
|
||||||
? {
|
|
||||||
[field]: { $nin: nin }
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
{
|
|
||||||
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>>> {
|
async getDomainSpaces (domain: Domain): Promise<Set<Ref<Space>>> {
|
||||||
|
@ -82,7 +82,8 @@ import {
|
|||||||
type Filter,
|
type Filter,
|
||||||
type FindCursor,
|
type FindCursor,
|
||||||
type Sort,
|
type Sort,
|
||||||
type UpdateFilter
|
type UpdateFilter,
|
||||||
|
type FindOptions as MongoFindOptions
|
||||||
} from 'mongodb'
|
} from 'mongodb'
|
||||||
import { DBCollectionHelper, getMongoClient, getWorkspaceDB, type MongoClientReference } from './utils'
|
import { DBCollectionHelper, getMongoClient, getWorkspaceDB, type MongoClientReference } from './utils'
|
||||||
|
|
||||||
@ -517,10 +518,18 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
let result: WithLookup<T>[] = []
|
let result: WithLookup<T>[] = []
|
||||||
let total = options?.total === true ? 0 : -1
|
let total = options?.total === true ? 0 : -1
|
||||||
try {
|
try {
|
||||||
result = await ctx.with('toArray', {}, async (ctx) => await toArray(cursor), {
|
await ctx.with(
|
||||||
domain,
|
'toArray',
|
||||||
pipeline
|
{},
|
||||||
})
|
async (ctx) => {
|
||||||
|
result = await toArray(cursor)
|
||||||
|
},
|
||||||
|
() => ({
|
||||||
|
size: result.length,
|
||||||
|
domain,
|
||||||
|
pipeline
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error during executing cursor in findWithPipeline', clazz, cutObjectArray(query), options, e)
|
console.error('error during executing cursor in findWithPipeline', clazz, cutObjectArray(query), options, e)
|
||||||
throw e
|
throw e
|
||||||
@ -623,6 +632,33 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withContext('groupBy')
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
const result = await this.globalCtx.with(
|
||||||
|
'groupBy',
|
||||||
|
{ domain },
|
||||||
|
async (ctx) => {
|
||||||
|
const coll = this.collection(domain)
|
||||||
|
const grResult = await coll
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$' + field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.toArray()
|
||||||
|
return new Set(grResult.map((it) => it._id as unknown as T))
|
||||||
|
},
|
||||||
|
|
||||||
|
() => ({
|
||||||
|
findOps: this.findOps,
|
||||||
|
txOps: this.txOps
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
findOps: number = 0
|
findOps: number = 0
|
||||||
txOps: number = 0
|
txOps: number = 0
|
||||||
opIndex: number = 0
|
opIndex: number = 0
|
||||||
@ -695,6 +731,33 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
const coll = this.collection(domain)
|
const coll = this.collection(domain)
|
||||||
const mongoQuery = this.translateQuery(_class, query)
|
const mongoQuery = this.translateQuery(_class, query)
|
||||||
|
|
||||||
|
if (options?.limit === 1) {
|
||||||
|
// Skip sort/projection/etc.
|
||||||
|
return await ctx.with(
|
||||||
|
'find-one',
|
||||||
|
{},
|
||||||
|
async (ctx) => {
|
||||||
|
const findOptions: MongoFindOptions = {}
|
||||||
|
|
||||||
|
if (options?.sort !== undefined) {
|
||||||
|
findOptions.sort = this.collectSort<T>(options, _class)
|
||||||
|
}
|
||||||
|
if (options?.projection !== undefined) {
|
||||||
|
findOptions.projection = this.calcProjection<T>(options, _class)
|
||||||
|
} else {
|
||||||
|
findOptions.projection = { '%hash%': 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await coll.findOne(mongoQuery, findOptions)
|
||||||
|
if (doc != null) {
|
||||||
|
return toFindResult([doc as unknown as T])
|
||||||
|
}
|
||||||
|
return toFindResult([])
|
||||||
|
},
|
||||||
|
{ mongoQuery }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let cursor = coll.find<T>(mongoQuery)
|
let cursor = coll.find<T>(mongoQuery)
|
||||||
|
|
||||||
if (options?.projection !== undefined) {
|
if (options?.projection !== undefined) {
|
||||||
@ -702,6 +765,8 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
if (projection != null) {
|
if (projection != null) {
|
||||||
cursor = cursor.project(projection)
|
cursor = cursor.project(projection)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cursor = cursor.project({ '%hash%': 0 })
|
||||||
}
|
}
|
||||||
let total: number = -1
|
let total: number = -1
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
@ -721,11 +786,20 @@ abstract class MongoAdapterBase implements DbAdapter {
|
|||||||
|
|
||||||
// Error in case of timeout
|
// Error in case of timeout
|
||||||
try {
|
try {
|
||||||
const res: T[] = await ctx.with('toArray', {}, async (ctx) => await toArray(cursor), {
|
let res: T[] = []
|
||||||
mongoQuery,
|
await ctx.with(
|
||||||
options,
|
'toArray',
|
||||||
domain
|
{},
|
||||||
})
|
async (ctx) => {
|
||||||
|
res = await toArray(cursor)
|
||||||
|
},
|
||||||
|
() => ({
|
||||||
|
size: res.length,
|
||||||
|
mongoQuery,
|
||||||
|
options,
|
||||||
|
domain
|
||||||
|
})
|
||||||
|
)
|
||||||
if (options?.total === true && options?.limit === undefined) {
|
if (options?.total === true && options?.limit === undefined) {
|
||||||
total = res.length
|
total = res.length
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,10 @@ class StorageBlobAdapter implements DbAdapter {
|
|||||||
return await this.blobAdapter.findAll(ctx, _class, query, options)
|
return await this.blobAdapter.findAll(ctx, _class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
|
||||||
|
return await this.blobAdapter.groupBy(ctx, domain, field)
|
||||||
|
}
|
||||||
|
|
||||||
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
|
async tx (ctx: MeasureContext, ...tx: Tx[]): Promise<TxResult[]> {
|
||||||
throw new PlatformError(unknownError('Direct Blob operations are not possible'))
|
throw new PlatformError(unknownError('Direct Blob operations are not possible'))
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ describe('server', () => {
|
|||||||
close: async () => {},
|
close: async () => {},
|
||||||
storage: {} as unknown as ServerStorage,
|
storage: {} as unknown as ServerStorage,
|
||||||
domains: async () => [],
|
domains: async () => [],
|
||||||
|
groupBy: async () => new Set(),
|
||||||
find: (ctx: MeasureContext, domain: Domain) => ({
|
find: (ctx: MeasureContext, domain: Domain) => ({
|
||||||
next: async (ctx: MeasureContext) => undefined,
|
next: async (ctx: MeasureContext) => undefined,
|
||||||
close: async (ctx: MeasureContext) => {}
|
close: async (ctx: MeasureContext) => {}
|
||||||
@ -170,6 +171,7 @@ describe('server', () => {
|
|||||||
return toFindResult([d as unknown as T])
|
return toFindResult([d as unknown as T])
|
||||||
},
|
},
|
||||||
tx: async (ctx: SessionContext, tx: Tx): Promise<[TxResult, Tx[], string[] | undefined]> => [{}, [], undefined],
|
tx: async (ctx: SessionContext, tx: Tx): Promise<[TxResult, Tx[], string[] | undefined]> => [{}, [], undefined],
|
||||||
|
groupBy: async () => new Set(),
|
||||||
close: async () => {},
|
close: async () => {},
|
||||||
storage: {} as unknown as ServerStorage,
|
storage: {} as unknown as ServerStorage,
|
||||||
domains: async () => [],
|
domains: async () => [],
|
||||||
|
Loading…
Reference in New Issue
Block a user