diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index 4063ddc7b9..d35f2ea39e 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -20,7 +20,7 @@ import core from './component' import { _createMixinProxy, _mixinClass, _toDoc } from './proxy' import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx' import { TxProcessor } from './tx' -import getTypeOf from './typeof' +import { getTypeOf } from './typeof' /** * @public diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5d7ba91624..c8c963bdf9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,3 +29,4 @@ export * from './tx' export * from './utils' export * from './backup' export * from './status' +export * from './typeof' diff --git a/packages/core/src/typeof.ts b/packages/core/src/typeof.ts index f9af5b1d25..1e175eccf9 100644 --- a/packages/core/src/typeof.ts +++ b/packages/core/src/typeof.ts @@ -1,7 +1,7 @@ const se = typeof Symbol !== 'undefined' const ste = se && typeof Symbol.toStringTag !== 'undefined' -export default function getTypeOf (obj: any): string { +export function getTypeOf (obj: any): string { const typeofObj = typeof obj if (typeofObj !== 'object') { return typeofObj diff --git a/plugins/tracker-resources/src/components/issues/IssuePresenter.svelte b/plugins/tracker-resources/src/components/issues/IssuePresenter.svelte index 99ab01cac2..c8eb2df9d7 100644 --- a/plugins/tracker-resources/src/components/issues/IssuePresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuePresenter.svelte @@ -74,7 +74,6 @@ {#if presenters.length > 0}
{#each presenters as mixinPresenter} - {mixinPresenter.presenter} {/each}
diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index 6c81b09a29..4f85658a6e 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -53,7 +53,8 @@ import core, { TxResult, TxUpdateDoc, WithLookup, - WorkspaceId + WorkspaceId, + getTypeOf } from '@hcengineering/core' import type { DbAdapter, TxAdapter } from '@hcengineering/server-core' import { AnyBulkWriteOperation, Collection, Db, Document, Filter, MongoClient, Sort, UpdateFilter } from 'mongodb' @@ -61,7 +62,7 @@ import { createHash } from 'node:crypto' import { getMongoClient, getWorkspaceDB } from './utils' function translateDoc (doc: Doc): Document { - return doc as Document + return { ...doc, '%hash%': null } } function isLookupQuery (query: DocumentQuery): boolean { @@ -409,6 +410,8 @@ abstract class MongoAdapterBase implements DbAdapter { projection[ckey] = options.projection[key] } resultPipeline.push({ $project: projection }) + } else { + resultPipeline.push({ $project: { '%hash%': 0 } }) } pipeline.push({ $facet: { @@ -429,7 +432,7 @@ abstract class MongoAdapterBase implements DbAdapter { await this.fillLookupValue(clazz, options?.lookup, row) this.clearExtraLookups(row) } - return toFindResult(result, total) + return toFindResult(this.stripHash(result), total) } private translateKey(key: string, clazz: Ref>): string { @@ -537,6 +540,8 @@ abstract class MongoAdapterBase implements DbAdapter { projection[ckey] = options.projection[key] } cursor = cursor.project(projection) + } else { + cursor = cursor.project({ '%hash%': 0 }) } let total: number = -1 if (options !== null && options !== undefined) { @@ -565,7 +570,17 @@ abstract class MongoAdapterBase implements DbAdapter { if (options?.total === true && options?.limit === undefined) { total = res.length } - return toFindResult(res, total) + return toFindResult(this.stripHash(res), total) + } + + stripHash(docs: T[]): T[] { + docs.forEach((it) => { + if ('%hash%' in it) { + delete it['%hash%'] + } + return it + }) + return docs } find (domain: Domain): StorageIterator { @@ -578,14 +593,21 @@ abstract class MongoAdapterBase implements DbAdapter { if (d === null) { return undefined } - const doc = JSON.stringify(d) - const hash = createHash('sha256') - hash.update(doc) - const digest = hash.digest('base64') + let digest = (d as any)['%hash%'] + if (digest == null) { + if ('%hash%' in d) { + delete d['%hash%'] + } + const doc = JSON.stringify(d) + const hash = createHash('sha256') + hash.update(doc) + digest = hash.digest('base64') + await coll.updateOne({ _id: d._id }, { $set: { '%hash%': digest } }) + } return { id: d._id, hash: digest, - size: doc.length // Some approx size for document. + size: this.calcSize(d) // Some approx size for document. } }, close: async () => { @@ -594,11 +616,41 @@ abstract class MongoAdapterBase implements DbAdapter { } } + calcSize (obj: any): number { + if (typeof obj === 'undefined') { + return 0 + } + if (typeof obj === 'function') { + return 0 + } + let result = 0 + for (const key in obj) { + // include prototype properties + const value = obj[key] + const type = getTypeOf(value) + if (type === 'Array') { + result += this.calcSize(value) + } else if (type === 'Object') { + result += this.calcSize(value) + } else if (type === 'Date') { + result += new Date(value.getTime()).toString().length + } + if (type === 'string') { + result += (value as string).length + } else { + result += JSON.stringify(value).length + } + } + return result + } + async load (domain: Domain, docs: Ref[]): Promise { - return await this.db - .collection(domain) - .find({ _id: { $in: docs } }) - .toArray() + return this.stripHash( + await this.db + .collection(domain) + .find({ _id: { $in: docs } }) + .toArray() + ) } async upload (domain: Domain, docs: Doc[]): Promise { @@ -883,6 +935,9 @@ class MongoAdapter extends MongoAdapterBase { updateOne: { filter: { _id: tx.objectId }, update: { + $set: { + '%hash%': null + }, $pull: { [arr]: desc.$value } @@ -895,7 +950,8 @@ class MongoAdapter extends MongoAdapterBase { update: { $set: { modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn + modifiedOn: tx.modifiedOn, + '%hash%': null }, $push: { [arr]: { @@ -925,7 +981,8 @@ class MongoAdapter extends MongoAdapterBase { }, update: { $set: { - ...Object.fromEntries(Object.entries(desc.$update).map((it) => [arr + '.$.' + it[0], it[1]])) + ...Object.fromEntries(Object.entries(desc.$update).map((it) => [arr + '.$.' + it[0], it[1]])), + '%hash%': null } } } @@ -936,7 +993,8 @@ class MongoAdapter extends MongoAdapterBase { update: { $set: { modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn + modifiedOn: tx.modifiedOn, + '%hash%': null } } } @@ -956,7 +1014,8 @@ class MongoAdapter extends MongoAdapterBase { ...tx.operations, $set: { modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn + modifiedOn: tx.modifiedOn, + '%hash%': null } } as unknown as UpdateFilter, { returnDocument: 'after' } @@ -974,7 +1033,8 @@ class MongoAdapter extends MongoAdapterBase { ...tx.operations, $set: { modifiedBy: tx.modifiedBy, - modifiedOn: tx.modifiedOn + modifiedOn: tx.modifiedOn, + '%hash%': null } } return { @@ -986,7 +1046,14 @@ class MongoAdapter extends MongoAdapterBase { } } else { const filter = { _id: tx.objectId } - const update = { $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } } + const update = { + $set: { + ...tx.operations, + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn, + '%hash%': null + } + } const raw = tx.retrieve === true ? async (): Promise => { diff --git a/server/tool/src/connect.ts b/server/tool/src/connect.ts index dbb28a1c0b..a7ab5b3b81 100644 --- a/server/tool/src/connect.ts +++ b/server/tool/src/connect.ts @@ -34,6 +34,9 @@ export async function connect ( // eslint-disable-next-line const WebSocket = require('ws') + setMetadata(client.metadata.UseBinaryProtocol, true) + setMetadata(client.metadata.UseProtocolCompression, true) + setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) addLocation(clientId, () => import('@hcengineering/client-resources')) diff --git a/server/tool/src/upgrade.ts b/server/tool/src/upgrade.ts index e3c40cb2b3..9910094c4a 100644 --- a/server/tool/src/upgrade.ts +++ b/server/tool/src/upgrade.ts @@ -68,13 +68,20 @@ export class MigrateClientImpl implements MigrationClient { const t = Date.now() try { if (isOperator(operations)) { + if (operations?.$set !== undefined) { + operations.$set['%hash%'] = null + } else { + operations = { ...operations, $set: { '%hash%': null } } + } const result = await this.db .collection(domain) .updateMany(this.translateQuery(query), { ...operations } as unknown as UpdateFilter) return { matched: result.matchedCount, updated: result.modifiedCount } } else { - const result = await this.db.collection(domain).updateMany(this.translateQuery(query), { $set: operations }) + const result = await this.db + .collection(domain) + .updateMany(this.translateQuery(query), { $set: { ...operations, '%hash%': null } }) return { matched: result.matchedCount, updated: result.modifiedCount } } } finally { @@ -92,7 +99,7 @@ export class MigrateClientImpl implements MigrationClient { operations.map((it) => ({ updateOne: { filter: this.translateQuery(it.filter), - update: { $set: it.update } + update: { $set: { ...it.update, '%hash%': null } } } })) ) @@ -115,6 +122,9 @@ export class MigrateClientImpl implements MigrationClient { } let doc: Document | null while ((doc = await cursor.next()) != null) { + if ('%hash%' in doc) { + delete doc['%hash%'] + } await target.insertOne(doc) result.matched++ result.updated++