2021-08-18 19:46:59 +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.
|
|
|
|
//
|
|
|
|
|
2021-12-22 09:02:51 +00:00
|
|
|
import core, {
|
2021-12-02 09:07:25 +00:00
|
|
|
Class,
|
|
|
|
Doc,
|
2022-04-08 03:06:38 +00:00
|
|
|
DocumentQuery,
|
2022-05-23 15:53:33 +00:00
|
|
|
Domain,
|
2022-04-08 03:06:38 +00:00
|
|
|
DOMAIN_MODEL,
|
|
|
|
DOMAIN_TX,
|
|
|
|
escapeLikeForRegexp,
|
|
|
|
FindOptions,
|
|
|
|
FindResult,
|
|
|
|
Hierarchy,
|
|
|
|
isOperator,
|
|
|
|
Lookup,
|
|
|
|
Mixin,
|
|
|
|
ModelDb,
|
|
|
|
Ref,
|
|
|
|
ReverseLookups,
|
2022-04-29 05:27:17 +00:00
|
|
|
SortingOrder,
|
2022-05-13 17:31:57 +00:00
|
|
|
SortingQuery,
|
2022-05-23 15:53:33 +00:00
|
|
|
StorageIterator,
|
2022-04-29 05:27:17 +00:00
|
|
|
toFindResult,
|
|
|
|
Tx,
|
2021-12-02 09:07:25 +00:00
|
|
|
TxCreateDoc,
|
2022-04-08 03:06:38 +00:00
|
|
|
TxMixin,
|
|
|
|
TxProcessor,
|
|
|
|
TxPutBag,
|
2021-12-02 09:07:25 +00:00
|
|
|
TxRemoveDoc,
|
|
|
|
TxResult,
|
2022-04-29 05:27:17 +00:00
|
|
|
TxUpdateDoc,
|
|
|
|
WithLookup
|
2021-12-02 09:07:25 +00:00
|
|
|
} from '@anticrm/core'
|
2021-08-26 12:16:45 +00:00
|
|
|
import type { DbAdapter, TxAdapter } from '@anticrm/server-core'
|
2022-01-12 09:06:04 +00:00
|
|
|
import { Collection, Db, Document, Filter, MongoClient, Sort } from 'mongodb'
|
2021-12-02 09:07:25 +00:00
|
|
|
import { getMongoClient } from './utils'
|
2021-08-18 19:46:59 +00:00
|
|
|
|
2022-05-23 15:53:33 +00:00
|
|
|
import { createHash } from 'node:crypto'
|
|
|
|
|
2021-08-18 19:46:59 +00:00
|
|
|
function translateDoc (doc: Doc): Document {
|
|
|
|
return doc as Document
|
|
|
|
}
|
|
|
|
|
2022-04-29 05:27:17 +00:00
|
|
|
function isLookupQuery<T extends Doc> (query: DocumentQuery<T>): boolean {
|
2022-04-27 17:01:10 +00:00
|
|
|
for (const key in query) {
|
|
|
|
if (key.includes('$lookup.')) return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-05-13 17:31:57 +00:00
|
|
|
function isLookupSort<T extends Doc> (sort: SortingQuery<T> | undefined): boolean {
|
|
|
|
if (sort === undefined) return false
|
|
|
|
for (const key in sort) {
|
|
|
|
if (key.includes('$lookup.')) return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-01-31 09:06:30 +00:00
|
|
|
interface LookupStep {
|
|
|
|
from: string
|
|
|
|
localField: string
|
|
|
|
foreignField: string
|
|
|
|
as: string
|
|
|
|
}
|
|
|
|
|
2021-08-26 12:16:45 +00:00
|
|
|
abstract class MongoAdapterBase extends TxProcessor {
|
2022-04-08 03:06:38 +00:00
|
|
|
constructor (
|
|
|
|
protected readonly db: Db,
|
|
|
|
protected readonly hierarchy: Hierarchy,
|
|
|
|
protected readonly modelDb: ModelDb,
|
|
|
|
protected readonly client: MongoClient
|
|
|
|
) {
|
2021-08-18 19:46:59 +00:00
|
|
|
super()
|
|
|
|
}
|
|
|
|
|
2021-08-25 17:55:26 +00:00
|
|
|
async init (): Promise<void> {}
|
|
|
|
|
2022-01-12 09:06:04 +00:00
|
|
|
async close (): Promise<void> {
|
|
|
|
await this.client.close()
|
|
|
|
}
|
|
|
|
|
2021-12-02 09:07:25 +00:00
|
|
|
private translateQuery<T extends Doc>(clazz: Ref<Class<T>>, query: DocumentQuery<T>): Filter<Document> {
|
2021-10-01 13:33:49 +00:00
|
|
|
const translated: any = {}
|
|
|
|
for (const key in query) {
|
|
|
|
const value = (query as any)[key]
|
2022-03-24 09:04:29 +00:00
|
|
|
|
|
|
|
const tkey = this.checkMixinKey(key, clazz)
|
2021-10-10 13:20:20 +00:00
|
|
|
if (value !== null && typeof value === 'object') {
|
2021-10-01 13:33:49 +00:00
|
|
|
const keys = Object.keys(value)
|
|
|
|
if (keys[0] === '$like') {
|
|
|
|
const pattern = value.$like as string
|
2022-03-24 09:04:29 +00:00
|
|
|
translated[tkey] = {
|
2022-04-08 03:06:38 +00:00
|
|
|
$regex: `^${pattern
|
|
|
|
.split('%')
|
|
|
|
.map((it) => escapeLikeForRegexp(it))
|
|
|
|
.join('.*')}$`,
|
2021-10-01 13:33:49 +00:00
|
|
|
$options: 'i'
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2022-03-24 09:04:29 +00:00
|
|
|
translated[tkey] = value
|
2021-10-01 13:33:49 +00:00
|
|
|
}
|
2021-12-30 09:04:32 +00:00
|
|
|
const baseClass = this.hierarchy.getBaseClass(clazz)
|
|
|
|
const classes = this.hierarchy.getDescendants(baseClass)
|
2021-11-19 07:41:20 +00:00
|
|
|
|
|
|
|
// Only replace if not specified
|
|
|
|
if (translated._class?.$in === undefined) {
|
|
|
|
translated._class = { $in: classes }
|
|
|
|
}
|
2021-12-30 09:04:32 +00:00
|
|
|
|
|
|
|
if (baseClass !== clazz) {
|
|
|
|
// Add an mixin to be exists flag
|
|
|
|
translated[clazz] = { $exists: true }
|
|
|
|
}
|
2021-10-01 13:33:49 +00:00
|
|
|
// return Object.assign({}, query, { _class: { $in: classes } })
|
|
|
|
return translated
|
2021-09-08 08:34:48 +00:00
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async getLookupValue<T extends Doc>(lookup: Lookup<T>, result: LookupStep[], parent?: string): Promise<void> {
|
2022-01-31 09:06:30 +00:00
|
|
|
for (const key in lookup) {
|
|
|
|
if (key === '_id') {
|
|
|
|
await this.getReverseLookupValue(lookup, result, parent)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const value = (lookup as any)[key]
|
|
|
|
const fullKey = parent !== undefined ? parent + '.' + key : key
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
const [_class, nested] = value
|
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
|
|
|
if (domain !== DOMAIN_MODEL) {
|
|
|
|
result.push({
|
|
|
|
from: domain,
|
|
|
|
localField: fullKey,
|
|
|
|
foreignField: '_id',
|
|
|
|
as: fullKey.split('.').join('') + '_lookup'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
await this.getLookupValue(nested, result, fullKey + '_lookup')
|
|
|
|
} else {
|
|
|
|
const _class = value as Ref<Class<Doc>>
|
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
|
|
|
if (domain !== DOMAIN_MODEL) {
|
|
|
|
result.push({
|
|
|
|
from: domain,
|
|
|
|
localField: fullKey,
|
|
|
|
foreignField: '_id',
|
|
|
|
as: fullKey.split('.').join('') + '_lookup'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async getReverseLookupValue (
|
|
|
|
lookup: ReverseLookups,
|
|
|
|
result: LookupStep[],
|
|
|
|
parent?: string
|
|
|
|
): Promise<any | undefined> {
|
2022-01-31 09:06:30 +00:00
|
|
|
const fullKey = parent !== undefined ? parent + '.' + '_id' : '_id'
|
|
|
|
for (const key in lookup._id) {
|
|
|
|
const as = parent !== undefined ? parent + key : key
|
|
|
|
const value = lookup._id[key]
|
2022-03-04 09:01:46 +00:00
|
|
|
|
|
|
|
let _class: Ref<Class<Doc>>
|
|
|
|
let attr = 'attachedTo'
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
_class = value[0]
|
|
|
|
attr = value[1]
|
|
|
|
} else {
|
|
|
|
_class = value
|
|
|
|
}
|
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
2022-01-31 09:06:30 +00:00
|
|
|
if (domain !== DOMAIN_MODEL) {
|
|
|
|
const step = {
|
|
|
|
from: domain,
|
|
|
|
localField: fullKey,
|
2022-03-04 09:01:46 +00:00
|
|
|
foreignField: attr,
|
2022-01-31 09:06:30 +00:00
|
|
|
as: as.split('.').join('') + '_lookup'
|
|
|
|
}
|
|
|
|
result.push(step)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async getLookups<T extends Doc>(lookup: Lookup<T> | undefined, parent?: string): Promise<LookupStep[]> {
|
2022-01-31 09:06:30 +00:00
|
|
|
if (lookup === undefined) return []
|
|
|
|
const result: [] = []
|
|
|
|
await this.getLookupValue(lookup, result, parent)
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async fillLookup<T extends Doc>(
|
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
object: any,
|
|
|
|
key: string,
|
|
|
|
fullKey: string,
|
|
|
|
targetObject: any
|
|
|
|
): Promise<void> {
|
2022-01-31 09:06:30 +00:00
|
|
|
if (targetObject.$lookup === undefined) {
|
|
|
|
targetObject.$lookup = {}
|
|
|
|
}
|
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
|
|
|
if (domain !== DOMAIN_MODEL) {
|
|
|
|
const arr = object[fullKey]
|
2022-03-14 09:05:02 +00:00
|
|
|
if (arr !== undefined && Array.isArray(arr)) {
|
|
|
|
if (arr.length === 1) {
|
|
|
|
targetObject.$lookup[key] = arr[0]
|
|
|
|
} else if (arr.length > 1) {
|
|
|
|
targetObject.$lookup[key] = arr
|
|
|
|
}
|
|
|
|
}
|
2022-01-31 09:06:30 +00:00
|
|
|
} else {
|
2022-05-13 17:31:57 +00:00
|
|
|
targetObject.$lookup[key] = (await this.modelDb.findAll(_class, { _id: targetObject[key] }))[0]
|
2022-01-31 09:06:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async fillLookupValue<T extends Doc>(
|
|
|
|
lookup: Lookup<T> | undefined,
|
|
|
|
object: any,
|
|
|
|
parent?: string,
|
|
|
|
parentObject?: any
|
|
|
|
): Promise<void> {
|
2022-01-31 09:06:30 +00:00
|
|
|
if (lookup === undefined) return
|
|
|
|
for (const key in lookup) {
|
|
|
|
if (key === '_id') {
|
|
|
|
await this.fillReverseLookup(lookup, object, parent, parentObject)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const value = (lookup as any)[key]
|
|
|
|
const fullKey = parent !== undefined ? parent + key + '_lookup' : key + '_lookup'
|
|
|
|
const targetObject = parentObject ?? object
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
const [_class, nested] = value
|
|
|
|
await this.fillLookup(_class, object, key, fullKey, targetObject)
|
|
|
|
await this.fillLookupValue(nested, object, fullKey, targetObject.$lookup[key])
|
|
|
|
} else {
|
|
|
|
await this.fillLookup(value, object, key, fullKey, targetObject)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
private async fillReverseLookup (
|
|
|
|
lookup: ReverseLookups,
|
|
|
|
object: any,
|
|
|
|
parent?: string,
|
|
|
|
parentObject?: any
|
|
|
|
): Promise<void> {
|
2022-01-31 09:06:30 +00:00
|
|
|
const targetObject = parentObject ?? object
|
|
|
|
if (targetObject.$lookup === undefined) {
|
|
|
|
targetObject.$lookup = {}
|
|
|
|
}
|
|
|
|
for (const key in lookup._id) {
|
|
|
|
const value = lookup._id[key]
|
2022-03-04 09:01:46 +00:00
|
|
|
let _class: Ref<Class<Doc>>
|
|
|
|
let attr = 'attachedTo'
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
_class = value[0]
|
|
|
|
attr = value[1]
|
|
|
|
} else {
|
|
|
|
_class = value
|
|
|
|
}
|
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
2022-01-31 09:06:30 +00:00
|
|
|
const fullKey = parent !== undefined ? parent + key + '_lookup' : key + '_lookup'
|
|
|
|
if (domain !== DOMAIN_MODEL) {
|
|
|
|
const arr = object[fullKey]
|
|
|
|
targetObject.$lookup[key] = arr
|
|
|
|
} else {
|
2022-03-04 09:01:46 +00:00
|
|
|
const arr = await this.modelDb.findAll(_class, { [attr]: targetObject._id })
|
2022-01-31 09:06:30 +00:00
|
|
|
targetObject.$lookup[key] = arr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-02 09:07:25 +00:00
|
|
|
private async lookup<T extends Doc>(
|
|
|
|
clazz: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options: FindOptions<T>
|
|
|
|
): Promise<FindResult<T>> {
|
2021-09-03 14:34:08 +00:00
|
|
|
const pipeline = []
|
2022-04-27 17:01:10 +00:00
|
|
|
const match = { $match: this.translateQuery(clazz, query) }
|
2022-05-13 17:31:57 +00:00
|
|
|
const slowPipeline = isLookupQuery(query) || isLookupSort(options.sort)
|
2022-01-31 09:06:30 +00:00
|
|
|
const steps = await this.getLookups(options.lookup)
|
2022-04-27 17:01:10 +00:00
|
|
|
if (slowPipeline) {
|
|
|
|
for (const step of steps) {
|
|
|
|
pipeline.push({ $lookup: step })
|
|
|
|
}
|
2021-09-03 14:34:08 +00:00
|
|
|
}
|
2022-04-27 17:01:10 +00:00
|
|
|
pipeline.push(match)
|
|
|
|
const resultPipeline: any[] = []
|
2021-12-08 09:14:55 +00:00
|
|
|
if (options.sort !== undefined) {
|
|
|
|
const sort = {} as any
|
|
|
|
for (const _key in options.sort) {
|
2022-05-13 17:31:57 +00:00
|
|
|
let key: string = _key
|
|
|
|
const arr = key.split('.').filter((p) => p)
|
|
|
|
key = ''
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
const element = arr[i]
|
|
|
|
if (element === '$lookup') {
|
|
|
|
key += arr[++i] + '_lookup'
|
|
|
|
} else {
|
|
|
|
if (!key.endsWith('.') && i > 0) {
|
|
|
|
key += '.'
|
|
|
|
}
|
|
|
|
key += arr[i]
|
|
|
|
if (i !== arr.length - 1) {
|
|
|
|
key += '.'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Check if key is belong to mixin class, we need to add prefix.
|
|
|
|
key = this.checkMixinKey<T>(key, clazz)
|
|
|
|
}
|
2021-12-08 09:14:55 +00:00
|
|
|
sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1
|
|
|
|
}
|
2022-05-13 17:31:57 +00:00
|
|
|
pipeline.push({ $sort: sort })
|
2022-03-04 09:01:46 +00:00
|
|
|
}
|
2022-04-11 09:49:05 +00:00
|
|
|
if (options.limit !== undefined) {
|
2022-04-27 17:01:10 +00:00
|
|
|
resultPipeline.push({ $limit: options.limit })
|
2022-04-11 09:49:05 +00:00
|
|
|
}
|
2022-04-27 17:01:10 +00:00
|
|
|
if (!slowPipeline) {
|
|
|
|
for (const step of steps) {
|
|
|
|
resultPipeline.push({ $lookup: step })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (options?.projection !== undefined) {
|
|
|
|
resultPipeline.push({ $project: options.projection })
|
|
|
|
}
|
|
|
|
pipeline.push({
|
|
|
|
$facet: {
|
|
|
|
results: resultPipeline,
|
|
|
|
totalCount: [
|
|
|
|
{
|
|
|
|
$count: 'count'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
const domain = this.hierarchy.getDomain(clazz)
|
|
|
|
const cursor = this.db.collection(domain).aggregate(pipeline)
|
|
|
|
const res = (await cursor.toArray())[0]
|
|
|
|
const result = res.results as WithLookup<T>[]
|
|
|
|
const total = res.totalCount?.shift()?.count
|
2021-09-03 14:34:08 +00:00
|
|
|
for (const row of result) {
|
2022-01-31 09:06:30 +00:00
|
|
|
row.$lookup = {}
|
|
|
|
await this.fillLookupValue(options.lookup, row)
|
2022-02-25 09:05:55 +00:00
|
|
|
this.clearExtraLookups(row)
|
2021-09-03 14:34:08 +00:00
|
|
|
}
|
2022-04-11 09:49:05 +00:00
|
|
|
return toFindResult(result, total)
|
2021-09-03 14:34:08 +00:00
|
|
|
}
|
|
|
|
|
2022-02-25 09:05:55 +00:00
|
|
|
private clearExtraLookups (row: any): void {
|
|
|
|
for (const key in row) {
|
|
|
|
if (key.endsWith('_lookup')) {
|
|
|
|
// eslint-disable-next-line
|
|
|
|
delete row[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-02 09:04:13 +00:00
|
|
|
private checkMixinKey<T extends Doc>(key: string, clazz: Ref<Class<T>>): string {
|
|
|
|
if (!key.includes('.')) {
|
|
|
|
try {
|
|
|
|
const attr = this.hierarchy.getAttribute(clazz, key)
|
|
|
|
if (this.hierarchy.isMixin(attr.attributeOf)) {
|
|
|
|
// It is mixin
|
|
|
|
key = attr.attributeOf + '.' + key
|
|
|
|
}
|
|
|
|
} catch (err: any) {
|
|
|
|
// ignore, if
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return key
|
|
|
|
}
|
|
|
|
|
2021-12-02 09:07:25 +00:00
|
|
|
async findAll<T extends Doc>(
|
2021-08-18 19:46:59 +00:00
|
|
|
_class: Ref<Class<T>>,
|
|
|
|
query: DocumentQuery<T>,
|
|
|
|
options?: FindOptions<T>
|
|
|
|
): Promise<FindResult<T>> {
|
2021-09-03 14:34:08 +00:00
|
|
|
// TODO: rework this
|
|
|
|
if (options !== null && options !== undefined) {
|
2021-12-02 09:07:25 +00:00
|
|
|
if (options.lookup !== undefined) {
|
|
|
|
return await this.lookup(_class, query, options)
|
|
|
|
}
|
2021-09-03 14:34:08 +00:00
|
|
|
}
|
2021-08-18 19:46:59 +00:00
|
|
|
const domain = this.hierarchy.getDomain(_class)
|
2022-04-21 08:51:16 +00:00
|
|
|
const coll = this.db.collection(domain)
|
|
|
|
let cursor = coll.find<T>(this.translateQuery(_class, query))
|
2021-12-08 09:14:55 +00:00
|
|
|
|
2022-03-04 09:01:46 +00:00
|
|
|
if (options?.projection !== undefined) {
|
|
|
|
cursor = cursor.project(options.projection)
|
|
|
|
}
|
2022-04-08 03:06:38 +00:00
|
|
|
let total: number | undefined
|
2021-08-20 16:20:22 +00:00
|
|
|
if (options !== null && options !== undefined) {
|
|
|
|
if (options.sort !== undefined) {
|
|
|
|
const sort: Sort = {}
|
|
|
|
for (const key in options.sort) {
|
2022-02-02 09:04:13 +00:00
|
|
|
const ckey = this.checkMixinKey<T>(key, _class)
|
2021-08-20 16:20:22 +00:00
|
|
|
const order = options.sort[key] === SortingOrder.Ascending ? 1 : -1
|
2022-02-02 09:04:13 +00:00
|
|
|
sort[ckey] = order
|
2021-08-20 16:20:22 +00:00
|
|
|
}
|
|
|
|
cursor = cursor.sort(sort)
|
|
|
|
}
|
2021-12-08 09:14:55 +00:00
|
|
|
if (options.limit !== undefined) {
|
2022-04-21 08:51:16 +00:00
|
|
|
total = await coll.estimatedDocumentCount()
|
2021-12-08 09:14:55 +00:00
|
|
|
cursor = cursor.limit(options.limit)
|
|
|
|
}
|
2021-08-20 16:20:22 +00:00
|
|
|
}
|
2022-04-08 03:06:38 +00:00
|
|
|
const res = await cursor.toArray()
|
|
|
|
return toFindResult(res, total)
|
2021-08-18 19:46:59 +00:00
|
|
|
}
|
2022-05-23 15:53:33 +00:00
|
|
|
|
|
|
|
find (domain: Domain): StorageIterator {
|
|
|
|
const coll = this.db.collection<Doc>(domain)
|
|
|
|
const iterator = coll.find({}, {})
|
|
|
|
|
|
|
|
return {
|
|
|
|
next: async () => {
|
|
|
|
const d = await iterator.next()
|
|
|
|
if (d === null) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
const doc = JSON.stringify(d)
|
|
|
|
const hash = createHash('sha256')
|
|
|
|
hash.update(doc)
|
|
|
|
const digest = hash.digest('base64')
|
|
|
|
return {
|
|
|
|
id: d._id,
|
|
|
|
hash: digest,
|
|
|
|
size: doc.length // Some approx size for document.
|
|
|
|
}
|
|
|
|
},
|
|
|
|
close: async () => {
|
|
|
|
await iterator.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async load (domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
|
|
|
|
return await this.db
|
|
|
|
.collection(domain)
|
|
|
|
.find<Doc>({ _id: { $in: docs } })
|
|
|
|
.toArray()
|
|
|
|
}
|
2021-08-18 19:46:59 +00:00
|
|
|
}
|
|
|
|
|
2021-08-26 12:16:45 +00:00
|
|
|
class MongoAdapter extends MongoAdapterBase {
|
2021-10-13 16:46:48 +00:00
|
|
|
protected override async txPutBag (tx: TxPutBag<any>): Promise<TxResult> {
|
2021-09-15 17:03:34 +00:00
|
|
|
const domain = this.hierarchy.getDomain(tx.objectClass)
|
|
|
|
await this.db.collection(domain).updateOne({ _id: tx.objectId }, { $set: { [tx.bag + '.' + tx.key]: tx.value } })
|
2021-10-13 16:46:48 +00:00
|
|
|
return {}
|
2021-09-15 10:53:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-26 17:51:49 +00:00
|
|
|
protected override async txRemoveDoc (tx: TxRemoveDoc<Doc>): Promise<TxResult> {
|
|
|
|
const domain = this.hierarchy.getDomain(tx.objectClass)
|
|
|
|
await this.db.collection(domain).deleteOne({ _id: tx.objectId })
|
|
|
|
return {}
|
2021-09-15 10:53:11 +00:00
|
|
|
}
|
|
|
|
|
2021-12-30 09:04:32 +00:00
|
|
|
protected async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
|
|
|
const domain = this.hierarchy.getDomain(tx.objectClass)
|
|
|
|
|
|
|
|
if (isOperator(tx.attributes)) {
|
|
|
|
const operator = Object.keys(tx.attributes)[0]
|
|
|
|
if (operator === '$move') {
|
|
|
|
const keyval = (tx.attributes as any).$move
|
|
|
|
const arr = tx.mixin + '.' + Object.keys(keyval)[0]
|
|
|
|
const desc = keyval[arr]
|
|
|
|
const ops = [
|
|
|
|
{
|
|
|
|
updateOne: {
|
|
|
|
filter: { _id: tx.objectId },
|
|
|
|
update: {
|
|
|
|
$pull: {
|
|
|
|
[arr]: desc.$value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
updateOne: {
|
|
|
|
filter: { _id: tx.objectId },
|
|
|
|
update: {
|
|
|
|
$set: {
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
|
|
|
},
|
|
|
|
$push: {
|
|
|
|
[arr]: {
|
|
|
|
$each: [desc.$value],
|
|
|
|
$position: desc.$position
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
return await this.db.collection(domain).bulkWrite(ops as any)
|
|
|
|
} else {
|
|
|
|
return await this.db.collection(domain).updateOne(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{
|
|
|
|
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
|
|
|
$set: {
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
2022-04-08 03:06:38 +00:00
|
|
|
return await this.db.collection(domain).updateOne(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{
|
|
|
|
$set: {
|
|
|
|
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
2021-12-30 09:04:32 +00:00
|
|
|
}
|
2022-04-08 03:06:38 +00:00
|
|
|
}
|
|
|
|
)
|
2021-12-30 09:04:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private translateMixinAttrs (mixin: Ref<Mixin<Doc>>, attributes: Record<string, any>): Record<string, any> {
|
|
|
|
const attrs: Record<string, any> = {}
|
|
|
|
let count = 0
|
|
|
|
for (const [k, v] of Object.entries(attributes)) {
|
|
|
|
if (k.startsWith('$')) {
|
|
|
|
attrs[k] = this.translateMixinAttrs(mixin, v)
|
|
|
|
} else {
|
|
|
|
attrs[mixin + '.' + k] = v
|
|
|
|
}
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
|
|
|
|
if (count === 0) {
|
|
|
|
// We need at least one attribute, to be inside for first time,
|
|
|
|
// for mongo to create embedded object, if we don't want to get object first.
|
|
|
|
attrs[mixin + '.' + '__mixin'] = 'true'
|
|
|
|
}
|
|
|
|
return attrs
|
2021-09-15 10:53:11 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected override async txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult> {
|
2021-08-26 12:16:45 +00:00
|
|
|
const doc = TxProcessor.createDoc2Doc(tx)
|
|
|
|
const domain = this.hierarchy.getDomain(doc._class)
|
|
|
|
await this.db.collection(domain).insertOne(translateDoc(doc))
|
2021-10-13 16:46:48 +00:00
|
|
|
return {}
|
2021-08-26 12:16:45 +00:00
|
|
|
}
|
2021-09-06 17:56:50 +00:00
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected override async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
2021-09-06 17:56:50 +00:00
|
|
|
const domain = this.hierarchy.getDomain(tx.objectClass)
|
2021-10-09 08:54:08 +00:00
|
|
|
if (isOperator(tx.operations)) {
|
|
|
|
const operator = Object.keys(tx.operations)[0]
|
|
|
|
if (operator === '$move') {
|
|
|
|
const keyval = (tx.operations as any).$move
|
|
|
|
const arr = Object.keys(keyval)[0]
|
|
|
|
const desc = keyval[arr]
|
|
|
|
const ops = [
|
|
|
|
{
|
|
|
|
updateOne: {
|
|
|
|
filter: { _id: tx.objectId },
|
|
|
|
update: {
|
|
|
|
$pull: {
|
|
|
|
[arr]: desc.$value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
updateOne: {
|
|
|
|
filter: { _id: tx.objectId },
|
|
|
|
update: {
|
2021-11-26 11:06:21 +00:00
|
|
|
$set: {
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
|
|
|
},
|
2021-10-09 08:54:08 +00:00
|
|
|
$push: {
|
|
|
|
[arr]: {
|
|
|
|
$each: [desc.$value],
|
|
|
|
$position: desc.$position
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2021-10-13 18:58:14 +00:00
|
|
|
return await this.db.collection(domain).bulkWrite(ops as any)
|
2021-10-09 08:54:08 +00:00
|
|
|
} else {
|
2021-10-13 19:54:11 +00:00
|
|
|
if (tx.retrieve === true) {
|
2021-12-02 09:07:25 +00:00
|
|
|
const result = await this.db.collection(domain).findOneAndUpdate(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{
|
|
|
|
...tx.operations,
|
|
|
|
$set: {
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ returnDocument: 'after' }
|
|
|
|
)
|
2021-10-13 19:54:11 +00:00
|
|
|
return { object: result.value }
|
|
|
|
} else {
|
2021-12-02 09:07:25 +00:00
|
|
|
return await this.db.collection(domain).updateOne(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{
|
|
|
|
...tx.operations,
|
|
|
|
$set: {
|
|
|
|
modifiedBy: tx.modifiedBy,
|
|
|
|
modifiedOn: tx.modifiedOn
|
|
|
|
}
|
2021-11-29 11:07:49 +00:00
|
|
|
}
|
2021-12-02 09:07:25 +00:00
|
|
|
)
|
2021-10-13 19:54:11 +00:00
|
|
|
}
|
2021-10-09 08:54:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-10-13 19:54:11 +00:00
|
|
|
if (tx.retrieve === true) {
|
2021-12-02 09:07:25 +00:00
|
|
|
const result = await this.db
|
|
|
|
.collection(domain)
|
|
|
|
.findOneAndUpdate(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{ $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } },
|
|
|
|
{ returnDocument: 'after' }
|
|
|
|
)
|
2021-10-13 19:54:11 +00:00
|
|
|
return { object: result.value }
|
|
|
|
} else {
|
2021-12-02 09:07:25 +00:00
|
|
|
return await this.db
|
|
|
|
.collection(domain)
|
|
|
|
.updateOne(
|
|
|
|
{ _id: tx.objectId },
|
|
|
|
{ $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }
|
|
|
|
)
|
2021-10-13 19:54:11 +00:00
|
|
|
}
|
2021-10-09 08:54:08 +00:00
|
|
|
}
|
2021-09-06 17:56:50 +00:00
|
|
|
}
|
2021-08-26 12:16:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
2021-12-22 09:02:51 +00:00
|
|
|
txColl: Collection | undefined
|
2021-10-13 16:46:48 +00:00
|
|
|
protected txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult> {
|
2021-09-15 10:53:11 +00:00
|
|
|
throw new Error('Method not implemented.')
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected txPutBag (tx: TxPutBag<any>): Promise<TxResult> {
|
2021-09-15 10:53:11 +00:00
|
|
|
throw new Error('Method not implemented.')
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
2021-09-15 10:53:11 +00:00
|
|
|
throw new Error('Method not implemented.')
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected txRemoveDoc (tx: TxRemoveDoc<Doc>): Promise<TxResult> {
|
2021-09-15 10:53:11 +00:00
|
|
|
throw new Error('Method not implemented.')
|
|
|
|
}
|
|
|
|
|
2021-10-13 16:46:48 +00:00
|
|
|
protected txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
2021-09-15 10:53:11 +00:00
|
|
|
throw new Error('Method not implemented.')
|
|
|
|
}
|
|
|
|
|
2022-04-14 05:30:30 +00:00
|
|
|
override async tx (tx: Tx, user: string): Promise<TxResult>
|
|
|
|
override async tx (tx: Tx): Promise<TxResult>
|
|
|
|
|
|
|
|
override async tx (tx: Tx, user?: string): Promise<TxResult> {
|
2021-12-22 09:02:51 +00:00
|
|
|
await this.txCollection().insertOne(translateDoc(tx))
|
2021-10-13 16:46:48 +00:00
|
|
|
return {}
|
2021-08-26 12:16:45 +00:00
|
|
|
}
|
|
|
|
|
2021-12-22 09:02:51 +00:00
|
|
|
private txCollection (): Collection {
|
|
|
|
if (this.txColl !== undefined) {
|
|
|
|
return this.txColl
|
|
|
|
}
|
|
|
|
this.txColl = this.db.collection(DOMAIN_TX)
|
|
|
|
return this.txColl
|
|
|
|
}
|
|
|
|
|
2021-08-26 12:16:45 +00:00
|
|
|
async getModel (): Promise<Tx[]> {
|
2022-04-08 03:06:38 +00:00
|
|
|
const model = await this.db
|
|
|
|
.collection(DOMAIN_TX)
|
|
|
|
.find<Tx>({ objectSpace: core.space.Model })
|
|
|
|
.sort({ _id: 1 })
|
|
|
|
.toArray()
|
2022-02-03 09:02:54 +00:00
|
|
|
// We need to put all core.account.System transactions first
|
|
|
|
const systemTr: Tx[] = []
|
|
|
|
const userTx: Tx[] = []
|
|
|
|
|
2022-04-08 03:06:38 +00:00
|
|
|
model.forEach((tx) => (tx.modifiedBy === core.account.System ? systemTr : userTx).push(tx))
|
2022-02-03 09:02:54 +00:00
|
|
|
|
|
|
|
return systemTr.concat(userTx)
|
2021-08-26 12:16:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
2021-12-02 09:07:25 +00:00
|
|
|
export async function createMongoAdapter (
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
url: string,
|
|
|
|
dbName: string,
|
|
|
|
modelDb: ModelDb
|
|
|
|
): Promise<DbAdapter> {
|
|
|
|
const client = await getMongoClient(url)
|
2021-08-26 12:16:45 +00:00
|
|
|
const db = client.db(dbName)
|
2022-01-12 09:06:04 +00:00
|
|
|
return new MongoAdapter(db, hierarchy, modelDb, client)
|
2021-08-26 12:16:45 +00:00
|
|
|
}
|
|
|
|
|
2021-08-18 19:46:59 +00:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
*/
|
2021-12-02 09:07:25 +00:00
|
|
|
export async function createMongoTxAdapter (
|
|
|
|
hierarchy: Hierarchy,
|
|
|
|
url: string,
|
|
|
|
dbName: string,
|
|
|
|
modelDb: ModelDb
|
|
|
|
): Promise<TxAdapter> {
|
|
|
|
const client = await getMongoClient(url)
|
2021-08-18 19:46:59 +00:00
|
|
|
const db = client.db(dbName)
|
2022-01-12 09:06:04 +00:00
|
|
|
return new MongoTxAdapter(db, hierarchy, modelDb, client)
|
2021-08-18 19:46:59 +00:00
|
|
|
}
|