mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
Low level bulk operations (#2592)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
59c53a09f2
commit
9fef46b059
@ -28,7 +28,8 @@ export default async () => {
|
||||
if (client === undefined) {
|
||||
client = await createClient(connect)
|
||||
for (const op of migrateOperations) {
|
||||
await op.upgrade(client)
|
||||
console.log('Migrate', op[0])
|
||||
await op[1].upgrade(client)
|
||||
}
|
||||
}
|
||||
// Check if we had dev hook for client.
|
||||
|
@ -44,8 +44,18 @@ class InMemoryTxAdapter extends DummyDbAdapter implements TxAdapter {
|
||||
return await this.txdb.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
tx (tx: Tx): Promise<TxResult> {
|
||||
return this.txdb.tx(tx)
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
const r: TxResult[] = []
|
||||
for (const t of tx) {
|
||||
r.push(await this.txdb.tx(t))
|
||||
}
|
||||
if (r.length === 1) {
|
||||
return r[0]
|
||||
}
|
||||
if (r.length === 0) {
|
||||
return {}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async init (model: Tx[]): Promise<void> {
|
||||
|
@ -63,7 +63,7 @@ function prepareTools (): {
|
||||
minio: MinioService
|
||||
txes: Tx[]
|
||||
version: Data<Version>
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
} {
|
||||
return { ...prepareToolsRaw(builder.getTxes()), version, migrateOperations }
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export function devTool (
|
||||
minio: MinioService
|
||||
txes: Tx[]
|
||||
version: Data<Version>
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
},
|
||||
productId: string
|
||||
): void {
|
||||
|
@ -105,7 +105,7 @@ export async function restoreWorkspace (
|
||||
elasticUrl: string,
|
||||
transactorUrl: string,
|
||||
rawTxes: Tx[],
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
): Promise<void> {
|
||||
console.log('Restoring workspace', mongoUrl, workspaceId, fileName)
|
||||
const client = new MongoClient(mongoUrl)
|
||||
|
@ -38,27 +38,27 @@ import { hrOperation } from '@hcengineering/model-hr'
|
||||
import { documentOperation } from '@hcengineering/model-document'
|
||||
import { bitrixOperation } from '@hcengineering/model-bitrix'
|
||||
|
||||
export const migrateOperations: MigrateOperation[] = [
|
||||
coreOperation,
|
||||
chunterOperation,
|
||||
demoOperation,
|
||||
gmailOperation,
|
||||
templatesOperation,
|
||||
telegramOperation,
|
||||
taskOperation,
|
||||
attachmentOperation,
|
||||
automationOperation,
|
||||
leadOperation,
|
||||
recruitOperation,
|
||||
viewOperation,
|
||||
contactOperation,
|
||||
tagsOperation,
|
||||
notificationOperation,
|
||||
settingOperation,
|
||||
trackerOperation,
|
||||
boardOperation,
|
||||
hrOperation,
|
||||
documentOperation,
|
||||
bitrixOperation,
|
||||
inventoryOperation
|
||||
export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['core', coreOperation],
|
||||
['chunter', chunterOperation],
|
||||
['demo', demoOperation],
|
||||
['gmail', gmailOperation],
|
||||
['templates', templatesOperation],
|
||||
['telegram', telegramOperation],
|
||||
['task', taskOperation],
|
||||
['attachment', attachmentOperation],
|
||||
['', automationOperation],
|
||||
['lead', leadOperation],
|
||||
['recruit', recruitOperation],
|
||||
['view', viewOperation],
|
||||
['contact', contactOperation],
|
||||
['tags', tagsOperation],
|
||||
['notification', notificationOperation],
|
||||
['setting', settingOperation],
|
||||
['tracker', trackerOperation],
|
||||
['board', boardOperation],
|
||||
['hr', hrOperation],
|
||||
['document', documentOperation],
|
||||
['bitrix', bitrixOperation],
|
||||
['inventiry', inventoryOperation]
|
||||
]
|
||||
|
@ -44,17 +44,27 @@ async function createSpace (tx: TxOperations): Promise<void> {
|
||||
}
|
||||
|
||||
async function setCreate (client: MigrationClient): Promise<void> {
|
||||
const docs = await client.find<Contact>(DOMAIN_CONTACT, {
|
||||
_class: { $in: [contact.class.Contact, contact.class.Organization, contact.class.Person, contact.class.Employee] },
|
||||
while (true) {
|
||||
const docs = await client.find<Contact>(
|
||||
DOMAIN_CONTACT,
|
||||
{
|
||||
_class: {
|
||||
$in: [contact.class.Contact, contact.class.Organization, contact.class.Person, contact.class.Employee]
|
||||
},
|
||||
createOn: { $exists: false }
|
||||
})
|
||||
for (const doc of docs) {
|
||||
const tx = (
|
||||
await client.find<TxCreateDoc<Contact>>(DOMAIN_TX, {
|
||||
objectId: doc._id,
|
||||
},
|
||||
{ limit: 500 }
|
||||
)
|
||||
if (docs.length === 0) {
|
||||
break
|
||||
}
|
||||
console.log('processing createOn migration', docs.length)
|
||||
const creates = await client.find<TxCreateDoc<Contact>>(DOMAIN_TX, {
|
||||
objectId: { $in: docs.map((it) => it._id) },
|
||||
_class: core.class.TxCreateDoc
|
||||
})
|
||||
)[0]
|
||||
for (const doc of docs) {
|
||||
const tx = creates.find((it) => it.objectId === doc._id)
|
||||
if (tx !== undefined) {
|
||||
await client.update(
|
||||
DOMAIN_CONTACT,
|
||||
@ -77,6 +87,7 @@ async function setCreate (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const contactOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
|
@ -264,30 +264,44 @@ export const DOMAIN_TX = 'tx' as Domain
|
||||
* @public
|
||||
*/
|
||||
export interface WithTx {
|
||||
tx: (tx: Tx) => Promise<TxResult>
|
||||
tx: (...txs: Tx[]) => Promise<TxResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export abstract class TxProcessor implements WithTx {
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
async tx (...txes: Tx[]): Promise<TxResult> {
|
||||
const result: TxResult[] = []
|
||||
for (const tx of txes) {
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc:
|
||||
return await this.txCreateDoc(tx as TxCreateDoc<Doc>)
|
||||
result.push(await this.txCreateDoc(tx as TxCreateDoc<Doc>))
|
||||
break
|
||||
case core.class.TxCollectionCUD:
|
||||
return await this.txCollectionCUD(tx as TxCollectionCUD<Doc, AttachedDoc>)
|
||||
result.push(await this.txCollectionCUD(tx as TxCollectionCUD<Doc, AttachedDoc>))
|
||||
break
|
||||
case core.class.TxUpdateDoc:
|
||||
return await this.txUpdateDoc(tx as TxUpdateDoc<Doc>)
|
||||
result.push(await this.txUpdateDoc(tx as TxUpdateDoc<Doc>))
|
||||
break
|
||||
case core.class.TxRemoveDoc:
|
||||
return await this.txRemoveDoc(tx as TxRemoveDoc<Doc>)
|
||||
result.push(await this.txRemoveDoc(tx as TxRemoveDoc<Doc>))
|
||||
break
|
||||
case core.class.TxMixin:
|
||||
return await this.txMixin(tx as TxMixin<Doc, Doc>)
|
||||
result.push(await this.txMixin(tx as TxMixin<Doc, Doc>))
|
||||
break
|
||||
case core.class.TxApplyIf:
|
||||
// Apply if processed on server
|
||||
return await Promise.resolve({})
|
||||
}
|
||||
throw new Error('TxProcessor: unhandled transaction class: ' + tx._class)
|
||||
}
|
||||
if (result.length === 0) {
|
||||
return {}
|
||||
}
|
||||
if (result.length === 1) {
|
||||
return result[0]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static createDoc2Doc<T extends Doc>(tx: TxCreateDoc<T>): T {
|
||||
|
@ -70,7 +70,9 @@
|
||||
$: if (_id && _class) {
|
||||
query.query(_class, { _id }, (result) => {
|
||||
object = result[0]
|
||||
if (object != null) {
|
||||
realObjectClass = object._class
|
||||
}
|
||||
})
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
|
@ -14,41 +14,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Doc, Ref, Tx, TxCollectionCUD, TxCreateDoc, TxRemoveDoc } from '@hcengineering/core'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import type { Doc, Ref, Tx, TxRemoveDoc } from '@hcengineering/core'
|
||||
import core, { TxProcessor } from '@hcengineering/core'
|
||||
|
||||
const findCreateTx = async (
|
||||
id: Ref<Attachment>,
|
||||
findAll: TriggerControl['findAll']
|
||||
): Promise<TxCreateDoc<Attachment> | undefined> => {
|
||||
const createTx = (await findAll<TxCreateDoc<Attachment>>(core.class.TxCreateDoc, { objectId: id }))[0]
|
||||
|
||||
if (createTx !== undefined) {
|
||||
return createTx
|
||||
}
|
||||
|
||||
const colTx = (
|
||||
await findAll<TxCollectionCUD<Doc, Attachment>>(core.class.TxCollectionCUD, {
|
||||
'tx._class': core.class.TxCreateDoc,
|
||||
'tx.objectClass': attachment.class.Attachment,
|
||||
'tx.objectId': id
|
||||
})
|
||||
)[0]
|
||||
|
||||
if (colTx === undefined) return
|
||||
|
||||
return colTx.tx as TxCreateDoc<Attachment>
|
||||
}
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnAttachmentDelete (
|
||||
tx: Tx,
|
||||
{ findAll, hierarchy, fulltextFx, storageFx }: TriggerControl
|
||||
{ findAll, hierarchy, fulltextFx, storageFx, removedMap }: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxRemoveDoc) {
|
||||
@ -61,14 +38,12 @@ export async function OnAttachmentDelete (
|
||||
return []
|
||||
}
|
||||
|
||||
const createTx = await findCreateTx(rmTx.objectId, findAll)
|
||||
// Obtain document being deleted.
|
||||
const attach = removedMap.get(rmTx.objectId) as Attachment
|
||||
|
||||
if (createTx === undefined) {
|
||||
if (attach === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const attach = TxProcessor.createDoc2Doc(createTx)
|
||||
|
||||
fulltextFx(async (adapter) => {
|
||||
await adapter.remove([attach.file as Ref<Doc>])
|
||||
})
|
||||
|
@ -15,7 +15,7 @@
|
||||
//
|
||||
|
||||
import contact, { Contact, contactId, formatName, Organization, Person } from '@hcengineering/contact'
|
||||
import core, { concatLink, Doc, Tx, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core'
|
||||
import core, { concatLink, Doc, Tx, TxRemoveDoc } from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
@ -25,7 +25,10 @@ import { workbenchId } from '@hcengineering/workbench'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnContactDelete (tx: Tx, { findAll, hierarchy, storageFx }: TriggerControl): Promise<Tx[]> {
|
||||
export async function OnContactDelete (
|
||||
tx: Tx,
|
||||
{ findAll, hierarchy, storageFx, removedMap }: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
if (tx._class !== core.class.TxRemoveDoc) {
|
||||
return []
|
||||
}
|
||||
@ -36,15 +39,12 @@ export async function OnContactDelete (tx: Tx, { findAll, hierarchy, storageFx }
|
||||
return []
|
||||
}
|
||||
|
||||
const createTx = (await findAll<TxCreateDoc<Contact>>(core.class.TxCreateDoc, { objectId: rmTx.objectId }))[0]
|
||||
if (createTx === undefined) {
|
||||
const removeContact = removedMap.get(rmTx.objectId) as Contact
|
||||
if (removeContact === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const updateTxes = await findAll<TxUpdateDoc<Contact>>(core.class.TxUpdateDoc, { objectId: rmTx.objectId })
|
||||
const avatar: string | undefined = [createTx.attributes.avatar, ...updateTxes.map((x) => x.operations.avatar)]
|
||||
.filter((x): x is string => x !== undefined)
|
||||
.slice(-1)[0]
|
||||
const avatar: string | undefined = [removeContact.avatar].filter((x): x is string => x !== undefined).slice(-1)[0]
|
||||
|
||||
if (avatar === undefined) {
|
||||
return []
|
||||
|
@ -64,18 +64,16 @@ export async function onTagReference (tx: Tx, control: TriggerControl): Promise<
|
||||
}
|
||||
if (isRemove) {
|
||||
const ctx = actualTx as TxRemoveDoc<TagReference>
|
||||
const createTx = (
|
||||
await control.findAll(core.class.TxCollectionCUD, { 'tx.objectId': ctx.objectId }, { limit: 1 })
|
||||
)[0]
|
||||
if (createTx !== undefined) {
|
||||
const actualCreateTx = TxProcessor.extractTx(createTx)
|
||||
const doc = TxProcessor.createDoc2Doc(actualCreateTx as TxCreateDoc<TagReference>)
|
||||
const doc = control.removedMap.get(ctx.objectId) as TagReference
|
||||
if (doc !== undefined) {
|
||||
if (!control.removedMap.has(doc.tag)) {
|
||||
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
|
||||
$inc: { refCount: -1 }
|
||||
})
|
||||
return [res]
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -393,7 +393,7 @@ export async function listAccounts (db: Db): Promise<Account[]> {
|
||||
export async function createWorkspace (
|
||||
version: Data<Version>,
|
||||
txes: Tx[],
|
||||
migrationOperation: MigrateOperation[],
|
||||
migrationOperation: [string, MigrateOperation][],
|
||||
db: Db,
|
||||
productId: string,
|
||||
workspace: string,
|
||||
@ -421,7 +421,7 @@ export async function createWorkspace (
|
||||
export async function upgradeWorkspace (
|
||||
version: Data<Version>,
|
||||
txes: Tx[],
|
||||
migrationOperation: MigrateOperation[],
|
||||
migrationOperation: [string, MigrateOperation][],
|
||||
productId: string,
|
||||
db: Db,
|
||||
workspace: string
|
||||
@ -449,7 +449,7 @@ export async function upgradeWorkspace (
|
||||
* @public
|
||||
*/
|
||||
export const createUserWorkspace =
|
||||
(version: Data<Version>, txes: Tx[], migrationOperation: MigrateOperation[]) =>
|
||||
(version: Data<Version>, txes: Tx[], migrationOperation: [string, MigrateOperation][]) =>
|
||||
async (db: Db, productId: string, token: string, workspace: string): Promise<LoginInfo> => {
|
||||
const { email } = decodeToken(token)
|
||||
await createWorkspace(version, txes, migrationOperation, db, productId, workspace, '')
|
||||
@ -925,7 +925,7 @@ function wrap (f: (db: Db, productId: string, ...args: any[]) => Promise<any>):
|
||||
export function getMethods (
|
||||
version: Data<Version>,
|
||||
txes: Tx[],
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
): Record<string, AccountMethod> {
|
||||
return {
|
||||
login: wrap(login),
|
||||
|
@ -46,7 +46,7 @@ export interface DbAdapter {
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<FindResult<T>>
|
||||
tx: (tx: Tx) => Promise<TxResult>
|
||||
tx: (...tx: Tx[]) => Promise<TxResult>
|
||||
|
||||
find: (domain: Domain) => StorageIterator
|
||||
|
||||
@ -97,7 +97,7 @@ export class DummyDbAdapter implements DbAdapter {
|
||||
return toFindResult([])
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -137,8 +137,8 @@ class InMemoryAdapter extends DummyDbAdapter implements DbAdapter {
|
||||
return await this.modeldb.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
return await this.modeldb.tx(tx)
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
return await this.modeldb.tx(...tx)
|
||||
}
|
||||
|
||||
async init (model: Tx[]): Promise<void> {
|
||||
|
@ -39,10 +39,8 @@ import core, {
|
||||
Tx,
|
||||
TxApplyIf,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxFactory,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxResult,
|
||||
@ -64,6 +62,7 @@ import type {
|
||||
FullTextAdapterFactory,
|
||||
ObjectDDParticipant
|
||||
} from './types'
|
||||
import { createCacheFindAll } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -138,16 +137,50 @@ class TServerStorage implements ServerStorage {
|
||||
return adapter
|
||||
}
|
||||
|
||||
private async routeTx (ctx: MeasureContext, tx: Tx): Promise<TxResult> {
|
||||
if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) {
|
||||
const txCUD = tx as TxCUD<Doc>
|
||||
const domain = this.hierarchy.getDomain(txCUD.objectClass)
|
||||
const adapter = this.getAdapter(domain)
|
||||
const res = await adapter.tx(txCUD)
|
||||
return res
|
||||
private async routeTx (ctx: MeasureContext, ...txes: Tx[]): Promise<TxResult> {
|
||||
let part: TxCUD<Doc>[] = []
|
||||
let lastDomain: Domain | undefined
|
||||
const result: TxResult[] = []
|
||||
const processPart = async (): Promise<void> => {
|
||||
if (part.length > 0) {
|
||||
const adapter = this.getAdapter(lastDomain as Domain)
|
||||
const r = await adapter.tx(...part)
|
||||
if (Array.isArray(r)) {
|
||||
result.push(...r)
|
||||
} else {
|
||||
result.push(r)
|
||||
}
|
||||
part = []
|
||||
}
|
||||
}
|
||||
for (const tx of txes) {
|
||||
const txCUD = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (!this.hierarchy.isDerived(txCUD._class, core.class.TxCUD)) {
|
||||
// Skip unsupported tx
|
||||
console.error('Unsupported transacton', tx)
|
||||
continue
|
||||
}
|
||||
const domain = this.hierarchy.getDomain(txCUD.objectClass)
|
||||
if (part.length > 0) {
|
||||
if (lastDomain !== domain) {
|
||||
await processPart()
|
||||
}
|
||||
lastDomain = domain
|
||||
part.push(txCUD)
|
||||
} else {
|
||||
lastDomain = domain
|
||||
part.push(txCUD)
|
||||
}
|
||||
}
|
||||
await processPart()
|
||||
|
||||
if (result.length === 1) {
|
||||
return result[0]
|
||||
}
|
||||
if (result.length === 0) {
|
||||
return [{}, false]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async getCollectionUpdateTx<D extends Doc>(
|
||||
@ -174,7 +207,7 @@ class TServerStorage implements ServerStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCollection (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
private async updateCollection (ctx: MeasureContext, tx: Tx, findAll: ServerStorage['findAll']): Promise<Tx[]> {
|
||||
if (tx._class !== core.class.TxCollectionCUD) {
|
||||
return []
|
||||
}
|
||||
@ -195,7 +228,7 @@ class TServerStorage implements ServerStorage {
|
||||
return []
|
||||
}
|
||||
|
||||
const oldAttachedTo = (await this.findAll(ctx, _class, { _id }, { limit: 1 }))[0]
|
||||
const oldAttachedTo = (await findAll(ctx, _class, { _id }, { limit: 1 }))[0]
|
||||
let oldTx: Tx | null = null
|
||||
if (oldAttachedTo !== undefined) {
|
||||
const attr = this.hierarchy.getAttribute(oldAttachedTo._class, colTx.collection)
|
||||
@ -209,7 +242,7 @@ class TServerStorage implements ServerStorage {
|
||||
|
||||
const newAttachedToClass = operations.attachedToClass ?? _class
|
||||
const newAttachedToCollection = operations.collection ?? colTx.collection
|
||||
const newAttachedTo = (await this.findAll(ctx, newAttachedToClass, { _id: operations.attachedTo }, { limit: 1 }))[0]
|
||||
const newAttachedTo = (await findAll(ctx, newAttachedToClass, { _id: operations.attachedTo }, { limit: 1 }))[0]
|
||||
let newTx: Tx | null = null
|
||||
const newAttr = this.hierarchy.getAttribute(newAttachedToClass, newAttachedToCollection)
|
||||
if (newAttachedTo !== undefined && newAttr !== undefined) {
|
||||
@ -226,7 +259,14 @@ class TServerStorage implements ServerStorage {
|
||||
return [...(oldTx !== null ? [oldTx] : []), ...(newTx !== null ? [newTx] : [])]
|
||||
}
|
||||
|
||||
private async processCollection (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
private async processCollection (
|
||||
ctx: MeasureContext,
|
||||
txes: Tx[],
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const colTx = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
const _id = colTx.objectId
|
||||
@ -235,28 +275,29 @@ class TServerStorage implements ServerStorage {
|
||||
// Skip model operations
|
||||
if (this.hierarchy.getDomain(_class) === DOMAIN_MODEL) {
|
||||
// We could not update increments for model classes
|
||||
return []
|
||||
continue
|
||||
}
|
||||
|
||||
const isCreateTx = colTx.tx._class === core.class.TxCreateDoc
|
||||
const isDeleteTx = colTx.tx._class === core.class.TxRemoveDoc
|
||||
const isUpdateTx = colTx.tx._class === core.class.TxUpdateDoc
|
||||
if (isUpdateTx) {
|
||||
return await this.updateCollection(ctx, tx)
|
||||
result.push(...(await this.updateCollection(ctx, tx, findAll)))
|
||||
}
|
||||
|
||||
if (isCreateTx || isDeleteTx) {
|
||||
const attachedTo = (await this.findAll(ctx, _class, { _id }, { limit: 1 }))[0]
|
||||
if ((isCreateTx || isDeleteTx) && !removedMap.has(_id)) {
|
||||
const attachedTo = (await findAll(ctx, _class, { _id }, { limit: 1 }))[0]
|
||||
if (attachedTo !== undefined) {
|
||||
return [
|
||||
result.push(
|
||||
await this.getCollectionUpdateTx(_id, _class, tx.modifiedBy, colTx.modifiedOn, attachedTo, {
|
||||
$inc: { [colTx.collection]: isCreateTx ? 1 : -1 }
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
@ -299,49 +340,95 @@ class TServerStorage implements ServerStorage {
|
||||
)
|
||||
}
|
||||
|
||||
private async buildRemovedDoc (ctx: MeasureContext, tx: TxRemoveDoc<Doc>): Promise<Doc | undefined> {
|
||||
const isAttached = this.hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc)
|
||||
const txes = await this.findAll<TxCUD<Doc>>(
|
||||
private async buildRemovedDoc (ctx: MeasureContext, rawTxes: Tx[], findAll: ServerStorage['findAll']): Promise<Doc[]> {
|
||||
const removeObjectIds: Ref<Doc>[] = []
|
||||
const removeAttachObjectIds: Ref<AttachedDoc>[] = []
|
||||
|
||||
const removeTxes = rawTxes
|
||||
.filter((it) => this.hierarchy.isDerived(it._class, core.class.TxRemoveDoc))
|
||||
.map((it) => TxProcessor.extractTx(it) as TxRemoveDoc<Doc>)
|
||||
|
||||
for (const rtx of removeTxes) {
|
||||
const isAttached = this.hierarchy.isDerived(rtx.objectClass, core.class.AttachedDoc)
|
||||
if (isAttached) {
|
||||
removeAttachObjectIds.push(rtx.objectId as Ref<AttachedDoc>)
|
||||
} else {
|
||||
removeObjectIds.push(rtx.objectId)
|
||||
}
|
||||
}
|
||||
|
||||
const txes =
|
||||
removeObjectIds.length > 0
|
||||
? await findAll<TxCUD<Doc>>(
|
||||
ctx,
|
||||
isAttached ? core.class.TxCollectionCUD : core.class.TxCUD,
|
||||
isAttached
|
||||
? { 'tx.objectId': tx.objectId as Ref<AttachedDoc> }
|
||||
: {
|
||||
objectId: tx.objectId
|
||||
core.class.TxCUD,
|
||||
{
|
||||
objectId: { $in: removeObjectIds }
|
||||
},
|
||||
{ sort: { modifiedOn: 1 } }
|
||||
)
|
||||
const createTx = isAttached
|
||||
? txes.find((tx) => (tx as TxCollectionCUD<Doc, AttachedDoc>).tx._class === core.class.TxCreateDoc)
|
||||
: txes.find((tx) => tx._class === core.class.TxCreateDoc)
|
||||
if (createTx === undefined) return
|
||||
let doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<Doc>)
|
||||
for (let tx of txes) {
|
||||
tx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc<Doc>)
|
||||
} else if (tx._class === core.class.TxMixin) {
|
||||
const mixinTx = tx as TxMixin<Doc, Doc>
|
||||
doc = TxProcessor.updateMixin4Doc(doc, mixinTx)
|
||||
: []
|
||||
const result: Doc[] = []
|
||||
|
||||
const txesAttach =
|
||||
removeAttachObjectIds.length > 0
|
||||
? await findAll<TxCollectionCUD<Doc, AttachedDoc>>(
|
||||
ctx,
|
||||
core.class.TxCollectionCUD,
|
||||
{ 'tx.objectId': { $in: removeAttachObjectIds } },
|
||||
{ sort: { modifiedOn: 1 } }
|
||||
)
|
||||
: []
|
||||
|
||||
for (const rtx of removeTxes) {
|
||||
const isAttached = this.hierarchy.isDerived(rtx.objectClass, core.class.AttachedDoc)
|
||||
|
||||
const objTxex = isAttached
|
||||
? txesAttach.filter((tx) => tx.tx.objectId === rtx.objectId)
|
||||
: txes.filter((it) => it.objectId === rtx.objectId)
|
||||
|
||||
const doc = TxProcessor.buildDoc2Doc(objTxex)
|
||||
if (doc !== undefined) {
|
||||
result.push(doc)
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
return result
|
||||
}
|
||||
|
||||
private async processRemove (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) return []
|
||||
const rtx = actualTx as TxRemoveDoc<Doc>
|
||||
private async processRemove (
|
||||
ctx: MeasureContext,
|
||||
txes: Tx[],
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
const object = await this.buildRemovedDoc(ctx, rtx)
|
||||
if (object === undefined) return []
|
||||
result.push(...(await this.deleteClassCollections(ctx, object._class, rtx.objectId)))
|
||||
|
||||
const objects = await this.buildRemovedDoc(ctx, txes, findAll)
|
||||
for (const obj of objects) {
|
||||
removedMap.set(obj._id, obj)
|
||||
}
|
||||
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) {
|
||||
continue
|
||||
}
|
||||
const rtx = actualTx as TxRemoveDoc<Doc>
|
||||
const object = removedMap.get(rtx.objectId)
|
||||
if (object === undefined) {
|
||||
continue
|
||||
}
|
||||
result.push(...(await this.deleteClassCollections(ctx, object._class, rtx.objectId, findAll, removedMap)))
|
||||
const mixins = this.getMixins(object._class, object)
|
||||
for (const mixin of mixins) {
|
||||
result.push(...(await this.deleteClassCollections(ctx, mixin, rtx.objectId, object._class)))
|
||||
result.push(
|
||||
...(await this.deleteClassCollections(ctx, mixin, rtx.objectId, findAll, removedMap, object._class))
|
||||
)
|
||||
}
|
||||
|
||||
result.push(...(await this.deleteRelatedDocuments(ctx, object, findAll, removedMap)))
|
||||
}
|
||||
result.push(...(await this.deleteRelatedDocuments(ctx, object)))
|
||||
return result
|
||||
}
|
||||
|
||||
@ -349,6 +436,8 @@ class TServerStorage implements ServerStorage {
|
||||
ctx: MeasureContext,
|
||||
_class: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>,
|
||||
to?: Ref<Class<Doc>>
|
||||
): Promise<Tx[]> {
|
||||
const attributes = this.hierarchy.getAllAttributes(_class, to)
|
||||
@ -356,18 +445,18 @@ class TServerStorage implements ServerStorage {
|
||||
for (const attribute of attributes) {
|
||||
if (this.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) {
|
||||
const collection = attribute[1].type as Collection<AttachedDoc>
|
||||
const allAttached = await this.findAll(ctx, collection.of, { attachedTo: objectId })
|
||||
const allAttached = await findAll(ctx, collection.of, { attachedTo: objectId })
|
||||
for (const attached of allAttached) {
|
||||
result.push(...(await this.deleteObject(ctx, attached)))
|
||||
result.push(...this.deleteObject(ctx, attached, removedMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async deleteObject (ctx: MeasureContext, object: Doc): Promise<Tx[]> {
|
||||
private deleteObject (ctx: MeasureContext, object: Doc, removedMap: Map<Ref<Doc>, Doc>): Tx[] {
|
||||
const result: Tx[] = []
|
||||
const factory = new TxFactory(core.account.System)
|
||||
const factory = new TxFactory(object.modifiedBy)
|
||||
if (this.hierarchy.isDerived(object._class, core.class.AttachedDoc)) {
|
||||
const adoc = object as AttachedDoc
|
||||
const nestedTx = factory.createTxRemoveDoc(adoc._class, adoc.space, adoc._id)
|
||||
@ -378,14 +467,21 @@ class TServerStorage implements ServerStorage {
|
||||
adoc.collection,
|
||||
nestedTx
|
||||
)
|
||||
removedMap.set(adoc._id, adoc)
|
||||
result.push(tx)
|
||||
} else {
|
||||
result.push(factory.createTxRemoveDoc(object._class, object.space, object._id))
|
||||
removedMap.set(object._id, object)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async deleteRelatedDocuments (ctx: MeasureContext, object: Doc): Promise<Tx[]> {
|
||||
private async deleteRelatedDocuments (
|
||||
ctx: MeasureContext,
|
||||
object: Doc,
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
const objectClass = this.hierarchy.getClass(object._class)
|
||||
if (this.hierarchy.hasMixin(objectClass, serverCore.mixin.ObjectDDParticipant)) {
|
||||
@ -395,39 +491,48 @@ class TServerStorage implements ServerStorage {
|
||||
)
|
||||
const collector = await getResource(removeParticipand.collectDocs)
|
||||
const docs = await collector(object, this.hierarchy, async (_class, query, options) => {
|
||||
return await this.findAll(ctx, _class, query, options)
|
||||
return await findAll(ctx, _class, query, options)
|
||||
})
|
||||
for (const d of docs) {
|
||||
result.push(...(await this.deleteObject(ctx, d)))
|
||||
result.push(...this.deleteObject(ctx, d, removedMap))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async processMove (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
|
||||
const rtx = actualTx as TxUpdateDoc<Doc>
|
||||
if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) return []
|
||||
private async processMove (ctx: MeasureContext, txes: Tx[], findAll: ServerStorage['findAll']): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
const factory = new TxFactory(core.account.System)
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) {
|
||||
continue
|
||||
}
|
||||
const rtx = actualTx as TxUpdateDoc<Doc>
|
||||
if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) {
|
||||
continue
|
||||
}
|
||||
const factory = new TxFactory(tx.modifiedBy)
|
||||
for (const [, attribute] of this.hierarchy.getAllAttributes(rtx.objectClass)) {
|
||||
if (!this.hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
|
||||
if (!this.hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
||||
continue
|
||||
}
|
||||
const collection = attribute.type as Collection<AttachedDoc>
|
||||
const allAttached = await this.findAll(ctx, collection.of, { attachedTo: rtx.objectId, space: rtx.objectSpace })
|
||||
const allAttached = await findAll(ctx, collection.of, { attachedTo: rtx.objectId, space: rtx.objectSpace })
|
||||
const allTx = allAttached.map(({ _class, space, _id }) =>
|
||||
factory.createTxUpdateDoc(_class, space, _id, { space: rtx.operations.space })
|
||||
)
|
||||
result.push(...allTx)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async proccessDerived (
|
||||
ctx: MeasureContext,
|
||||
tx: Tx,
|
||||
_class: Ref<Class<Tx>>,
|
||||
triggerFx: Effects
|
||||
txes: Tx[],
|
||||
triggerFx: Effects,
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
): Promise<Tx[]> {
|
||||
const fAll =
|
||||
(mctx: MeasureContext) =>
|
||||
@ -436,13 +541,20 @@ class TServerStorage implements ServerStorage {
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> =>
|
||||
this.findAll(mctx, clazz, query, options)
|
||||
const derived = [
|
||||
...(await ctx.with('process-collection', { _class }, () => this.processCollection(ctx, tx))),
|
||||
...(await ctx.with('process-remove', { _class }, () => this.processRemove(ctx, tx))),
|
||||
...(await ctx.with('process-move', { _class }, () => this.processMove(ctx, tx))),
|
||||
...(await ctx.with('process-triggers', {}, (ctx) =>
|
||||
this.triggers.apply(tx.modifiedBy, tx, {
|
||||
findAll(mctx, clazz, query, options)
|
||||
|
||||
const removed = await ctx.with('process-remove', {}, () => this.processRemove(ctx, txes, findAll, removedMap))
|
||||
const collections = await ctx.with('process-collection', {}, () =>
|
||||
this.processCollection(ctx, txes, findAll, removedMap)
|
||||
)
|
||||
const moves = await ctx.with('process-move', {}, () => this.processMove(ctx, txes, findAll))
|
||||
|
||||
const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
result.push(
|
||||
...(await this.triggers.apply(tx.modifiedBy, tx, {
|
||||
removedMap,
|
||||
workspace: this.workspace,
|
||||
fx: triggerFx.fx,
|
||||
fulltextFx: (f) => triggerFx.fx(() => f(this.fulltextAdapter)),
|
||||
@ -460,24 +572,31 @@ class TServerStorage implements ServerStorage {
|
||||
txFx: async (f) => {
|
||||
await f(this.getAdapter(DOMAIN_TX))
|
||||
}
|
||||
}))
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
))
|
||||
]
|
||||
|
||||
return await this.processDerivedTxes(derived, ctx, triggerFx)
|
||||
const derived = [...removed, ...collections, ...moves, ...triggers]
|
||||
|
||||
return await this.processDerivedTxes(derived, ctx, triggerFx, findAll, removedMap)
|
||||
}
|
||||
|
||||
private async processDerivedTxes (derived: Tx[], ctx: MeasureContext, triggerFx: Effects): Promise<Tx[]> {
|
||||
private async processDerivedTxes (
|
||||
derived: Tx[],
|
||||
ctx: MeasureContext,
|
||||
triggerFx: Effects,
|
||||
findAll: ServerStorage['findAll'],
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
): Promise<Tx[]> {
|
||||
derived.sort((a, b) => a.modifiedOn - b.modifiedOn)
|
||||
|
||||
for (const tx of derived) {
|
||||
await ctx.with('derived-route-tx', { _class: txClass(tx) }, (ctx) => this.routeTx(ctx, tx))
|
||||
}
|
||||
await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived))
|
||||
|
||||
const nestedTxes: Tx[] = []
|
||||
for (const tx of derived) {
|
||||
const _class = txClass(tx)
|
||||
nestedTxes.push(...(await this.proccessDerived(ctx, tx, _class, triggerFx)))
|
||||
if (derived.length > 0) {
|
||||
nestedTxes.push(...(await this.proccessDerived(ctx, derived, triggerFx, findAll, removedMap)))
|
||||
}
|
||||
|
||||
const res = [...derived, ...nestedTxes]
|
||||
@ -490,7 +609,8 @@ class TServerStorage implements ServerStorage {
|
||||
*/
|
||||
async verifyApplyIf (
|
||||
ctx: MeasureContext,
|
||||
applyIf: TxApplyIf
|
||||
applyIf: TxApplyIf,
|
||||
findAll: ServerStorage['findAll']
|
||||
): Promise<{
|
||||
onEnd: () => void
|
||||
passed: boolean
|
||||
@ -510,7 +630,7 @@ class TServerStorage implements ServerStorage {
|
||||
)
|
||||
let passed = true
|
||||
for (const { _class, query } of applyIf.match) {
|
||||
const res = await this.findAll(ctx, _class, query, { limit: 1 })
|
||||
const res = await findAll(ctx, _class, query, { limit: 1 })
|
||||
if (res.length === 0) {
|
||||
passed = false
|
||||
break
|
||||
@ -522,6 +642,7 @@ class TServerStorage implements ServerStorage {
|
||||
async tx (ctx: MeasureContext, tx: Tx): Promise<[TxResult, Tx[]]> {
|
||||
// store tx
|
||||
const _class = txClass(tx)
|
||||
const cacheFind = createCacheFindAll(this)
|
||||
const objClass = txObjectClass(tx)
|
||||
return await ctx.with('tx', { _class, objClass }, async (ctx) => {
|
||||
if (tx.space !== core.space.DerivedTx && !this.hierarchy.isDerived(tx._class, core.class.TxApplyIf)) {
|
||||
@ -546,23 +667,21 @@ class TServerStorage implements ServerStorage {
|
||||
const applyIf = tx as TxApplyIf
|
||||
// Wait for scope promise if found
|
||||
let passed: boolean
|
||||
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf))
|
||||
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, cacheFind))
|
||||
result = passed
|
||||
if (passed) {
|
||||
// Store apply if transaction's
|
||||
await ctx.with('domain-tx', { _class, objClass }, async () => {
|
||||
const atx = await this.getAdapter(DOMAIN_TX)
|
||||
for (const ctx of applyIf.txes) {
|
||||
await atx.tx(ctx)
|
||||
}
|
||||
await atx.tx(...applyIf.txes)
|
||||
})
|
||||
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx)
|
||||
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx, cacheFind, new Map<Ref<Doc>, Doc>())
|
||||
}
|
||||
} else {
|
||||
// store object
|
||||
result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, tx))
|
||||
// invoke triggers and store derived objects
|
||||
derived = await this.proccessDerived(ctx, tx, _class, triggerFx)
|
||||
derived = await this.proccessDerived(ctx, [tx], triggerFx, cacheFind, new Map<Ref<Doc>, Doc>())
|
||||
}
|
||||
|
||||
// index object
|
||||
|
@ -105,6 +105,7 @@ export interface TriggerControl {
|
||||
findAll: Storage['findAll']
|
||||
hierarchy: Hierarchy
|
||||
modelDb: ModelDb
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
|
||||
fulltextFx: (f: (adapter: FullTextAdapter) => Promise<void>) => void
|
||||
// Since we don't have other storages let's consider adapter is MinioClient
|
||||
|
34
server/core/src/utils.ts
Normal file
34
server/core/src/utils.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
FindResult,
|
||||
MeasureContext,
|
||||
Ref,
|
||||
ServerStorage
|
||||
} from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createCacheFindAll (storage: ServerStorage): ServerStorage['findAll'] {
|
||||
// We will cache all queries for same objects for all derived data checks.
|
||||
const queryCache = new Map<string, FindResult<Doc>>()
|
||||
|
||||
return async <T extends Doc>(
|
||||
ctx: MeasureContext,
|
||||
clazz: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> => {
|
||||
const key = JSON.stringify(clazz) + JSON.stringify(query) + JSON.stringify(options)
|
||||
let cacheResult = queryCache.get(key)
|
||||
if (cacheResult !== undefined) {
|
||||
return cacheResult as FindResult<T>
|
||||
}
|
||||
cacheResult = await storage.findAll(ctx, clazz, query, options)
|
||||
queryCache.set(key, cacheResult)
|
||||
return cacheResult as FindResult<T>
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ class ElasticDataAdapter implements DbAdapter {
|
||||
return Object.assign([], { total: 0 })
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
@ -38,6 +39,7 @@ import core, {
|
||||
StorageIterator,
|
||||
toFindResult,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
@ -48,7 +50,7 @@ import core, {
|
||||
WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import type { DbAdapter, TxAdapter } from '@hcengineering/server-core'
|
||||
import { Collection, Db, Document, Filter, MongoClient, Sort, UpdateFilter } from 'mongodb'
|
||||
import { AnyBulkWriteOperation, Collection, Db, Document, Filter, MongoClient, Sort, UpdateFilter } from 'mongodb'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { getMongoClient, getWorkspaceDB } from './utils'
|
||||
|
||||
@ -80,18 +82,20 @@ interface LookupStep {
|
||||
pipeline?: any
|
||||
}
|
||||
|
||||
abstract class MongoAdapterBase extends TxProcessor {
|
||||
abstract class MongoAdapterBase implements DbAdapter {
|
||||
constructor (
|
||||
protected readonly db: Db,
|
||||
protected readonly hierarchy: Hierarchy,
|
||||
protected readonly modelDb: ModelDb,
|
||||
protected readonly client: MongoClient
|
||||
) {
|
||||
super()
|
||||
}
|
||||
) {}
|
||||
|
||||
async init (): Promise<void> {}
|
||||
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
return {}
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
}
|
||||
@ -105,14 +109,7 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const keys = Object.keys(value)
|
||||
if (keys[0] === '$like') {
|
||||
const pattern = value.$like as string
|
||||
translated[tkey] = {
|
||||
$regex: `^${pattern
|
||||
.split('%')
|
||||
.map((it) => escapeLikeForRegexp(it))
|
||||
.join('.*')}$`,
|
||||
$options: 'i'
|
||||
}
|
||||
translated[tkey] = translateLikeQuery(value.$like as string)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -130,7 +127,6 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
// Add an mixin to be exists flag
|
||||
translated[clazz] = { $exists: true }
|
||||
}
|
||||
// return Object.assign({}, query, { _class: { $in: classes } })
|
||||
return translated
|
||||
}
|
||||
|
||||
@ -522,9 +518,6 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
}
|
||||
}))
|
||||
)
|
||||
|
||||
// await coll.deleteMany({ _id: { $in: keys } })
|
||||
// await coll.insertMany(Array.from(docMap.values()) as Document[])
|
||||
}
|
||||
}
|
||||
|
||||
@ -555,75 +548,173 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
class MongoAdapter extends MongoAdapterBase {
|
||||
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 {}
|
||||
interface DomainOperation {
|
||||
raw: () => Promise<TxResult>
|
||||
domain: Domain
|
||||
bulk?: AnyBulkWriteOperation[]
|
||||
}
|
||||
|
||||
protected async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
class MongoAdapter extends MongoAdapterBase {
|
||||
getOperations (tx: Tx): DomainOperation | undefined {
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc:
|
||||
return this.txCreateDoc(tx as TxCreateDoc<Doc>)
|
||||
case core.class.TxCollectionCUD:
|
||||
return this.txCollectionCUD(tx as TxCollectionCUD<Doc, AttachedDoc>)
|
||||
case core.class.TxUpdateDoc:
|
||||
return this.txUpdateDoc(tx as TxUpdateDoc<Doc>)
|
||||
case core.class.TxRemoveDoc:
|
||||
return this.txRemoveDoc(tx as TxRemoveDoc<Doc>)
|
||||
case core.class.TxMixin:
|
||||
return this.txMixin(tx as TxMixin<Doc, Doc>)
|
||||
case core.class.TxApplyIf:
|
||||
return undefined
|
||||
}
|
||||
|
||||
console.error('Unknown/Unsupported operation:', tx._class, tx)
|
||||
}
|
||||
|
||||
async tx (...txes: Tx[]): Promise<TxResult> {
|
||||
const result: TxResult[] = []
|
||||
|
||||
const bulkOperations: DomainOperation[] = []
|
||||
|
||||
let lastDomain: Domain | undefined
|
||||
|
||||
const bulkExecute = async (): Promise<void> => {
|
||||
if (lastDomain === undefined || bulkOperations.length === 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.db
|
||||
.collection(lastDomain)
|
||||
.bulkWrite(bulkOperations.reduce<AnyBulkWriteOperation[]>((ops, op) => ops.concat(...(op.bulk ?? [])), []))
|
||||
} catch (err: any) {
|
||||
console.trace(err)
|
||||
throw err
|
||||
}
|
||||
bulkOperations.splice(0, bulkOperations.length)
|
||||
lastDomain = undefined
|
||||
}
|
||||
|
||||
if (txes.length > 1) {
|
||||
for (const tx of txes) {
|
||||
const dop: DomainOperation | undefined = this.getOperations(tx)
|
||||
if (dop === undefined) {
|
||||
continue
|
||||
}
|
||||
if (dop.bulk === undefined) {
|
||||
// Execute previous bulk and capture result.
|
||||
await bulkExecute()
|
||||
try {
|
||||
result.push(await dop.raw())
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (lastDomain === undefined) {
|
||||
lastDomain = dop.domain
|
||||
}
|
||||
bulkOperations.push(dop)
|
||||
}
|
||||
await bulkExecute()
|
||||
} else {
|
||||
return (await this.getOperations(txes[0])?.raw()) ?? {}
|
||||
}
|
||||
if (result.length === 0) {
|
||||
return {}
|
||||
}
|
||||
if (result.length === 1) {
|
||||
return result[0]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
protected txCollectionCUD (tx: TxCollectionCUD<Doc, AttachedDoc>): DomainOperation {
|
||||
// We need update only create transactions to contain attached, attachedToClass.
|
||||
if (tx.tx._class === core.class.TxCreateDoc) {
|
||||
const createTx = tx.tx as TxCreateDoc<AttachedDoc>
|
||||
const d: TxCreateDoc<AttachedDoc> = {
|
||||
...createTx,
|
||||
attributes: {
|
||||
...createTx.attributes,
|
||||
attachedTo: tx.objectId,
|
||||
attachedToClass: tx.objectClass,
|
||||
collection: tx.collection
|
||||
}
|
||||
}
|
||||
return this.txCreateDoc(d)
|
||||
}
|
||||
// We could cast since we know collection cud is supported.
|
||||
return this.getOperations(tx.tx) as DomainOperation
|
||||
}
|
||||
|
||||
protected txRemoveDoc (tx: TxRemoveDoc<Doc>): DomainOperation {
|
||||
const domain = this.hierarchy.getDomain(tx.objectClass)
|
||||
return {
|
||||
raw: () => this.db.collection(domain).deleteOne({ _id: tx.objectId }),
|
||||
domain,
|
||||
bulk: [{ deleteOne: { filter: { _id: tx.objectId } } }]
|
||||
}
|
||||
}
|
||||
|
||||
protected txMixin (tx: TxMixin<Doc, Doc>): DomainOperation {
|
||||
const domain = this.hierarchy.getDomain(tx.objectClass)
|
||||
|
||||
const filter = { _id: tx.objectId }
|
||||
const modifyOp = {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
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 = [
|
||||
const ops: any = [
|
||||
{ updateOne: { filter, update: { $pull: { [arr]: desc.$value } } } },
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
filter,
|
||||
update: { $set: modifyOp, $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 },
|
||||
// return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).bulkWrite(ops),
|
||||
domain,
|
||||
bulk: ops
|
||||
}
|
||||
}
|
||||
const update = { ...this.translateMixinAttrs(tx.mixin, tx.attributes), $set: { ...modifyOp } }
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).updateOne(filter, update),
|
||||
domain,
|
||||
bulk: [
|
||||
{
|
||||
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
updateOne: {
|
||||
filter,
|
||||
update
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
} else {
|
||||
return await this.db.collection(domain).updateOne(
|
||||
{ _id: tx.objectId },
|
||||
}
|
||||
const update = { $set: { ...this.translateMixinAttrs(tx.mixin, tx.attributes), ...modifyOp } }
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).updateOne(filter, update),
|
||||
domain,
|
||||
bulk: [
|
||||
{
|
||||
$set: {
|
||||
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
updateOne: {
|
||||
filter,
|
||||
update
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -647,14 +738,22 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
return attrs
|
||||
}
|
||||
|
||||
protected override async txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult> {
|
||||
protected txCreateDoc (tx: TxCreateDoc<Doc>): DomainOperation {
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const domain = this.hierarchy.getDomain(doc._class)
|
||||
await this.db.collection(domain).insertOne(translateDoc(doc))
|
||||
return {}
|
||||
const tdoc = translateDoc(doc)
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).insertOne(tdoc),
|
||||
domain,
|
||||
bulk: [
|
||||
{
|
||||
insertOne: { document: tdoc }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected override async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
||||
protected txUpdateDoc (tx: TxUpdateDoc<Doc>): DomainOperation {
|
||||
const domain = this.hierarchy.getDomain(tx.objectClass)
|
||||
if (isOperator(tx.operations)) {
|
||||
const operator = Object.keys(tx.operations)[0]
|
||||
@ -662,7 +761,8 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
const keyval = (tx.operations as any).$move
|
||||
const arr = Object.keys(keyval)[0]
|
||||
const desc = keyval[arr]
|
||||
const ops = [
|
||||
|
||||
const ops: any = [
|
||||
{
|
||||
updateOne: {
|
||||
filter: { _id: tx.objectId },
|
||||
@ -691,7 +791,11 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
}
|
||||
}
|
||||
]
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).bulkWrite(ops),
|
||||
domain,
|
||||
bulk: ops
|
||||
}
|
||||
} else if (operator === '$update') {
|
||||
const keyval = (tx.operations as any).$update
|
||||
const arr = Object.keys(keyval)[0]
|
||||
@ -722,9 +826,14 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
}
|
||||
}
|
||||
]
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).bulkWrite(ops),
|
||||
domain,
|
||||
bulk: ops
|
||||
}
|
||||
} else {
|
||||
if (tx.retrieve === true) {
|
||||
const raw = async (): Promise<TxResult> => {
|
||||
const result = await this.db.collection(domain).findOneAndUpdate(
|
||||
{ _id: tx.objectId },
|
||||
{
|
||||
@ -737,36 +846,46 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
{ returnDocument: 'after' }
|
||||
)
|
||||
return { object: result.value }
|
||||
}
|
||||
return {
|
||||
raw,
|
||||
domain,
|
||||
bulk: undefined
|
||||
}
|
||||
} else {
|
||||
return await this.db.collection(domain).updateOne(
|
||||
{ _id: tx.objectId },
|
||||
{
|
||||
const filter = { _id: tx.objectId }
|
||||
const update = {
|
||||
...tx.operations,
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
}
|
||||
)
|
||||
return {
|
||||
raw: async () => await this.db.collection(domain).updateOne(filter, update),
|
||||
domain,
|
||||
bulk: [{ updateOne: { filter, update } }]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tx.retrieve === true) {
|
||||
const result = await this.db.collection(domain).findOneAndUpdate(
|
||||
{ _id: tx.objectId },
|
||||
{
|
||||
$set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn }
|
||||
} as unknown as UpdateFilter<Document>,
|
||||
{ returnDocument: 'after' }
|
||||
)
|
||||
return { object: result.value }
|
||||
} else {
|
||||
return await this.db
|
||||
const filter = { _id: tx.objectId }
|
||||
const update = { $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }
|
||||
const raw =
|
||||
tx.retrieve === true
|
||||
? async (): Promise<TxResult> => {
|
||||
const result = await this.db
|
||||
.collection(domain)
|
||||
.updateOne(
|
||||
{ _id: tx.objectId },
|
||||
{ $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }
|
||||
)
|
||||
.findOneAndUpdate(filter, update, { returnDocument: 'after' })
|
||||
return { object: result.value }
|
||||
}
|
||||
: async () => await this.db.collection(domain).updateOne(filter, update)
|
||||
|
||||
// Disable bulk for operators
|
||||
return {
|
||||
raw,
|
||||
domain,
|
||||
bulk: [{ updateOne: { filter, update } }]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -774,27 +893,9 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
|
||||
class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
||||
txColl: Collection | undefined
|
||||
protected txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
protected txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
protected txRemoveDoc (tx: TxRemoveDoc<Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
protected txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
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> {
|
||||
await this.txCollection().insertOne(translateDoc(tx))
|
||||
override async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
await this.txCollection().insertMany(tx.map((it) => translateDoc(it)))
|
||||
return {}
|
||||
}
|
||||
|
||||
@ -822,6 +923,16 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
function translateLikeQuery (pattern: string): { $regex: string, $options: string } {
|
||||
return {
|
||||
$regex: `^${pattern
|
||||
.split('%')
|
||||
.map((it) => escapeLikeForRegexp(it))
|
||||
.join('.*')}$`,
|
||||
$options: 'i'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -45,7 +45,7 @@ class MinioBlobAdapter implements DbAdapter {
|
||||
return Object.assign([], { total: 0 })
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
async tx (...tx: Tx[]): Promise<TxResult> {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ export async function initModel (
|
||||
transactorUrl: string,
|
||||
workspaceId: WorkspaceId,
|
||||
rawTxes: Tx[],
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
): Promise<void> {
|
||||
const { mongodbUri, minio, txes } = prepareTools(rawTxes)
|
||||
if (txes.some((tx) => tx.objectSpace !== core.space.Model)) {
|
||||
@ -115,7 +115,8 @@ export async function initModel (
|
||||
})) as unknown as CoreClient & BackupClient
|
||||
try {
|
||||
for (const op of migrateOperations) {
|
||||
await op.upgrade(connection)
|
||||
console.log('Migrage', op[0])
|
||||
await op[1].upgrade(connection)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
@ -142,7 +143,7 @@ export async function upgradeModel (
|
||||
transactorUrl: string,
|
||||
workspaceId: WorkspaceId,
|
||||
rawTxes: Tx[],
|
||||
migrateOperations: MigrateOperation[]
|
||||
migrateOperations: [string, MigrateOperation][]
|
||||
): Promise<void> {
|
||||
const { mongodbUri, txes } = prepareTools(rawTxes)
|
||||
|
||||
@ -171,7 +172,8 @@ export async function upgradeModel (
|
||||
|
||||
const migrateClient = new MigrateClientImpl(db)
|
||||
for (const op of migrateOperations) {
|
||||
await op.migrate(migrateClient)
|
||||
console.log('migrate:', op[0])
|
||||
await op[1].migrate(migrateClient)
|
||||
}
|
||||
|
||||
console.log('Apply upgrade operations')
|
||||
@ -182,7 +184,8 @@ export async function upgradeModel (
|
||||
await createUpdateIndexes(connection, db)
|
||||
|
||||
for (const op of migrateOperations) {
|
||||
await op.upgrade(connection)
|
||||
console.log('upgrade:', op[0])
|
||||
await op[1].upgrade(connection)
|
||||
}
|
||||
|
||||
await connection.close()
|
||||
|
Loading…
Reference in New Issue
Block a user