diff --git a/dev/import-tool/src/index.ts b/dev/import-tool/src/index.ts index 22c5dae1c4..d70b360b87 100644 --- a/dev/import-tool/src/index.ts +++ b/dev/import-tool/src/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import { buildSocialIdString, concatLink, SocialIdType, TxOperations } from '@hcengineering/core' +import { concatLink, TxOperations } from '@hcengineering/core' import { ClickupImporter, defaultDocumentPreprocessors, @@ -75,8 +75,8 @@ export function importTool (): void { setMetadata(serverClientPlugin.metadata.Endpoint, config.ACCOUNTS_URL) console.log('Trying to login user: ', user) const unauthAccountClient = getAccountClient() - const { account, token } = await unauthAccountClient.login(user, password) - if (token === undefined || account === undefined) { + const { account, token, socialId } = await unauthAccountClient.login(user, password) + if (token === undefined || account === undefined || socialId === undefined) { console.log('Login failed for user: ', user) return } @@ -95,7 +95,7 @@ export function importTool (): void { console.log('Connecting to Transactor URL: ', selectedWs.endpoint) const connection = await createClient(selectedWs.endpoint, selectedWs.token) - const client = new TxOperations(connection, buildSocialIdString({ type: SocialIdType.EMAIL, value: user })) + const client = new TxOperations(connection, socialId) const fileUploader = new FrontFileUploader( getFrontUrl(), selectedWs.workspace, diff --git a/dev/tool/src/benchmark.ts b/dev/tool/src/benchmark.ts index ea6dbd89b2..ca3bc6f3ef 100644 --- a/dev/tool/src/benchmark.ts +++ b/dev/tool/src/benchmark.ts @@ -46,7 +46,7 @@ import os from 'os' import { Worker, isMainThread, parentPort } from 'worker_threads' import { CSVWriter } from './csv' -import { AvatarType, getPersonBySocialId } from '@hcengineering/contact' +import { AvatarType, getPersonBySocialKey } from '@hcengineering/contact' import contact from '@hcengineering/model-contact' import recruit from '@hcengineering/model-recruit' import { type Vacancy } from '@hcengineering/recruit' @@ -579,7 +579,7 @@ export async function generateWorkspaceData ( const client = new TxOperations(connection, core.account.System) try { const emailSocialString = buildSocialIdString({ type: SocialIdType.EMAIL, value: email }) - const person = await getPersonBySocialId(client, emailSocialString) + const person = await getPersonBySocialKey(client, emailSocialString) const account = person?.personUuid as AccountUuid if (account == null) { throw new Error('User not found') @@ -632,7 +632,7 @@ export async function generateEmployee (client: TxOperations): Promise { async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {}) - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) + const socialIdBySocialKey = new Map() + const socialIdByOldAccount = new Map() ctx.info('processing activity reactions ', {}) const iterator = await client.traverse(DOMAIN_REACTION, { _class: activity.class.Reaction }) @@ -221,7 +225,14 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {}) - const socialIdByAccount = await getSocialIdByOldAccount(client) - const accountUuidBySocialId = new Map() + const socialKeyByAccount = await getSocialKeyByOldAccount(client) + const accountUuidBySocialKey = new Map() ctx.info('processing activity doc updates ', {}) function getUpdatedClass (attrKey: string): string { @@ -264,9 +275,9 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise { if (['members', 'owners', 'user'].includes(attrKey)) { - return (await getAccountUuidByOldAccount(client, oldVal, socialIdByAccount, accountUuidBySocialId)) ?? oldVal + return (await getAccountUuidByOldAccount(client, oldVal, socialKeyByAccount, accountUuidBySocialKey)) ?? oldVal } else { - return socialIdByAccount[oldVal] ?? oldVal + return socialKeyByAccount[oldVal] ?? oldVal } } @@ -363,11 +374,11 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('activity migrateSocialIdsInDocUpdates', {}) - const accountUuidBySocialId = new Map() + const accountUuidBySocialKey = new Map() ctx.info('processing activity doc updates ', {}) async function getUpdatedVal (oldVal: string): Promise { - return (await getAccountUuidBySocialId(client, oldVal as PersonId, accountUuidBySocialId)) ?? oldVal + return (await getAccountUuidBySocialKey(client, oldVal, accountUuidBySocialKey)) ?? oldVal } async function migrateField

( @@ -455,6 +466,51 @@ async function migrateSocialIdsInDocUpdates (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('activity migrateSocialKeysToSocialIds', {}) + + ctx.info('processing activity reactions ', {}) + const socialIdBySocialKey = new Map() + const iterator = await client.traverse(DOMAIN_REACTION, { _class: activity.class.Reaction }) + + try { + let processed = 0 + while (true) { + const docs = await iterator.next(200) + if (docs === null || docs.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const doc of docs) { + const reaction = doc as Reaction + const newCreateBy = + (await getSocialIdBySocialKey(client, reaction.createBy, socialIdBySocialKey)) ?? reaction.createBy + + if (newCreateBy === reaction.createBy) continue + + operations.push({ + filter: { _id: doc._id }, + update: { + createBy: newCreateBy + } + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_REACTION, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await iterator.close() + } + ctx.info('finished processing activity reactions ', {}) +} + export const activityOperation: MigrateOperation = { async migrate (client: MigrationClient, mode): Promise { await tryMigrate(mode, client, activityId, [ @@ -543,6 +599,12 @@ export const activityOperation: MigrateOperation = { state: 'social-ids-in-doc-updates', mode: 'upgrade', func: migrateSocialIdsInDocUpdates + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'social-keys-to-social-ids', + mode: 'upgrade', + func: migrateSocialKeysToSocialIds } ]) }, diff --git a/models/analytics-collector/src/index.ts b/models/analytics-collector/src/index.ts index 575601e327..76e681cf0b 100644 --- a/models/analytics-collector/src/index.ts +++ b/models/analytics-collector/src/index.ts @@ -13,12 +13,12 @@ // limitations under the License. // -import { type Builder, Model, Prop, ReadOnly, TypeString, UX, TypeBoolean } from '@hcengineering/model' +import { type Builder, Model, Prop, ReadOnly, TypeString, UX, TypeBoolean, TypeAccountUuid } from '@hcengineering/model' import { type OnboardingChannel } from '@hcengineering/analytics-collector' import chunter from '@hcengineering/chunter' import { TChannel } from '@hcengineering/model-chunter' import activity, { type ActivityMessageControl } from '@hcengineering/activity' -import core, { type PersonId, type WorkspaceUuid } from '@hcengineering/core' +import core, { type AccountUuid, type WorkspaceUuid } from '@hcengineering/core' import analyticsCollector from './plugin' @@ -40,9 +40,9 @@ export class TOnboardingChannel extends TChannel implements OnboardingChannel { @ReadOnly() userName!: string - @Prop(TypeString(), analyticsCollector.string.SocialId) + @Prop(TypeAccountUuid(), analyticsCollector.string.Account) @ReadOnly() - socialString!: PersonId + account!: AccountUuid @Prop(TypeString(), analyticsCollector.string.WorkspaceName) @ReadOnly() diff --git a/models/analytics-collector/src/migration.ts b/models/analytics-collector/src/migration.ts index d4082e4426..d587cba61d 100644 --- a/models/analytics-collector/src/migration.ts +++ b/models/analytics-collector/src/migration.ts @@ -13,19 +13,16 @@ // limitations under the License. // -import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector' import { - tryMigrate, type MigrateOperation, - type MigrateUpdate, type MigrationClient, - type MigrationDocumentQuery, - type MigrationUpgradeClient + type MigrationUpgradeClient, + tryMigrate } from '@hcengineering/model' -import { DOMAIN_SPACE, getSocialKeyByOldEmail } from '@hcengineering/model-core' +import { analyticsCollectorId } from '@hcengineering/analytics-collector' +import { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification' import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' -import { buildSocialIdString, type Doc, MeasureMetricsContext } from '@hcengineering/core' async function removeOnboardingChannels (client: MigrationClient): Promise { const channels = await client.find(DOMAIN_SPACE, { 'analytics:mixin:AnalyticsChannel': { $exists: true } }) @@ -44,48 +41,6 @@ async function removeOnboardingChannels (client: MigrationClient): Promise await client.deleteMany(DOMAIN_SPACE, { _id: { $in: channelsIds } }) } -async function migrateAccountsToSocialIds (client: MigrationClient): Promise { - const ctx = new MeasureMetricsContext('analytics collector migrateAccountsToSocialIds', {}) - - ctx.info('processing analytics collector onboarding channels ', {}) - const iterator = await client.traverse(DOMAIN_SPACE, { _class: analyticsCollector.class.OnboardingChannel }) - - try { - let processed = 0 - while (true) { - const docs = await iterator.next(200) - if (docs === null || docs.length === 0) { - break - } - - const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] - - for (const doc of docs) { - const email = (doc as any).email - if (email === undefined || email === '') continue - const socialString = buildSocialIdString(getSocialKeyByOldEmail(email)) - - operations.push({ - filter: { _id: doc._id }, - update: { - socialString - } - }) - } - - if (operations.length > 0) { - await client.bulk(DOMAIN_SPACE, operations) - } - - processed += docs.length - ctx.info('...processed', { count: processed }) - } - } finally { - await iterator.close() - } - ctx.info('finished processing analytics collector onboarding channels ', {}) -} - export const analyticsCollectorOperation: MigrateOperation = { async migrate (client: MigrationClient, mode): Promise { await tryMigrate(mode, client, analyticsCollectorId, [ @@ -93,10 +48,6 @@ export const analyticsCollectorOperation: MigrateOperation = { state: 'remove-analytics-channels-v3', mode: 'upgrade', func: removeOnboardingChannels - }, - { - state: 'accounts-to-social-ids', - func: migrateAccountsToSocialIds } ]) }, diff --git a/models/calendar/src/migration.ts b/models/calendar/src/migration.ts index 6986ac17f0..94f0dd08a8 100644 --- a/models/calendar/src/migration.ts +++ b/models/calendar/src/migration.ts @@ -14,7 +14,7 @@ // import { type Calendar, calendarId, type Event, type ReccuringEvent } from '@hcengineering/calendar' -import { type Doc, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core' +import { type AccountUuid, type Doc, MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core' import { createDefaultSpace, type MigrateUpdate, @@ -25,18 +25,18 @@ import { type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' -import { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core' +import { DOMAIN_SPACE, getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core' import { DOMAIN_CALENDAR, DOMAIN_EVENT } from '.' import calendar from './plugin' -function getCalendarId (socialString: PersonId): Ref { - return `${socialString}_calendar` as Ref +function getCalendarId (val: string): Ref { + return `${val}_calendar` as Ref } async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('calendar migrateAccountsToSocialIds', {}) const hierarchy = client.hierarchy - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) const eventClasses = hierarchy.getDescendants(calendar.class.Event) const calendars = await client.find(DOMAIN_CALENDAR, { @@ -53,7 +53,7 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('calendar migrateSocialIdsToAccountUuids', {}) + const hierarchy = client.hierarchy + const accountUuidBySocialKey = new Map() + + const eventClasses = hierarchy.getDescendants(calendar.class.Event) + + const calendars = await client.find(DOMAIN_CALENDAR, { + _class: calendar.class.Calendar + }) + + ctx.info('processing internal calendars') + + for (const calendar of calendars) { + const id = calendar._id + if (!id.endsWith('_calendar')) { + ctx.warn('Wrong calendar id format', { calendar: calendar._id }) + continue + } + + const socialKey = id.substring(0, id.length - 9) + const accountUuid = await getAccountUuidBySocialKey(client, socialKey, accountUuidBySocialKey) + if (accountUuid == null) { + ctx.warn('no account uuid for social key', { socialKey }) + continue + } + + await client.delete(DOMAIN_CALENDAR, calendar._id) + await client.create(DOMAIN_CALENDAR, { + ...calendar, + _id: getCalendarId(accountUuid) + }) + } + + let processedEvents = 0 + const eventsIterator = await client.traverse(DOMAIN_EVENT, { + _class: { $in: eventClasses } + }) + + try { + while (true) { + const events = await eventsIterator.next(200) + if (events === null || events.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const event of events) { + const id = event.calendar + if (!id.endsWith('_calendar')) { + // Nothing to do, in external calendar + continue + } + + const socialKey = id.substring(0, id.length - 9) + const accountUuid = await getAccountUuidBySocialKey(client, socialKey, accountUuidBySocialKey) + if (accountUuid == null) { + ctx.warn('no account uuid for social key', { socialKey }) + continue + } + + operations.push({ + filter: { _id: event._id }, + update: { + calendar: getCalendarId(accountUuid) + } + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_EVENT, operations) + } + + processedEvents += events.length + ctx.info('...processed events', { count: processedEvents }) + } + + ctx.info('finished processing events') + } finally { + await eventsIterator.close() + } +} + async function migrateCalendars (client: MigrationClient): Promise { await client.move( DOMAIN_SPACE, @@ -242,7 +326,13 @@ export const calendarOperation: MigrateOperation = { }, { state: 'accounts-to-social-ids', + mode: 'upgrade', func: migrateAccountsToSocialIds + }, + { + state: 'migrate-social-ids-to-account-uuids', + mode: 'upgrade', + func: migrateSocialIdsToAccountUuids } ]) }, diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index c5e9a13124..4b644af62d 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -61,7 +61,7 @@ import { TypeBoolean, TypeCollaborativeDoc, TypeDate, - TypePersonId, + TypeNumber, TypeRecord, TypeRef, TypeString, @@ -167,12 +167,13 @@ export class TChannel extends TAttachedDoc implements Channel { @Model(contact.class.SocialIdentity, core.class.AttachedDoc, DOMAIN_CHANNEL) @UX(contact.string.SocialId) export class TSocialIdentity extends TAttachedDoc implements SocialIdentity { + declare _id: Ref & PersonId declare attachedTo: Ref declare attachedToClass: Ref> - @Prop(TypePersonId(), getEmbeddedLabel('Key')) + @Prop(TypeString(), getEmbeddedLabel('Key')) @Hidden() - key!: PersonId + key!: string @Prop(TypeString(), contact.string.Type) type!: SocialIdType @@ -181,9 +182,9 @@ export class TSocialIdentity extends TAttachedDoc implements SocialIdentity { @Index(IndexKind.FullText) value!: string - @Prop(TypeBoolean(), contact.string.Confirmed) + @Prop(TypeNumber(), contact.string.Confirmed) @ReadOnly() - confirmed!: boolean + verifiedOn?: number } @Model(contact.class.Person, contact.class.Contact) diff --git a/models/contact/src/migration.ts b/models/contact/src/migration.ts index cddee2e694..1c85ee2387 100644 --- a/models/contact/src/migration.ts +++ b/models/contact/src/migration.ts @@ -1,6 +1,14 @@ // -import { AvatarType, type Person, type Contact, type SocialIdentity } from '@hcengineering/contact' +import { + AvatarType, + type Person, + type Contact, + type SocialIdentity, + type SocialIdentityRef, + getFirstName, + getLastName +} from '@hcengineering/contact' import { AccountRole, buildSocialIdString, @@ -9,8 +17,8 @@ import { type Domain, DOMAIN_MODEL_TX, DOMAIN_TX, - generateId, MeasureMetricsContext, + type PersonId, type Ref, type Space, type TxCUD @@ -22,11 +30,12 @@ import { type MigrationClient, type MigrationDocumentQuery, type MigrationUpgradeClient, + type ModelLogger, tryMigrate, tryUpgrade } from '@hcengineering/model' import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' -import core, { getAccountsFromTxes, getSocialKeyByOldEmail } from '@hcengineering/model-core' +import core, { getAccountsFromTxes, getSocialIdBySocialKey, getSocialKeyByOldEmail } from '@hcengineering/model-core' import { DOMAIN_VIEW } from '@hcengineering/model-view' import contact, { contactId, DOMAIN_CHANNEL, DOMAIN_CONTACT } from './index' @@ -122,7 +131,7 @@ async function fillAccountUuids (client: MigrationClient): Promise { )[0] if (socialIdentity == null) continue - const accountUuid = await client.accountClient.findPerson(socialIdentity.key) + const accountUuid = await client.accountClient.findPersonBySocialKey(socialIdentity.key) if (accountUuid == null) { continue } @@ -150,6 +159,66 @@ async function fillAccountUuids (client: MigrationClient): Promise { } } +async function fillSocialIdentitiesIds (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('contact fillSocialIdentitiesIds', {}) + ctx.info('filling social identities genenrated ids...') + const socialIdBySocialKey = new Map() + const iterator = await client.traverse(DOMAIN_CHANNEL, { _class: contact.class.SocialIdentity }) + let count = 0 + + try { + let newSids: SocialIdentity[] = [] + let newSidIds = new Set>() + let deleteSids: Ref[] = [] + + while (true) { + const socialIdentities = await iterator.next(200) + if (socialIdentities === null || socialIdentities.length === 0) { + break + } + + for (const socialIdentity of socialIdentities) { + const socialId = await getSocialIdBySocialKey(client, socialIdentity.key, socialIdBySocialKey) + + if (socialId == null || socialId === socialIdentity._id) continue + + const socialIdRef = socialId as SocialIdentityRef + // Some old data might contain duplicate accounts for github users + // so need to filter just in case + if (!newSidIds.has(socialIdRef)) { + newSidIds.add(socialIdRef) + newSids.push({ + ...socialIdentity, + _id: socialIdRef + }) + } + + deleteSids.push(socialIdentity._id) + count++ + + if (newSids.length > 50) { + await client.create(DOMAIN_CHANNEL, newSids) + await client.deleteMany(DOMAIN_CHANNEL, { _id: { $in: deleteSids } }) + newSids = [] + newSidIds = new Set() + deleteSids = [] + } + } + } + + if (newSids.length > 0) { + await client.create(DOMAIN_CHANNEL, newSids) + await client.deleteMany(DOMAIN_CHANNEL, { _id: { $in: deleteSids } }) + newSids = [] + newSidIds = new Set() + deleteSids = [] + } + ctx.info('finished filling social identities genenrated ids. Updated count: ', { count }) + } finally { + await iterator.close() + } +} + async function assignWorkspaceRoles (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('contact assignWorkspaceRoles', {}) ctx.info('assigning workspace roles...') @@ -166,7 +235,7 @@ async function assignWorkspaceRoles (client: MigrationClient): Promise { } const socialKey = getSocialKeyByOldEmail(email) try { - await client.accountClient.updateWorkspaceRoleBySocialId(buildSocialIdString(socialKey), role) + await client.accountClient.updateWorkspaceRoleBySocialKey(buildSocialIdString(socialKey), role) } catch (err: any) { ctx.error('Failed to update workspace role', { email, ...socialKey, role, err }) } @@ -230,6 +299,7 @@ async function createSocialIdentities (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('createSocialIdentities', {}) ctx.info('processing person accounts ', {}) + const socialIdBySocialKey = new Map() const personAccountsTxes: any[] = await client.find>(DOMAIN_MODEL_TX, { objectClass: 'contact:class:PersonAccount' as Ref> }) @@ -240,13 +310,17 @@ async function createSocialIdentities (client: MigrationClient): Promise { if (email === '') continue const socialIdKey = getSocialKeyByOldEmail(email) - const socialId: SocialIdentity = { - _id: generateId(), + const socialKey = buildSocialIdString(socialIdKey) + const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey) + + if (socialId == null) continue + + const socialIdObj: SocialIdentity = { + _id: socialId as SocialIdentityRef, _class: contact.class.SocialIdentity, space: contact.space.Contacts, ...socialIdKey, - key: buildSocialIdString(socialIdKey), - confirmed: false, + key: socialKey, attachedTo: pAcc.person, attachedToClass: contact.class.Person, @@ -258,11 +332,46 @@ async function createSocialIdentities (client: MigrationClient): Promise { modifiedBy: core.account.ConfigUser } - await client.create(DOMAIN_CHANNEL, socialId) + await client.create(DOMAIN_CHANNEL, socialIdObj) } } +async function ensureGlobalPersonsForLocalAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('contact ensureGlobalPersonsForLocalAccounts', {}) + ctx.info('ensuring global persons for local accounts ', {}) + + const personAccountsTxes: any[] = await client.find>(DOMAIN_MODEL_TX, { + objectClass: 'contact:class:PersonAccount' as Ref> + }) + const personAccounts = getAccountsFromTxes(personAccountsTxes) + + let count = 0 + for (const pAcc of personAccounts) { + const email: string = pAcc.email ?? '' + if (email === '') continue + + const socialIdKey = getSocialKeyByOldEmail(email) + const person = (await client.find(DOMAIN_CONTACT, { _id: pAcc.person }))[0] + const name = person?.name + const firstName = getFirstName(name) + const lastName = getLastName(name) + const effectiveFirstName = firstName === '' ? socialIdKey.value : firstName + + await client.accountClient.ensurePerson(socialIdKey.type, socialIdKey.value, effectiveFirstName, lastName) + count++ + } + ctx.info('finished ensuring global persons for local accounts. Total persons ensured: ', { count }) +} + export const contactOperation: MigrateOperation = { + async preMigrate (client: MigrationClient, logger: ModelLogger, mode): Promise { + await tryMigrate(mode, client, contactId, [ + { + state: 'ensure-accounts-global-persons', + func: (client) => ensureGlobalPersonsForLocalAccounts(client) + } + ]) + }, async migrate (client: MigrationClient, mode): Promise { await tryMigrate(mode, client, contactId, [ { @@ -407,19 +516,29 @@ export const contactOperation: MigrateOperation = { }, { state: 'create-social-identities', + mode: 'upgrade', func: createSocialIdentities }, { state: 'assign-workspace-roles', + mode: 'upgrade', func: assignWorkspaceRoles }, { state: 'fill-account-uuids', + mode: 'upgrade', func: fillAccountUuids }, { state: 'assign-employee-roles-v1', + mode: 'upgrade', func: assignEmployeeRoles + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'fill-social-identities-ids', + mode: 'upgrade', + func: fillSocialIdentitiesIds } ]) }, diff --git a/models/core/src/index.ts b/models/core/src/index.ts index 270709024e..8e08f4602c 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -84,13 +84,15 @@ export { coreId, DOMAIN_SPACE } from '@hcengineering/core' export * from './core' export { coreOperation, - getSocialIdByOldAccount, + getSocialKeyByOldAccount, getAccountsFromTxes, getSocialKeyByOldEmail, - getAccountUuidBySocialId, + getAccountUuidBySocialKey, getUniqueAccounts, getAccountUuidByOldAccount, - getUniqueAccountsFromOldAccounts + getUniqueAccountsFromOldAccounts, + getSocialIdBySocialKey, + getSocialIdFromOldAccount } from './migration' export * from './security' export * from './status' diff --git a/models/core/src/migration.ts b/models/core/src/migration.ts index 104709654d..ea25ad50c6 100644 --- a/models/core/src/migration.ts +++ b/models/core/src/migration.ts @@ -309,27 +309,27 @@ export function getAccountsFromTxes (accTxes: TxCUD[]): any { .filter((it) => it !== undefined) } -export async function getSocialIdByOldAccount (client: MigrationClient): Promise> { +export async function getSocialKeyByOldAccount (client: MigrationClient): Promise> { const systemAccounts = [core.account.System, core.account.ConfigUser] const accountsTxes: TxCUD[] = await client.find>(DOMAIN_MODEL_TX, { objectClass: { $in: ['core:class:Account', 'contact:class:PersonAccount'] as Ref>[] } }) const accounts = getAccountsFromTxes(accountsTxes) - const socialIdByAccount: Record = {} + const socialKeyByAccount: Record = {} for (const account of accounts) { if (account.email === undefined) { continue } if (systemAccounts.includes(account._id)) { - socialIdByAccount[account._id] = account._id + socialKeyByAccount[account._id] = account._id } else { - socialIdByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email)) + socialKeyByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email)) as any } } - return socialIdByAccount + return socialKeyByAccount } export function getSocialKeyByOldEmail (rawEmail: string): SocialKey { @@ -362,7 +362,9 @@ export function getSocialKeyByOldEmail (rawEmail: string): SocialKey { async function migrateAccounts (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('core migrateAccounts', {}) const hierarchy = client.hierarchy - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) + const socialIdBySocialKey = new Map() + const socialIdByOldAccount = new Map() ctx.info('migrating createdBy and modifiedBy') function chunkArray (array: T[], chunkSize: number): T[][] { @@ -379,9 +381,16 @@ async function migrateAccounts (client: MigrationClient): Promise { const groupByCreated = await client.groupBy(domain, 'createdBy', {}) const groupByModified = await client.groupBy(domain, 'modifiedBy', {}) - groupByCreated.forEach((_, accId) => { - const socialId = socialIdByAccount[accId] - if (socialId == null || accId === socialId) return + for (const accId of groupByCreated.keys()) { + if (accId == null) continue + const socialId = await getSocialIdFromOldAccount( + client, + accId, + socialKeyByAccount, + socialIdBySocialKey, + socialIdByOldAccount + ) + if (socialId == null || accId === socialId) continue operations.push({ filter: { createdBy: accId }, @@ -389,11 +398,18 @@ async function migrateAccounts (client: MigrationClient): Promise { createdBy: socialId } }) - }) + } - groupByModified.forEach((_, accId) => { - const socialId = socialIdByAccount[accId] - if (socialId == null || accId === socialId) return + for (const accId of groupByModified.keys()) { + if (accId == null) continue + const socialId = await getSocialIdFromOldAccount( + client, + accId, + socialKeyByAccount, + socialIdBySocialKey, + socialIdByOldAccount + ) + if (socialId == null || accId === socialId) continue operations.push({ filter: { modifiedBy: accId }, @@ -401,7 +417,7 @@ async function migrateAccounts (client: MigrationClient): Promise { modifiedBy: socialId } }) - }) + } if (operations.length > 0) { const operationsChunks = chunkArray(operations, 40) @@ -437,7 +453,7 @@ async function migrateAccounts (client: MigrationClient): Promise { } } - const accountUuidBySocialId = new Map() + const accountUuidBySocialKey = new Map() ctx.info('processing spaces members, owners and roles assignment', {}) let processedSpaces = 0 @@ -459,14 +475,14 @@ async function migrateAccounts (client: MigrationClient): Promise { const newMembers = await getUniqueAccountsFromOldAccounts( client, space.members, - socialIdByAccount, - accountUuidBySocialId + socialKeyByAccount, + accountUuidBySocialKey ) const newOwners = await getUniqueAccountsFromOldAccounts( client, space.owners ?? [], - socialIdByAccount, - accountUuidBySocialId + socialKeyByAccount, + accountUuidBySocialKey ) const update: MigrateUpdate = { members: newMembers as any, @@ -486,8 +502,8 @@ async function migrateAccounts (client: MigrationClient): Promise { const newAssignees = await getUniqueAccountsFromOldAccounts( client, oldAssignees, - socialIdByAccount, - accountUuidBySocialId + socialKeyByAccount, + accountUuidBySocialKey ) update[`${type.targetClass}`] = { @@ -525,8 +541,8 @@ async function migrateAccounts (client: MigrationClient): Promise { const newMembers = await getUniqueAccountsFromOldAccounts( client, spaceType.members, - socialIdByAccount, - accountUuidBySocialId + socialKeyByAccount, + accountUuidBySocialKey ) const tx: TxUpdateDoc = { _id: generateId(), @@ -550,41 +566,41 @@ async function migrateAccounts (client: MigrationClient): Promise { ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes }) } -export async function getAccountUuidBySocialId ( +export async function getAccountUuidBySocialKey ( client: MigrationClient, - socialId: PersonId, - accountUuidBySocialId: Map + socialKey: string, + accountUuidBySocialKey: Map ): Promise { - if (socialId === core.account.System) { + if (socialKey === core.account.System) { return systemAccountUuid } - if (socialId === core.account.ConfigUser) { + if (socialKey === core.account.ConfigUser) { return configUserAccountUuid } - const cached = accountUuidBySocialId.has(socialId) + const cached = accountUuidBySocialKey.has(socialKey) if (!cached) { - const personUuid = await client.accountClient.findPerson(socialId) + const personUuid = await client.accountClient.findPersonBySocialKey(socialKey) if (personUuid === undefined) { - console.log('Could not find person for', socialId) + console.log('Could not find person for', socialKey) } - accountUuidBySocialId.set(socialId, (personUuid as AccountUuid | undefined) ?? null) + accountUuidBySocialKey.set(socialKey, (personUuid as AccountUuid | undefined) ?? null) } - return accountUuidBySocialId.get(socialId) ?? null + return accountUuidBySocialKey.get(socialKey) ?? null } export async function getUniqueAccounts ( client: MigrationClient, - persons: PersonId[], - accountUuidBySocialId = new Map() + socialKeys: string[], + accountUuidBySocialKey = new Map() ): Promise { const accounts = new Set() - for (const person of persons) { - let newAccount = await getAccountUuidBySocialId(client, person as unknown as PersonId, accountUuidBySocialId) + for (const person of socialKeys) { + let newAccount = await getAccountUuidBySocialKey(client, person, accountUuidBySocialKey) if (newAccount == null && isUuid(person)) { newAccount = person as unknown as AccountUuid @@ -600,7 +616,7 @@ export async function getUniqueAccounts ( export async function getAccountUuidByOldAccount ( client: MigrationClient, oldAccount: string, - socialIdByOldAccount: Record, + socialKeyByOldAccount: Record, accountUuidByOldAccount: Map ): Promise { if (oldAccount === core.account.System) { @@ -614,13 +630,13 @@ export async function getAccountUuidByOldAccount ( const cached = accountUuidByOldAccount.has(oldAccount) if (!cached) { - const socialId = socialIdByOldAccount[oldAccount] + const socialId = socialKeyByOldAccount[oldAccount] if (socialId == null) { accountUuidByOldAccount.set(oldAccount, null) return null } - const personUuid = await client.accountClient.findPerson(socialId) + const personUuid = await client.accountClient.findPersonBySocialKey(socialId) accountUuidByOldAccount.set(oldAccount, (personUuid as AccountUuid | undefined) ?? null) } @@ -628,6 +644,42 @@ export async function getAccountUuidByOldAccount ( return accountUuidByOldAccount.get(oldAccount) ?? null } +export async function getSocialIdBySocialKey ( + client: MigrationClient, + socialKey: string, + socialIdBySocialKey?: Map +): Promise { + if ([core.account.System, core.account.ConfigUser].includes(socialKey as PersonId)) { + return socialKey as PersonId + } + + if (socialIdBySocialKey == null || !socialIdBySocialKey.has(socialKey)) { + const val = (await client.accountClient.findSocialIdBySocialKey(socialKey)) ?? null + if (socialIdBySocialKey == null) return val + + socialIdBySocialKey.set(socialKey, val) + } + + return socialIdBySocialKey.get(socialKey) ?? null +} + +export async function getSocialIdFromOldAccount ( + client: MigrationClient, + oldAccount: string, + socialKeyByOldAccount: Record, + socialIdBySocialKey: Map, + socialIdByOldAccount: Map +): Promise { + if (!socialIdByOldAccount.has(oldAccount)) { + const socialKey = socialKeyByOldAccount[oldAccount] + if (socialKey == null) return null + + socialIdByOldAccount.set(oldAccount, await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)) + } + + return socialIdByOldAccount.get(oldAccount) ?? null +} + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i function isUuid (val: string): boolean { return uuidRegex.test(val) @@ -636,12 +688,12 @@ function isUuid (val: string): boolean { export async function getUniqueAccountsFromOldAccounts ( client: MigrationClient, oldAccounts: string[], - socialIdByOldAccount: Record, + socialKeyByOldAccount: Record, accountUuidByOldAccount: Map = new Map() ): Promise { const accounts = new Set() for (const oldAcc of oldAccounts) { - let newAccount = await getAccountUuidByOldAccount(client, oldAcc, socialIdByOldAccount, accountUuidByOldAccount) + let newAccount = await getAccountUuidByOldAccount(client, oldAcc, socialKeyByOldAccount, accountUuidByOldAccount) if (newAccount == null && isUuid(oldAcc)) { newAccount = oldAcc as unknown as AccountUuid @@ -656,8 +708,8 @@ export async function getUniqueAccountsFromOldAccounts ( } /** - * Migrates social ids to new accounts where needed. - * Should only be applied to staging where old accounts have already been migrated to social ids. + * Migrates social keys to new accounts where needed. + * Should only be applied to staging where old accounts have already been migrated to social keys. * REMOVE IT BEFORE MERGING TO PRODUCTION * @param client * @returns @@ -665,7 +717,7 @@ export async function getUniqueAccountsFromOldAccounts ( async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('core migrateSpaceMembersToAccountUuids', {}) const hierarchy = client.hierarchy - const accountUuidBySocialId = new Map() + const accountUuidBySocialKey = new Map() const spaceTypes = client.model.findAllSync(core.class.SpaceType, {}) const spaceTypesById = toIdMap(spaceTypes) @@ -698,8 +750,8 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom if (!hierarchy.isDerived(s._class, core.class.Space)) continue const space = s as Space const update: MigrateUpdate = { - members: await getUniqueAccounts(client, space.members as unknown as PersonId[], accountUuidBySocialId), - owners: await getUniqueAccounts(client, (space.owners ?? []) as unknown as PersonId[], accountUuidBySocialId) + members: await getUniqueAccounts(client, space.members, accountUuidBySocialKey), + owners: await getUniqueAccounts(client, space.owners ?? [], accountUuidBySocialKey) } const type = spaceTypesById.get((space as TypedSpace).type) @@ -712,7 +764,7 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom for (const role of roles ?? []) { const oldAssignees: PersonId[] | undefined = (mixin as any)[role._id] if (oldAssignees != null && oldAssignees.length > 0) { - const newAssignees = await getUniqueAccounts(client, oldAssignees, accountUuidBySocialId) + const newAssignees = await getUniqueAccounts(client, oldAssignees, accountUuidBySocialKey) update[`${type.targetClass}`] = { [role._id]: newAssignees @@ -749,7 +801,7 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom const newMembers = await getUniqueAccounts( client, spaceType.members as unknown as PersonId[], - accountUuidBySocialId + accountUuidBySocialKey ) const tx: TxUpdateDoc = { _id: generateId(), @@ -773,6 +825,79 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes }) } +/** + * Migrates social keys to social ids where needed. + * Should only be applied to staging where old accounts have already been migrated to social keys. + * REMOVE IT BEFORE MERGING TO PRODUCTION + * @param client + * @returns + */ +async function migrateCreatedByToGenSocialIds (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('core migrateCreatedByToGenSocialIds', {}) + const socialIdBySocialKey = new Map() + + ctx.info('migrating createdBy and modifiedBy') + function chunkArray (array: T[], chunkSize: number): T[][] { + const chunks: T[][] = [] + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)) + } + return chunks + } + + for (const domain of client.hierarchy.domains()) { + ctx.info('processing domain ', { domain }) + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + const groupByCreated = await client.groupBy(domain, 'createdBy', {}) + const groupByModified = await client.groupBy(domain, 'modifiedBy', {}) + + for (const socialKey of groupByCreated.keys()) { + if (socialKey == null) continue + const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey) + if (socialId == null || socialKey === socialId) continue + + operations.push({ + filter: { createdBy: socialKey }, + update: { + createdBy: socialId + } + }) + } + + for (const socialKey of groupByModified.keys()) { + if (socialKey == null) continue + const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey) + if (socialId == null || socialKey === socialId) continue + + operations.push({ + filter: { modifiedBy: socialKey }, + update: { + modifiedBy: socialId + } + }) + } + + if (operations.length > 0) { + const operationsChunks = chunkArray(operations, 40) + ctx.info('chunks to process ', { total: operationsChunks.length }) + let processed = 0 + for (const operationsChunk of operationsChunks) { + if (operationsChunk.length === 0) continue + + await client.bulk(domain, operationsChunk) + processed++ + if (operationsChunks.length > 1) { + ctx.info('processed chunk', { processed, of: operationsChunks.length }) + } + } + } else { + ctx.info('no social keys to migrate') + } + } + + ctx.info('finished migrating createdBy and modifiedBy') +} + async function processMigrateJsonForDomain ( ctx: MeasureContext, domain: Domain, @@ -990,9 +1115,15 @@ export const coreOperation: MigrateOperation = { }, // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION { - state: 'space-members-to-account-uuids', + state: 'created-by-to-account-uuids', mode: 'upgrade', func: migrateSpaceMembersToAccountUuids + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'created-by-to-gen-social-ids', + mode: 'upgrade', + func: migrateCreatedByToGenSocialIds } ]) }, diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 1ea4a4875a..ecd8bc5da4 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -14,16 +14,7 @@ // import activity from '@hcengineering/activity' -import type { - CollectionSize, - MarkupBlobRef, - Domain, - Rank, - Ref, - Role, - RolesAssignment, - PersonId -} from '@hcengineering/core' +import type { CollectionSize, MarkupBlobRef, Domain, Rank, Ref, Role, RolesAssignment } from '@hcengineering/core' import { AccountUuid, AccountRole, IndexKind } from '@hcengineering/core' import { type Document, @@ -45,8 +36,8 @@ import { TypeNumber, TypeRef, TypeString, - TypePersonId, - UX + UX, + TypeAccountUuid } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' @@ -89,9 +80,9 @@ export class TDocument extends TDoc implements Document, Todoable { @Hidden() declare space: Ref - @Prop(TypePersonId(), document.string.LockedBy) + @Prop(TypeAccountUuid(), document.string.LockedBy) @Hidden() - lockedBy?: PersonId + lockedBy?: AccountUuid @Prop(Collection(attachment.class.Embedding), attachment.string.Embeddings) embeddings?: number diff --git a/models/document/src/migration.ts b/models/document/src/migration.ts index 2262c62cc2..091b18ae1d 100644 --- a/models/document/src/migration.ts +++ b/models/document/src/migration.ts @@ -22,7 +22,8 @@ import { SortingOrder, type Class, type CollaborativeDoc, - type Doc + type Doc, + type AccountUuid } from '@hcengineering/core' import { type Document, type DocumentSnapshot, type Teamspace } from '@hcengineering/document' import { @@ -35,7 +36,7 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' -import core, { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core' +import core, { DOMAIN_SPACE, getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core' import { DOMAIN_NOTIFICATION } from '@hcengineering/notification' import { type Asset } from '@hcengineering/platform' import { makeRank } from '@hcengineering/rank' @@ -292,7 +293,52 @@ async function migrateRanks (client: MigrationClient): Promise { async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('document migrateAccountsToSocialIds', {}) - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) + + ctx.info('processing document lockedBy ', {}) + const iterator = await client.traverse(DOMAIN_DOCUMENT, { _class: document.class.Document }) + + try { + let processed = 0 + while (true) { + const docs = await iterator.next(200) + if (docs === null || docs.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const doc of docs) { + const document = doc as Document + const newLockedBy: any = + document.lockedBy != null ? socialKeyByAccount[document.lockedBy] ?? document.lockedBy : document.lockedBy + + if (newLockedBy === document.lockedBy) continue + + operations.push({ + filter: { _id: document._id }, + update: { + lockedBy: newLockedBy + } + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_DOCUMENT, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await iterator.close() + } + ctx.info('finished processing document lockedBy ', {}) +} + +async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('document migrateSocialIdsToGlobalAccounts', {}) + const accountUuidBySocialKey = new Map() ctx.info('processing document lockedBy ', {}) const iterator = await client.traverse(DOMAIN_DOCUMENT, { _class: document.class.Document }) @@ -310,7 +356,9 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('notification migrateAccounts', {}) const hierarchy = client.hierarchy - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) + const socialIdBySocialKey = new Map() + const socialIdByOldAccount = new Map() const accountUuidByOldAccount = new Map() ctx.info('processing collaborators ', {}) @@ -276,7 +280,7 @@ async function migrateAccounts (client: MigrationClient): Promise { const newCollaborators = await getUniqueAccountsFromOldAccounts( client, oldCollaborators, - socialIdByAccount, + socialKeyByAccount, accountUuidByOldAccount ) @@ -329,8 +333,9 @@ async function migrateAccounts (client: MigrationClient): Promise { }) for (const oldAccId of groupByUser.keys()) { - const newAccId = await getAccountUuidByOldAccount(client, oldAccId, socialIdByAccount, accountUuidByOldAccount) - if (newAccId == null || oldAccId === newAccId) return + if (oldAccId == null) continue + const newAccId = await getAccountUuidByOldAccount(client, oldAccId, socialKeyByAccount, accountUuidByOldAccount) + if (newAccId == null || oldAccId === newAccId) continue operations.push({ filter: { @@ -356,20 +361,27 @@ async function migrateAccounts (client: MigrationClient): Promise { _class: notification.class.BrowserNotification }) - groupBySenderId.forEach((_, accId) => { - const socialId = socialIdByAccount[accId] - if (socialId == null || accId === socialId) return + for (const oldAccId of groupBySenderId.keys()) { + if (oldAccId == null) continue + const socialId = await getSocialIdFromOldAccount( + client, + oldAccId, + socialKeyByAccount, + socialIdBySocialKey, + socialIdByOldAccount + ) + if (socialId == null || oldAccId === socialId) continue operations.push({ filter: { - senderId: accId, + senderId: oldAccId, _class: notification.class.BrowserNotification }, update: { senderId: socialId } }) - }) + } if (operations.length > 0) { const operationsChunks = chunkArray(operations, 40) @@ -408,7 +420,7 @@ async function migrateAccounts (client: MigrationClient): Promise { for (const doc of docs) { const oldUser: any = doc.user - const newUser = await getAccountUuidByOldAccount(client, oldUser, socialIdByAccount, accountUuidByOldAccount) + const newUser = await getAccountUuidByOldAccount(client, oldUser, socialKeyByAccount, accountUuidByOldAccount) if (newUser != null && newUser !== oldUser) { operations.push({ @@ -443,7 +455,7 @@ async function migrateAccounts (client: MigrationClient): Promise { async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('notification migrateSocialIdsToAccountUuids', {}) const hierarchy = client.hierarchy - const accountUuidBySocialId = new Map() + const accountUuidBySocialKey = new Map() ctx.info('processing collaborators ', {}) for (const domain of client.hierarchy.domains()) { @@ -466,7 +478,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise if (oldCollaborators === undefined || oldCollaborators.length === 0) continue - const newCollaborators = await getUniqueAccounts(client, oldCollaborators, accountUuidBySocialId) + const newCollaborators = await getUniqueAccounts(client, oldCollaborators, accountUuidBySocialKey) operations.push({ filter: { _id: doc._id }, @@ -517,7 +529,8 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise }) for (const socialId of groupByUser.keys()) { - const account = await getAccountUuidBySocialId(client, socialId, accountUuidBySocialId) + if (socialId == null) continue + const account = await getAccountUuidBySocialKey(client, socialId, accountUuidBySocialKey) if (account == null || (account as unknown as PersonId) === socialId) continue @@ -581,7 +594,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise for (const doc of docs) { const oldUser: any = doc.user - const newUser = await getAccountUuidBySocialId(client, oldUser, accountUuidBySocialId) + const newUser = await getAccountUuidBySocialKey(client, oldUser, accountUuidBySocialKey) if (newUser != null && newUser !== oldUser) { operations.push({ @@ -606,6 +619,58 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise ctx.info('finished processing doc notify contexts ', {}) } +async function migrateSocialKeysToSocialIds (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('notification migrateSocialKeysToSocialIds', {}) + ctx.info('processing browser notifications sender ids ', {}) + const socialIdBySocialKey = new Map() + function chunkArray (array: T[], chunkSize: number): T[][] { + const chunks: T[][] = [] + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)) + } + return chunks + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + const groupBySenderId = await client.groupBy(DOMAIN_NOTIFICATION, 'senderId', { + _class: notification.class.BrowserNotification + }) + + for (const socialKey of groupBySenderId.keys()) { + if (socialKey == null) continue + const socialId = (await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)) ?? socialKey + if (socialId == null || socialKey === socialId) continue + + operations.push({ + filter: { + senderId: socialKey, + _class: notification.class.BrowserNotification + }, + update: { + senderId: socialId + } + }) + } + + if (operations.length > 0) { + const operationsChunks = chunkArray(operations, 40) + let processed = 0 + for (const operationsChunk of operationsChunks) { + if (operationsChunk.length === 0) continue + + await client.bulk(DOMAIN_NOTIFICATION, operationsChunk) + processed++ + if (operationsChunks.length > 1) { + ctx.info('processed chunk', { processed, of: operationsChunks.length }) + } + } + } else { + ctx.info('no social keys to migrate') + } + + ctx.info('finished processing browser notifications sender ids ', {}) +} + export async function migrateSettings (client: MigrationClient): Promise { await client.update( DOMAIN_PREFERENCE, @@ -886,6 +951,12 @@ export const notificationOperation: MigrateOperation = { state: 'migrate-social-ids-to-account-uuids', mode: 'upgrade', func: migrateSocialIdsToAccountUuids + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'migrate-social-keys-to-social-ids', + mode: 'upgrade', + func: migrateSocialKeysToSocialIds } ]) }, diff --git a/models/setting/src/migration.ts b/models/setting/src/migration.ts index 1f90415251..b72cee3753 100644 --- a/models/setting/src/migration.ts +++ b/models/setting/src/migration.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import core, { type AccountUuid, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core' +import core, { type AccountUuid, MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core' import { migrateSpace, type MigrateUpdate, @@ -24,7 +24,11 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import setting, { type Integration, settingId } from '@hcengineering/setting' -import { getSocialIdByOldAccount, getUniqueAccounts, getUniqueAccountsFromOldAccounts } from '@hcengineering/model-core' +import { + getSocialKeyByOldAccount, + getUniqueAccounts, + getUniqueAccountsFromOldAccounts +} from '@hcengineering/model-core' import { DOMAIN_SETTING } from '.' @@ -36,7 +40,7 @@ import { DOMAIN_SETTING } from '.' */ async function migrateAccounts (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('setting migrateAccounts', {}) - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) const accountUuidByOldAccount = new Map() ctx.info('processing setting integration shared ', {}) @@ -60,7 +64,7 @@ async function migrateAccounts (client: MigrationClient): Promise { const newShared = await getUniqueAccountsFromOldAccounts( client, integration.shared, - socialIdByAccount, + socialKeyByAccount, accountUuidByOldAccount ) @@ -94,7 +98,7 @@ async function migrateAccounts (client: MigrationClient): Promise { */ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('setting migrateAccounts', {}) - const accountUuidBySocialId = new Map() + const accountUuidBySocialKey = new Map() ctx.info('processing setting integration shared ', {}) const iterator = await client.traverse(DOMAIN_SETTING, { _class: setting.class.Integration }) @@ -114,11 +118,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise if (integration.shared === undefined || integration.shared.length === 0) continue - const newShared = await getUniqueAccounts( - client, - integration.shared as unknown as PersonId[], - accountUuidBySocialId - ) + const newShared = await getUniqueAccounts(client, integration.shared, accountUuidBySocialKey) operations.push({ filter: { _id: integration._id }, @@ -146,17 +146,20 @@ export const settingOperation: MigrateOperation = { await tryMigrate(mode, client, settingId, [ { state: 'removeDeprecatedSpace', + mode: 'upgrade', func: async (client: MigrationClient) => { await migrateSpace(client, 'setting:space:Setting' as Ref, core.space.Workspace, [DOMAIN_SETTING]) } }, { state: 'accounts-to-social-ids', + mode: 'upgrade', func: migrateAccounts }, // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION { state: 'migrate-social-ids-to-account-uuids', + mode: 'upgrade', func: migrateSocialIdsToAccountUuids } ]) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index d81e3a914b..e2157f9ab7 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -16,7 +16,6 @@ // import { - type PersonId, type Class, type Client, DOMAIN_MODEL, @@ -27,7 +26,8 @@ import { type Domain, type Ref, type Space, - type AnyAttribute + type AnyAttribute, + type AccountUuid } from '@hcengineering/core' import { type Builder, Mixin, Model, UX } from '@hcengineering/model' import core, { TClass, TDoc } from '@hcengineering/model-core' @@ -119,7 +119,7 @@ export class TFilteredView extends TDoc implements FilteredView { viewOptions?: ViewOptions filterClass?: Ref> viewletId?: Ref | null - users!: PersonId[] + users!: AccountUuid[] attachedTo!: string sharable?: boolean } diff --git a/models/view/src/migration.ts b/models/view/src/migration.ts index 47cdbe9ca8..228d1b5a73 100644 --- a/models/view/src/migration.ts +++ b/models/view/src/migration.ts @@ -23,10 +23,10 @@ import { } from '@hcengineering/model' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' import view, { type Filter, type FilteredView, type ViewletPreference, viewId } from '@hcengineering/view' -import { getSocialIdByOldAccount } from '@hcengineering/model-core' +import { getSocialKeyByOldAccount, getUniqueAccounts } from '@hcengineering/model-core' +import { type AccountUuid, MeasureMetricsContext } from '@hcengineering/core' import { DOMAIN_VIEW } from '.' -import { MeasureMetricsContext } from '@hcengineering/core' async function removeDoneStatePref (client: MigrationClient): Promise { const prefs = await client.find(DOMAIN_PREFERENCE, { @@ -83,7 +83,7 @@ async function removeDoneStateFilter (client: MigrationClient): Promise { async function migrateAccountsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('view migrateAccountsToSocialIds', {}) - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) ctx.info('processing view filtered view users ', {}) const iterator = await client.traverse(DOMAIN_VIEW, { _class: view.class.FilteredView }) @@ -103,7 +103,52 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise socialIdByAccount[u] ?? u) + const newUsers = filteredView.users.map((u) => socialKeyByAccount[u] ?? u) + + operations.push({ + filter: { _id: filteredView._id }, + update: { + users: newUsers as any + } + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_VIEW, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await iterator.close() + } + ctx.info('finished processing view filtered view users ', {}) +} + +async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('view migrateSocialIdsToGlobalAccounts', {}) + const accountUuidBySocialKey = new Map() + + ctx.info('processing view filtered view users ', {}) + const iterator = await client.traverse(DOMAIN_VIEW, { _class: view.class.FilteredView }) + + try { + let processed = 0 + while (true) { + const docs = await iterator.next(200) + if (docs === null || docs.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const doc of docs) { + const filteredView = doc as FilteredView + + if (filteredView.users === undefined || filteredView.users.length === 0) continue + + const newUsers = await getUniqueAccounts(client, filteredView.users, accountUuidBySocialKey) operations.push({ filter: { _id: filteredView._id }, @@ -141,7 +186,13 @@ export const viewOperation: MigrateOperation = { }, { state: 'accounts-to-social-ids', + mode: 'upgrade', func: migrateAccountsToSocialIds + }, + { + state: 'social-ids-to-global-accounts', + mode: 'upgrade', + func: migrateSocialIdsToGlobalAccounts } ]) }, diff --git a/models/workbench/src/index.ts b/models/workbench/src/index.ts index 2a346890f5..6c073ffdef 100644 --- a/models/workbench/src/index.ts +++ b/models/workbench/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole, type PersonId } from '@hcengineering/core' +import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole, type AccountUuid } from '@hcengineering/core' import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model' import preference, { TPreference } from '@hcengineering/model-preference' import { createAction } from '@hcengineering/model-view' @@ -106,7 +106,7 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent { @Model(workbench.class.WorkbenchTab, preference.class.Preference) @UX(workbench.string.Tab) export class TWorkbenchTab extends TPreference implements WorkbenchTab { - declare attachedTo: PersonId + declare attachedTo: AccountUuid location!: string name?: string isPinned!: boolean diff --git a/models/workbench/src/migration.ts b/models/workbench/src/migration.ts index 01939638a5..39012456dd 100644 --- a/models/workbench/src/migration.ts +++ b/models/workbench/src/migration.ts @@ -20,8 +20,8 @@ import { } from '@hcengineering/model' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' import workbench, { type WorkbenchTab } from '@hcengineering/workbench' -import core, { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core' -import { getSocialIdByOldAccount } from '@hcengineering/model-core' +import core, { type AccountUuid, DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core' +import { getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core' import { workbenchId } from '.' @@ -32,10 +32,10 @@ async function removeTabs (client: MigrationClient): Promise { async function migrateTabsToSocialIds (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('workbench migrateTabsToSocialIds', {}) ctx.info('migrating workbench tabs to social ids...') - const socialIdByAccount = await getSocialIdByOldAccount(client) + const socialKeyByAccount = await getSocialKeyByOldAccount(client) const tabs = await client.find(DOMAIN_PREFERENCE, { _class: workbench.class.WorkbenchTab }) for (const tab of tabs) { - const newAttachedTo = socialIdByAccount[tab.attachedTo] + const newAttachedTo: any = socialKeyByAccount[tab.attachedTo] if (newAttachedTo != null && newAttachedTo !== tab.attachedTo) { await client.update(DOMAIN_PREFERENCE, { _id: tab._id }, { attachedTo: newAttachedTo }) } @@ -43,6 +43,21 @@ async function migrateTabsToSocialIds (client: MigrationClient): Promise { ctx.info('migrating workbench tabs to social ids completed...') } +async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('workbench migrateSocialIdsToGlobalAccounts', {}) + ctx.info('migrating workbench tabs to global accounts...') + const accountUuidBySocialKey = new Map() + + const tabs = await client.find(DOMAIN_PREFERENCE, { _class: workbench.class.WorkbenchTab }) + for (const tab of tabs) { + const newAttachedTo = await getAccountUuidBySocialKey(client, tab.attachedTo, accountUuidBySocialKey) + if (newAttachedTo != null && newAttachedTo !== tab.attachedTo) { + await client.update(DOMAIN_PREFERENCE, { _id: tab._id }, { attachedTo: newAttachedTo }) + } + } + ctx.info('migrating workbench tabs to global accounts completed...') +} + export const workbenchOperation: MigrateOperation = { async migrate (client: MigrationClient, mode): Promise { await tryMigrate(mode, client, workbenchId, [ @@ -63,7 +78,13 @@ export const workbenchOperation: MigrateOperation = { }, { state: 'tabs-accounts-to-social-ids', + mode: 'upgrade', func: migrateTabsToSocialIds + }, + { + state: 'tabs-social-ids-to-global-accounts', + mode: 'upgrade', + func: migrateSocialIdsToGlobalAccounts } ]) }, diff --git a/packages/account-client/src/client.ts b/packages/account-client/src/client.ts index 709d7b8bc2..9c8d720737 100644 --- a/packages/account-client/src/client.ts +++ b/packages/account-client/src/client.ts @@ -80,7 +80,9 @@ export interface AccountClient { updateWorkspaceRole: (account: string, role: AccountRole) => Promise updateWorkspaceName: (name: string) => Promise deleteWorkspace: () => Promise - findPerson: (socialString: PersonId) => Promise + findPersonBySocialKey: (socialKey: string) => Promise + findPersonBySocialId: (socialId: PersonId) => Promise + findSocialIdBySocialKey: (socialKey: string) => Promise // Service methods workerHandshake: (region: string, version: Data, operation: WorkspaceOperation) => Promise @@ -104,7 +106,7 @@ export interface AccountClient { ) => Promise assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise updateBackupInfo: (info: BackupStatus) => Promise - updateWorkspaceRoleBySocialId: (socialKey: string, targetRole: AccountRole) => Promise + updateWorkspaceRoleBySocialKey: (socialKey: string, targetRole: AccountRole) => Promise ensurePerson: ( socialType: SocialIdType, socialValue: string, @@ -530,15 +532,33 @@ class AccountClientImpl implements AccountClient { await this.rpc(request) } - async findPerson (socialString: string): Promise { + async findPersonBySocialKey (socialString: string): Promise { const request = { - method: 'findPerson' as const, + method: 'findPersonBySocialKey' as const, params: { socialString } } return await this.rpc(request) } + async findPersonBySocialId (socialId: PersonId): Promise { + const request = { + method: 'findPersonBySocialId' as const, + params: { socialId } + } + + return await this.rpc(request) + } + + async findSocialIdBySocialKey (socialKey: string): Promise { + const request = { + method: 'findSocialIdBySocialKey' as const, + params: { socialKey } + } + + return await this.rpc(request) + } + async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise { const request = { method: 'listWorkspaces' as const, @@ -579,9 +599,9 @@ class AccountClientImpl implements AccountClient { await this.rpc(request) } - async updateWorkspaceRoleBySocialId (socialKey: string, targetRole: AccountRole): Promise { + async updateWorkspaceRoleBySocialKey (socialKey: string, targetRole: AccountRole): Promise { const request = { - method: 'updateWorkspaceRoleBySocialId' as const, + method: 'updateWorkspaceRoleBySocialKey' as const, params: { socialKey, targetRole } } diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts index cb0fb8abe0..cf49f0c0e9 100644 --- a/packages/api-client/src/client.ts +++ b/packages/api-client/src/client.ts @@ -37,8 +37,8 @@ import { PersonId, TxOperations, WorkspaceUuid, - buildSocialIdString, - generateId + generateId, + pickPrimarySocialId } from '@hcengineering/core' import { addLocation, getResource } from '@hcengineering/platform' @@ -60,19 +60,14 @@ export async function connect (url: string, options: ConnectOptions): Promise buildSocialIdString(si)) - - if (socialStrings.length === 0) { - throw new Error('No social ids found for the logged in user') - } - + const socialId = pickPrimarySocialId(await accountClient.getSocialIds()) const wsLoginInfo = await accountClient.selectWorkspace(options.workspace) if (wsLoginInfo === undefined) { throw new Error(`Workspace ${options.workspace} not found`) } - return await createClient(url, endpoint, token, wsLoginInfo.workspace, socialStrings[0], config, options) + return await createClient(url, endpoint, token, wsLoginInfo.workspace, socialId._id, config, options) } async function createClient ( diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 289da90a2d..4ab84c423c 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -91,8 +91,7 @@ export type AccountUuid = PersonUuid & { __accountUuid: true } /** * @public - * String representation of a social id linked to a global person. - * E.g. email:pied.piper@hcengineering.com or huly:ea3bf257-94b5-4a31-a7da-466d578d850f + * Generated identifier of a social id linked to a global person. */ export type PersonId = string & { __personId: true } @@ -863,11 +862,19 @@ export enum SocialIdType { HULY = 'huly', TELEGRAM = 'telegram' } + export interface SocialId { - id: string + // generated ID so the actual social ID can be detached from a person w/o losing the ID in the linked database records + _id: PersonId + + // Should never be changed after creation type: SocialIdType value: string - key: PersonId + key: string // Calculated from type and value. Just for convenience. + + // To be used later when person detaches social id from his account by any means + // There should always be only one ACTIVE social id with the same key every time + // active: boolean verifiedOn?: number } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 0cff96ae71..70d3ab09bf 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -34,7 +34,6 @@ import { IndexKind, Obj, Permission, - PersonId, Ref, Role, roleOrder, @@ -907,13 +906,21 @@ export function combineAttributes ( ).filter((v) => v != null) } -export function buildSocialIdString (key: SocialKey): PersonId { - return `${key.type}:${key.value}` as PersonId +export function buildSocialIdString (key: SocialKey): string { + return `${key.type}:${key.value}` } -export function parseSocialIdString (id: PersonId): SocialKey { +export function parseSocialIdString (id: string): SocialKey { const [type, value] = id.split(':') + if (type === undefined || value === undefined) { + throw new Error(`Social id is not valid: ${id}`) + } + + if (!Object.values(SocialIdType).includes(type as SocialIdType)) { + throw new Error(`Social id type is not valid: ${id}`) + } + return { type: type as SocialIdType, value } } diff --git a/packages/importer/src/huly/huly.ts b/packages/importer/src/huly/huly.ts index 20c5afbbed..0d09a78cb6 100644 --- a/packages/importer/src/huly/huly.ts +++ b/packages/importer/src/huly/huly.ts @@ -343,6 +343,7 @@ export class HulyFormatImporter { private personsByName = new Map>() private employeesByName = new Map>() private accountsByEmail = new Map() + private readonly personIdByEmail = new Map() constructor ( private readonly client: TxOperations, @@ -606,12 +607,28 @@ export class HulyFormatImporter { return person } - private getSocialIdByEmail (email: string): PersonId { + private async getPersonIdByEmail (email: string): Promise { if (email === this.importerEmailPlaceholder && this.importerSocialId != null) { return this.importerSocialId } - return buildSocialIdString({ type: SocialIdType.EMAIL, value: email }) + const personId = this.personIdByEmail.get(email) + if (personId !== undefined) { + return personId + } + + const socialId = await this.client.findOne(contact.class.SocialIdentity, { + type: SocialIdType.EMAIL, + value: email + }) + + if (socialId === undefined) { + throw new Error(`Social ID not found for email: ${email}`) + } + + this.personIdByEmail.set(email, socialId._id) + + return socialId._id } private findAccountByEmail (email: string): AccountUuid { @@ -767,7 +784,7 @@ export class HulyFormatImporter { } return { text: comment.text, - author: this.getSocialIdByEmail(comment.author), + author: await this.getPersonIdByEmail(comment.author), attachments } }) @@ -969,6 +986,24 @@ export class HulyFormatImporter { }, new Map()) } + private async cachePersonIdsByEmails (): Promise { + const employees = await this.client.findAll( + contact.mixin.Employee, + { active: true }, + { lookup: { _id: { socialIds: contact.class.SocialIdentity } } } + ) + + this.accountsByEmail = employees.reduce((map, employee) => { + employee.$lookup?.socialIds?.forEach((socialId) => { + if ((socialId as SocialIdentity).type === SocialIdType.EMAIL) { + map.set((socialId as SocialIdentity).value, employee.personUuid) + } + }) + + return map + }, new Map()) + } + private async cachePersonsByNames (): Promise { this.personsByName = (await this.client.findAll(contact.class.Person, {})) .map((person) => { diff --git a/plugins/activity-resources/src/components/reactions/Reactions.svelte b/plugins/activity-resources/src/components/reactions/Reactions.svelte index d34dca7c53..54ddcdfca6 100644 --- a/plugins/activity-resources/src/components/reactions/Reactions.svelte +++ b/plugins/activity-resources/src/components/reactions/Reactions.svelte @@ -29,16 +29,16 @@ const dispatch = createEventDispatcher() const me = getCurrentAccount() - let reactionsAccounts = new Map() + let reactionsPersons = new Map() let opened: boolean = false $: { - reactionsAccounts.clear() + reactionsPersons.clear() reactions.forEach((r) => { - const accounts = reactionsAccounts.get(r.emoji) ?? [] - reactionsAccounts.set(r.emoji, [...accounts, r.createBy]) + const persons = reactionsPersons.get(r.emoji) ?? [] + reactionsPersons.set(r.emoji, [...persons, r.createBy]) }) - reactionsAccounts = reactionsAccounts + reactionsPersons = reactionsPersons } function getClickHandler (emoji: string): ((e: CustomEvent) => void) | undefined { @@ -63,21 +63,21 @@

- {#each [...reactionsAccounts] as [emoji, accounts]} + {#each [...reactionsPersons] as [emoji, persons]}
{emoji} - {accounts.length} + {persons.length}
{/each} - {#if object && reactionsAccounts.size > 0 && !readonly} + {#if object && reactionsPersons.size > 0 && !readonly}
diff --git a/plugins/ai-bot-resources/package.json b/plugins/ai-bot-resources/package.json index 797989cead..0907e66d5a 100644 --- a/plugins/ai-bot-resources/package.json +++ b/plugins/ai-bot-resources/package.json @@ -45,6 +45,8 @@ "@hcengineering/love": "^0.6.0", "@hcengineering/platform": "^0.6.11", "@hcengineering/presentation": "^0.6.3", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", "@hcengineering/ui": "^0.6.15", "svelte": "^4.2.19" } diff --git a/plugins/ai-bot-resources/src/index.ts b/plugins/ai-bot-resources/src/index.ts index 7a6cbaf4b6..f754410903 100644 --- a/plugins/ai-bot-resources/src/index.ts +++ b/plugins/ai-bot-resources/src/index.ts @@ -16,5 +16,6 @@ import { type Resources } from '@hcengineering/platform' export * from './requests' +export * from './utils' export default async (): Promise => ({}) diff --git a/plugins/ai-bot-resources/src/utils.ts b/plugins/ai-bot-resources/src/utils.ts new file mode 100644 index 0000000000..2f60f5e838 --- /dev/null +++ b/plugins/ai-bot-resources/src/utils.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2025 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 { derived, writable } from 'svelte/store' +import contact, { type SocialIdentity } from '@hcengineering/contact' +import { personRefByPersonIdStore } from '@hcengineering/contact-resources' +import { createQuery, onClient } from '@hcengineering/presentation' +import { aiBotEmailSocialKey } from '@hcengineering/ai-bot' + +export const aiBotSocialIdentityStore = writable() +const identityQuery = createQuery(true) + +export const aiBotPersonRefStore = derived( + [personRefByPersonIdStore, aiBotSocialIdentityStore], + ([personRefByPersonId, aiBotSocialIdentity]) => { + return personRefByPersonId.get(aiBotSocialIdentity?._id) + } +) + +onClient(() => { + identityQuery.query(contact.class.SocialIdentity, { key: aiBotEmailSocialKey }, (res) => { + aiBotSocialIdentityStore.set(res[0]) + }) +}) diff --git a/plugins/ai-bot/src/index.ts b/plugins/ai-bot/src/index.ts index 58b6d0ce3e..d3b2350ced 100644 --- a/plugins/ai-bot/src/index.ts +++ b/plugins/ai-bot/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { AccountUuid, buildSocialIdString, SocialIdType } from '@hcengineering/core' +import { buildSocialIdString, SocialIdType } from '@hcengineering/core' import type { Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -21,9 +21,8 @@ export * from './rest' export const aiBotId = 'ai-bot' as Plugin -export const aiBotAccountUuid = '' as AccountUuid export const aiBotAccountEmail = 'huly.ai.bot@hc.engineering' -export const aiBotEmailSocialId = buildSocialIdString({ +export const aiBotEmailSocialKey = buildSocialIdString({ type: SocialIdType.EMAIL, value: aiBotAccountEmail }) diff --git a/plugins/analytics-collector-assets/lang/cs.json b/plugins/analytics-collector-assets/lang/cs.json index 4350ffa98a..7362bd4878 100644 --- a/plugins/analytics-collector-assets/lang/cs.json +++ b/plugins/analytics-collector-assets/lang/cs.json @@ -15,6 +15,7 @@ "WorkspaceName": "Název pracovního prostoru", "WorkspaceUrl": "URL pracovního prostoru", "Email": "E-mail", + "Account": "Účet", "UserName": "Uživatelské jméno", "DisableAIReplies": "Zakázat odpovědi AI", "ShowAIReplies": "Zobrazit odpovědi AI" diff --git a/plugins/analytics-collector-assets/lang/en.json b/plugins/analytics-collector-assets/lang/en.json index eeeef6ecdd..2f5f302300 100644 --- a/plugins/analytics-collector-assets/lang/en.json +++ b/plugins/analytics-collector-assets/lang/en.json @@ -14,7 +14,7 @@ "WorkspaceId": "Workspace id", "WorkspaceName": "Workspace name", "WorkspaceUrl": "Workspace url", - "SocialId": "Social Id", + "Account": "Account", "UserName": "User name", "DisableAIReplies": "Disable AI replies", "ShowAIReplies": "Show AI replies" diff --git a/plugins/analytics-collector-assets/lang/es.json b/plugins/analytics-collector-assets/lang/es.json index a00892a8b3..dd98214614 100644 --- a/plugins/analytics-collector-assets/lang/es.json +++ b/plugins/analytics-collector-assets/lang/es.json @@ -14,7 +14,7 @@ "WorkspaceId": "Workspace id", "WorkspaceName": "Workspace name", "WorkspaceUrl": "Workspace url", - "SocialId": "Social Id", + "Account": "Cuenta", "UserName": "User name", "DisableAIReplies": "Disable AI replies", "ShowAIReplies": "Show AI replies" diff --git a/plugins/analytics-collector-assets/lang/pt.json b/plugins/analytics-collector-assets/lang/pt.json index a00892a8b3..26e0fa1d1f 100644 --- a/plugins/analytics-collector-assets/lang/pt.json +++ b/plugins/analytics-collector-assets/lang/pt.json @@ -14,7 +14,7 @@ "WorkspaceId": "Workspace id", "WorkspaceName": "Workspace name", "WorkspaceUrl": "Workspace url", - "SocialId": "Social Id", + "Account": "Conta", "UserName": "User name", "DisableAIReplies": "Disable AI replies", "ShowAIReplies": "Show AI replies" diff --git a/plugins/analytics-collector-assets/lang/ru.json b/plugins/analytics-collector-assets/lang/ru.json index 4f68cd1d12..d656f6b992 100644 --- a/plugins/analytics-collector-assets/lang/ru.json +++ b/plugins/analytics-collector-assets/lang/ru.json @@ -14,7 +14,7 @@ "WorkspaceId": "Id пространства", "WorkspaceName": "Имя пространства", "WorkspaceUrl": "URL пространства", - "SocialId": "Социальный ID", + "Account": "Учетная запись", "UserName": "Имя пользователя", "DisableAIReplies": "Отключить ответы ИИ", "ShowAIReplies": "Показать ответы ИИ" diff --git a/plugins/analytics-collector/src/index.ts b/plugins/analytics-collector/src/index.ts index c912d371d6..32921af6c1 100644 --- a/plugins/analytics-collector/src/index.ts +++ b/plugins/analytics-collector/src/index.ts @@ -49,7 +49,7 @@ const analyticsCollector = plugin(analyticsCollectorId, { WorkspaceId: '' as IntlString, WorkspaceName: '' as IntlString, WorkspaceUrl: '' as IntlString, - SocialId: '' as IntlString, + Account: '' as IntlString, UserName: '' as IntlString, DisableAIReplies: '' as IntlString, ShowAIReplies: '' as IntlString diff --git a/plugins/analytics-collector/src/types.ts b/plugins/analytics-collector/src/types.ts index 88000c4cd4..4cdb3c7f35 100644 --- a/plugins/analytics-collector/src/types.ts +++ b/plugins/analytics-collector/src/types.ts @@ -14,7 +14,7 @@ // import { Channel } from '@hcengineering/chunter' -import type { PersonId, WorkspaceUuid } from '@hcengineering/core' +import type { AccountUuid, WorkspaceUuid } from '@hcengineering/core' export enum AnalyticEventType { SetUser = 'setUser', @@ -34,7 +34,7 @@ export interface OnboardingChannel extends Channel { workspaceId: WorkspaceUuid workspaceName: string workspaceUrl: string - socialString: PersonId + account: AccountUuid userName: string disableAIReplies: boolean showAIReplies: boolean diff --git a/plugins/analytics-collector/src/utils.ts b/plugins/analytics-collector/src/utils.ts index fe9aa776a8..a093acf4ad 100644 --- a/plugins/analytics-collector/src/utils.ts +++ b/plugins/analytics-collector/src/utils.ts @@ -13,8 +13,6 @@ // limitations under the License. // -import type { PersonId } from '@hcengineering/core' - -export function getOnboardingChannelName (worksapceUrl: string, personId: PersonId): string { - return `${personId}; ${worksapceUrl}` +export function getOnboardingChannelName (worksapceUrl: string, name: string): string { + return `${name}; ${worksapceUrl}` } diff --git a/plugins/attachment-resources/src/components/FileBrowser.svelte b/plugins/attachment-resources/src/components/FileBrowser.svelte index 72f594930f..37665c92dd 100644 --- a/plugins/attachment-resources/src/components/FileBrowser.svelte +++ b/plugins/attachment-resources/src/components/FileBrowser.svelte @@ -70,7 +70,7 @@ attachedTo: { $in: selectedParticipants_ }, attachedToClass: contact.class.Person }) - const senderQuery = allSocialIds.length !== 0 ? { modifiedBy: { $in: allSocialIds.map((si) => si.key) } } : {} + const senderQuery = allSocialIds.length !== 0 ? { modifiedBy: { $in: allSocialIds.map((si) => si._id) } } : {} let spaceQuery: { space: any } if (selectedSpaces_.length > 0) { diff --git a/plugins/calendar-resources/src/components/CalendarView.svelte b/plugins/calendar-resources/src/components/CalendarView.svelte index efbb0ddb0e..a1574283cc 100644 --- a/plugins/calendar-resources/src/components/CalendarView.svelte +++ b/plugins/calendar-resources/src/components/CalendarView.svelte @@ -260,7 +260,7 @@ attachedToClass: dragItem._class, _class: dragEventClass, collection: 'events', - calendar: `${myPrimaryId}_calendar` as Ref, + calendar: `${acc.uuid}_calendar` as Ref, modifiedBy: myPrimaryId, participants: [me], modifiedOn: Date.now(), diff --git a/plugins/calendar-resources/src/components/CreateEvent.svelte b/plugins/calendar-resources/src/components/CreateEvent.svelte index 76d6fac5b2..b26b6ae500 100644 --- a/plugins/calendar-resources/src/components/CreateEvent.svelte +++ b/plugins/calendar-resources/src/components/CreateEvent.svelte @@ -77,7 +77,7 @@ let description: Markup = EmptyMarkup let visibility: Visibility = 'private' - let _calendar: Ref = `${myPrimaryId}_calendar` as Ref + let _calendar: Ref = `${acc.uuid}_calendar` as Ref const q = createQuery() q.query( diff --git a/plugins/calendar-resources/src/components/CreateReminder.svelte b/plugins/calendar-resources/src/components/CreateReminder.svelte index 07f4981b2f..ddba6eb844 100644 --- a/plugins/calendar-resources/src/components/CreateReminder.svelte +++ b/plugins/calendar-resources/src/components/CreateReminder.svelte @@ -44,7 +44,7 @@ let date: number | undefined if (value != null) date = value if (date === undefined) return - const _calendar = `${getCurrentAccount().primarySocialId}_calendar` as Ref + const _calendar = `${getCurrentAccount().uuid}_calendar` as Ref await client.addCollection(calendar.class.Event, calendar.space.Calendar, attachedTo, attachedToClass, 'events', { calendar: _calendar, eventId: generateEventId(), diff --git a/plugins/contact-resources/src/components/CreateEmployee.svelte b/plugins/contact-resources/src/components/CreateEmployee.svelte index 73535fd3ba..29ce9fbff1 100644 --- a/plugins/contact-resources/src/components/CreateEmployee.svelte +++ b/plugins/contact-resources/src/components/CreateEmployee.svelte @@ -13,7 +13,15 @@ // limitations under the License. --> diff --git a/plugins/products-resources/src/components/product-version/EditProductVersion.svelte b/plugins/products-resources/src/components/product-version/EditProductVersion.svelte index 255a879caf..ad77c4fed3 100644 --- a/plugins/products-resources/src/components/product-version/EditProductVersion.svelte +++ b/plugins/products-resources/src/components/product-version/EditProductVersion.svelte @@ -18,7 +18,6 @@ {#if value.status === RequestStatus.Active} - {#if value.createdBy !== undefined && $mySocialStringsStore.includes(value.createdBy)} + {#if value.createdBy !== undefined && account.socialIds.includes(value.createdBy)}
diff --git a/plugins/setting-resources/src/components/Profile.svelte b/plugins/setting-resources/src/components/Profile.svelte index a1c8355ac4..9764c05e48 100644 --- a/plugins/setting-resources/src/components/Profile.svelte +++ b/plugins/setting-resources/src/components/Profile.svelte @@ -34,6 +34,7 @@ import setting from '../plugin' const client = getClient() + const account = getCurrentAccount() const me = getCurrentEmployee() $: employee = $personByIdStore.get(me) @@ -60,7 +61,7 @@ message: setting.string.LeaveDescr, action: async () => { const leaveWorkspace = await getResource(login.function.LeaveWorkspace) - const loginInfo = await leaveWorkspace(getCurrentAccount().uuid) + const loginInfo = await leaveWorkspace(account.uuid) if (loginInfo?.token != null) { await logIn(loginInfo) diff --git a/plugins/task-resources/src/components/AssignedTasks.svelte b/plugins/task-resources/src/components/AssignedTasks.svelte index f15e0088a5..0162428f7d 100644 --- a/plugins/task-resources/src/components/AssignedTasks.svelte +++ b/plugins/task-resources/src/components/AssignedTasks.svelte @@ -18,7 +18,6 @@ import { createQuery } from '@hcengineering/presentation' import { Task } from '@hcengineering/task' import { getCurrentEmployee } from '@hcengineering/contact' - import { mySocialStringsStore } from '@hcengineering/contact-resources' import { Component, IModeSelector, @@ -49,8 +48,9 @@ let search = '' const dispatch = createEventDispatcher() const me = getCurrentEmployee() + const account = getCurrentAccount() const assigned = { assignee: me } - $: created = { createdBy: { $in: $mySocialStringsStore } } + $: created = { createdBy: { $in: account.socialIds } } let subscribed = { _id: { $in: [] as Ref[] } } let mode: string | undefined = undefined let baseQuery: DocumentQuery | undefined = undefined diff --git a/plugins/time-resources/src/components/CreateToDoPopup.svelte b/plugins/time-resources/src/components/CreateToDoPopup.svelte index cc0e85c9ae..d3b0bf058e 100644 --- a/plugins/time-resources/src/components/CreateToDoPopup.svelte +++ b/plugins/time-resources/src/components/CreateToDoPopup.svelte @@ -30,7 +30,6 @@ import DueDateEditor from './DueDateEditor.svelte' import PriorityEditor from './PriorityEditor.svelte' import Workslots from './Workslots.svelte' - import { mySocialStringsStore } from '@hcengineering/contact-resources' export let object: Doc | undefined @@ -112,12 +111,12 @@ dispatch('close', true) } - let _calendar: Ref = `${myAccount.primarySocialId}_calendar` as Ref + let _calendar: Ref = `${myAccount.uuid}_calendar` as Ref const q = createQuery() q.query( calendar.class.ExternalCalendar, - { default: true, hidden: false, createdBy: { $in: $mySocialStringsStore } }, + { default: true, hidden: false, createdBy: { $in: myAccount.socialIds } }, (res) => { if (res.length > 0) { _calendar = res[0]._id diff --git a/plugins/time-resources/src/components/PlanView.svelte b/plugins/time-resources/src/components/PlanView.svelte index 594bc50f0b..40346416ad 100644 --- a/plugins/time-resources/src/components/PlanView.svelte +++ b/plugins/time-resources/src/components/PlanView.svelte @@ -54,7 +54,7 @@ hidden: false, default: true }) - const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.primarySocialId}_calendar` as Ref) + const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.uuid}_calendar` as Ref) const dueDate = date + defaultDuration await client.addCollection(time.class.WorkSlot, calendar.space.Calendar, doc._id, doc._class, 'workslots', { calendar: _calendar, diff --git a/plugins/time-resources/src/components/PlanningCalendar.svelte b/plugins/time-resources/src/components/PlanningCalendar.svelte index b170107513..9518db2297 100644 --- a/plugins/time-resources/src/components/PlanningCalendar.svelte +++ b/plugins/time-resources/src/components/PlanningCalendar.svelte @@ -114,7 +114,7 @@ current.date = e.detail.date.getTime() current.dueDate = new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30) } else { - const _calendar = `${myAcc.primarySocialId}_calendar` as Ref + const _calendar = `${myAcc.uuid}_calendar` as Ref const ev: WorkSlot = { _id: dragItemId, allDay: false, @@ -129,7 +129,7 @@ visibility: 'public', calendar: _calendar, space: calendar.space.Calendar, - modifiedBy: getCurrentAccount().primarySocialId, + modifiedBy: myAcc.primarySocialId, participants: [getCurrentEmployee()], modifiedOn: Date.now(), date: e.detail.date.getTime(), diff --git a/plugins/time-resources/src/components/TodoWorkslots.svelte b/plugins/time-resources/src/components/TodoWorkslots.svelte index 35e724acc3..5bae3119c1 100644 --- a/plugins/time-resources/src/components/TodoWorkslots.svelte +++ b/plugins/time-resources/src/components/TodoWorkslots.svelte @@ -61,7 +61,7 @@ hidden: false, default: true }) - const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.primarySocialId}_calendar` as Ref) + const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.uuid}_calendar` as Ref) const dueDate = date + defaultDuration await client.addCollection(time.class.WorkSlot, calendar.space.Calendar, todo._id, todo._class, 'workslots', { eventId: generateEventId(), diff --git a/plugins/time-resources/src/components/team/calendar/TeamCalendar.svelte b/plugins/time-resources/src/components/team/calendar/TeamCalendar.svelte index c05c19fc10..f5827634d7 100644 --- a/plugins/time-resources/src/components/team/calendar/TeamCalendar.svelte +++ b/plugins/time-resources/src/components/team/calendar/TeamCalendar.svelte @@ -49,7 +49,7 @@ let txes = new Map, Tx[]>() - $: personsSocialStrings = persons.map((p) => ($socialIdsByPersonRefStore.get(p) ?? []).map((si) => si.key)).flat() + $: personsSocialStrings = persons.map((p) => ($socialIdsByPersonRefStore.get(p) ?? []).map((si) => si._id)).flat() $: txCreateQuery.query( core.class.Tx, { modifiedBy: { $in: personsSocialStrings }, modifiedOn: { $gt: fromDate, $lt: toDate } }, diff --git a/plugins/view-resources/src/components/filter/FilterSave.svelte b/plugins/view-resources/src/components/filter/FilterSave.svelte index 4b97deefa4..ee5033c5da 100644 --- a/plugins/view-resources/src/components/filter/FilterSave.svelte +++ b/plugins/view-resources/src/components/filter/FilterSave.svelte @@ -29,7 +29,7 @@ viewOptions, viewletId: getActiveViewletId(), sharable, - users: [getCurrentAccount().primarySocialId] + users: [getCurrentAccount().uuid] }) } diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index 2eb39cd675..e375f3b1f5 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -37,7 +37,8 @@ import { Tx, TxOperations, Type, - UXObject + UXObject, + AccountUuid } from '@hcengineering/core' import { Asset, IntlString, Resource, Status } from '@hcengineering/platform' import { Preference } from '@hcengineering/preference' @@ -112,7 +113,7 @@ export interface FilteredView extends Doc { filterClass?: Ref> viewletId?: Ref | null sharable?: boolean - users: PersonId[] + users: AccountUuid[] createdBy: PersonId attachedTo: string } diff --git a/plugins/workbench-resources/src/components/SavedView.svelte b/plugins/workbench-resources/src/components/SavedView.svelte index 96ce903aae..b88aa73a46 100644 --- a/plugins/workbench-resources/src/components/SavedView.svelte +++ b/plugins/workbench-resources/src/components/SavedView.svelte @@ -1,5 +1,5 @@