// // Copyright © 2023 Hardcore Engineering Inc. // // 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. // import core, { type AnyAttribute, type BackupClient, type Class, ClassifierKind, type Client as CoreClient, type Doc, type Domain, Hierarchy, type Obj, type Ref, SortingOrder, type WorkspaceId } from '@hcengineering/core' import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo' import { connect } from '@hcengineering/server-tool' interface PropertyInfo { name: string cValue: any mValue: any cModifiedOn: number mModifiedOn: number } interface ObjectPropertyInfo { doc: Ref properties: PropertyInfo[] } export async function showMixinForeignAttributes ( workspaceId: WorkspaceId, transactorUrl: string, cmd: { detail: boolean, mixin: string, property: string } ): Promise { console.log(`Running mixin attribute check with ${JSON.stringify(cmd)}`) const connection = (await connect(transactorUrl, workspaceId, undefined, { mode: 'backup' })) as unknown as CoreClient & BackupClient try { const result = await getMixinWithForeignProperties(connection, cmd.mixin, cmd.property) console.log(`Found ${result.size} mixin(s)\n`) // print summary for (const [mixin, objects] of result) { const properties = [...new Set(objects.map((o) => o.properties.map((p) => p.name)).flat())].sort() console.log('*', mixin) console.log(' -', properties.join(', ')) } // print details if (cmd.detail) { for (const [mixin, objects] of result) { console.log(mixin, '\n', JSON.stringify(objects, undefined, 2), '\n') } } } catch (err: any) { console.trace(err) } finally { await connection.close() } } export async function fixMixinForeignAttributes ( mongoUrl: string, workspaceId: WorkspaceId, transactorUrl: string, cmd: { mixin: string, property: string } ): Promise { console.log(`Running mixin attribute check with ${JSON.stringify(cmd)}`) const connection = (await connect(transactorUrl, workspaceId, undefined, { mode: 'backup' })) as unknown as CoreClient & BackupClient try { const result = await getMixinWithForeignProperties(connection, cmd.mixin, cmd.property) console.log(`Found ${result.size} mixin(s)\n`) for (const [mixin, objects] of result) { console.log(mixin, '\n', JSON.stringify(objects, undefined, 2), '\n') } if (result.size > 0) { const client = getMongoClient(mongoUrl) try { const _client = await client.getClient() const db = getWorkspaceMongoDB(_client, workspaceId) for (const [mixin, objects] of result) { console.log('fixing', mixin) let domain: Domain | undefined try { domain = connection.getHierarchy().getDomain(mixin) } catch (err: any) { console.error('failed to get domain for', mixin) } if (domain === undefined) continue for (const { doc, properties } of objects) { for (const property of properties) { console.log('fixing', mixin, doc, property.name) const mixinPropertyName = `${mixin}.${property.name}` const propertyName = property.name if (property.mModifiedOn > property.cModifiedOn) { console.log(`- renaming: ${mixinPropertyName} -> ${propertyName}`) await db.collection(domain).updateOne({ _id: doc }, { $rename: { [mixinPropertyName]: propertyName } }) } else { console.log(`- removing: ${mixinPropertyName}`) await db.collection(domain).updateOne({ _id: doc }, { $unset: { [mixinPropertyName]: '' } }) } } } } } finally { client.close() } } } catch (err: any) { console.trace(err) } finally { await connection.close() } } /** * Returns properties that present in the mixin but not declared as the mixin attribute. * If the property is not an attribute, it won't be returned as 'foreign' * @param doc * @param mixin * @param attributes * @returns array of property names */ function getForeignProperties ( doc: Doc, mixin: Ref>, attributes: Map, attribute: string ): string[] { if (!(mixin in doc)) return [] const mixinAttr = (doc as any)[mixin] return Object.entries(mixinAttr) .filter(([key]) => attribute === '' || key === attribute) .filter(([key]) => (attributes.has(key) ? attributes.get(key)?.attributeOf !== mixin : false)) .map(([key]) => key) } async function getMixinWithForeignProperties ( connection: CoreClient, mixin: string, attribute: string ): Promise>, ObjectPropertyInfo[]>> { const hierarchy = connection.getHierarchy() const mixins = await connection.findAll(core.class.Class, { kind: ClassifierKind.MIXIN, ...(mixin !== '' ? { _id: mixin as Ref> } : {}) }) const result = new Map>, ObjectPropertyInfo[]>() for (const [index, mixin] of mixins.entries()) { console.log(`(${index + 1}/${mixins.length}) Processing ${mixin._id} ...`) const attributes = hierarchy.getAllAttributes(mixin._id) try { // Find objects that have mixin attributes const objects = await connection.findAll(mixin._id, { [mixin._id]: { $exists: true } }) for (const doc of objects) { const foreignProperties = getForeignProperties(doc, mixin._id, attributes, attribute) if (foreignProperties.length > 0) { const _doc = Hierarchy.toDoc(doc) const properties: PropertyInfo[] = [] for (const property of foreignProperties) { const cValue = (_doc as any)[property] const mValue = (_doc as any)[mixin._id]?.[property] // check class and mixin transactions for the property const updateDocTx = await connection.findAll( core.class.TxUpdateDoc, { objectId: doc._id, [`operations.${property}`]: { $exists: true } }, { limit: 1, sort: { modifiedOn: SortingOrder.Descending } } ) const mixinTx = await connection.findAll( core.class.TxMixin, { objectId: doc._id, [`attributes.${property}`]: { $exists: true } }, { limit: 1, sort: { modifiedOn: SortingOrder.Descending } } ) const cModifiedOn = updateDocTx[0]?.modifiedOn ?? 0 const mModifiedOn = mixinTx[0]?.modifiedOn ?? 0 properties.push({ name: property, cValue, mValue, cModifiedOn, mModifiedOn }) } result.set(mixin._id, [...(result.get(mixin._id) ?? []), { doc: doc._id, properties }]) } } const objectsCount = result.get(mixin._id)?.length ?? 0 console.log(`(${index + 1}/${mixins.length}) ... processed ${mixin._id} done, found ${objectsCount} objects`) } catch (err: any) { console.error(err) } } return result }