diff --git a/.vscode/launch.json b/.vscode/launch.json index 0100c0e635..e4d2215fa1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,14 +33,56 @@ "type": "node", "request": "launch", "args": ["src/__start.ts"], + "env": { + // "FULLTEXT_URL": "http://localhost:4700", + "FULLTEXT_URL": "http://huly.local:4702", + // "MONGO_URL": "mongodb://localhost:27017", + "DB_URL": "mongodb://localhost:27017", + // "DB_URL": "postgresql://postgres:example@localhost:5432", + // "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", + // "GREEN_URL": "http://huly.local:6767?token=secret", + "SERVER_PORT": "3333", + "APM_SERVER_URL2": "http://localhost:8200", + "METRICS_CONSOLE": "false", + "METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds., + "STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin", + "SERVER_SECRET": "secret", + "ENABLE_CONSOLE": "true", + "COLLABORATOR_URL": "ws://localhost:3078", + "REKONI_URL": "http://localhost:4004", + "FRONT_URL": "http://localhost:8080", + "ACCOUNTS_URL": "http://localhost:3000", + "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", + // "SERVER_PROVIDER":"uweb" + "SERVER_PROVIDER":"ws", + "MODEL_VERSION": "0.7.1", + // "VERSION": "0.6.289", + "ELASTIC_INDEX_NAME": "local_storage_index", + "UPLOAD_URL": "/files", + "AI_BOT_URL": "http://localhost:4010", + "STATS_URL": "http://huly.local:4900" + }, + "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], + "runtimeVersion": "20", + "showAsyncStacks": true, + "outputCapture": "std", + "sourceMaps": true, + "cwd": "${workspaceRoot}/pods/server", + "protocol": "inspector" + }, + { + "name": "Debug server (CR)", + "type": "node", + "request": "launch", + "args": ["src/__start.ts"], "env": { // "FULLTEXT_URL": "http://localhost:4700", "FULLTEXT_URL": "http://huly.local:4702", // "MONGO_URL": "mongodb://localhost:27017", // "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", - "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable", - "GREEN_URL": "http://huly.local:6767?token=secret", + "DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable", + // "GREEN_URL": "http://huly.local:6767?token=secret", "SERVER_PORT": "3332", "APM_SERVER_URL2": "http://localhost:8200", "METRICS_CONSOLE": "false", @@ -379,7 +421,7 @@ "name": "Debug tool upgrade", "type": "node", "request": "launch", - "args": ["src/__start.ts", "upgrade", "--force"], + "args": ["src/__start.ts", "upgrade-workspace", "mongo-1000-1"], "env": { "SERVER_SECRET": "secret", "MINIO_ACCESS_KEY": "minioadmin", @@ -404,7 +446,7 @@ "name": "Debug tool upgrade PG(Cockroach)", "type": "node", "request": "launch", - "args": ["src/__start.ts", "upgrade-workspace", "w-haiodo-alex-staff-c-673ee7ab-87df5406ea-2b8b4d" ], + "args": ["src/__start.ts", "upgrade-workspace", "platform" ], "env": { "SERVER_SECRET": "secret", "MINIO_ACCESS_KEY": "minioadmin", diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 99ca8cf30a..d24b9fbe8b 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -21,6 +21,8 @@ services: restart: unless-stopped cockroach: image: cockroachdb/cockroach:latest-v24.2 + extra_hosts: + - 'huly.local:host-gateway' ports: - '26257:26257' - '8089:8080' diff --git a/dev/tool/src/benchmark.ts b/dev/tool/src/benchmark.ts index 74cce54c54..ea6dbd89b2 100644 --- a/dev/tool/src/benchmark.ts +++ b/dev/tool/src/benchmark.ts @@ -33,7 +33,8 @@ import core, { SocialIdType, type PersonUuid, platformNow, - platformNowDiff + platformNowDiff, + type AccountUuid } from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' import { connect } from '@hcengineering/server-tool' @@ -579,24 +580,25 @@ export async function generateWorkspaceData ( try { const emailSocialString = buildSocialIdString({ type: SocialIdType.EMAIL, value: email }) const person = await getPersonBySocialId(client, emailSocialString) - if (person == null) { + const account = person?.personUuid as AccountUuid + if (account == null) { throw new Error('User not found') } - const employees: PersonId[] = [emailSocialString] + const accounts: AccountUuid[] = [account] const start = platformNow() for (let i = 0; i < 100; i++) { - const socialString = await generateEmployee(client) - employees.push(socialString) + const acc = await generateEmployee(client) + accounts.push(acc) } if (parallel) { const promises: Promise[] = [] for (let i = 0; i < 10; i++) { - promises.push(generateVacancy(client, employees)) + promises.push(generateVacancy(client, accounts)) } await Promise.all(promises) } else { for (let i = 0; i < 10; i++) { - await generateVacancy(client, employees) + await generateVacancy(client, accounts) } } console.log('Generate', platformNowDiff(start)) @@ -605,8 +607,8 @@ export async function generateWorkspaceData ( } } -export async function generateEmployee (client: TxOperations): Promise { - const personUuid = generateId() as unknown as PersonUuid // TODO: will it work or need to actually be a UUID? +export async function generateEmployee (client: TxOperations): Promise { + const personUuid = generateId() as unknown as AccountUuid // TODO: will it work or need to actually be a UUID? const personId = await client.createDoc(contact.class.Person, contact.space.Contacts, { name: generateId().toString(), city: '', @@ -634,10 +636,10 @@ export async function generateEmployee (client: TxOperations): Promise } ) - return socialString + return personUuid } -async function generateVacancy (client: TxOperations, members: PersonId[]): Promise { +async function generateVacancy (client: TxOperations, members: AccountUuid[]): Promise { // generate vacancies const _id = generateId() await client.createDoc( diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 7d532d0da7..3966a8c92d 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -25,7 +25,7 @@ import accountPlugin, { type AccountDB } from '@hcengineering/account' import { setMetadata } from '@hcengineering/platform' -import { createFileBackupStorage, createStorageBackupStorage, restore } from '@hcengineering/server-backup' +import { backup, createFileBackupStorage, createStorageBackupStorage, restore } from '@hcengineering/server-backup' import serverClientPlugin, { getAccountClient } from '@hcengineering/server-client' import { registerAdapterFactory, @@ -852,49 +852,60 @@ export function devTool ( // }) // }) - // program - // .command('backup ') - // .description('dump workspace transactions and minio resources') - // .option('-i, --include ', 'A list of ; separated domain names to include during backup', '*') - // .option('-s, --skip ', 'A list of ; separated domain names to skip during backup', '') - // .option( - // '-ct, --contentTypes ', - // 'A list of ; separated content types for blobs to skip download if size >= limit', - // '' - // ) - // .option('-bl, --blobLimit ', 'A blob size limit in megabytes (default 15mb)', '15') - // .option('-f, --force', 'Force backup', false) - // .option('-t, --timeout ', 'Connect timeout in seconds', '30') - // .action( - // async ( - // dirName: string, - // workspace: string, - // cmd: { - // skip: string - // force: boolean - // timeout: string - // include: string - // blobLimit: string - // contentTypes: string - // } - // ) => { - // const storage = await createFileBackupStorage(dirName) - // const wsid = getWorkspaceId(workspace) - // const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external') - // await backup(toolCtx, endpoint, wsIds, storage, { - // force: cmd.force, - // include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())), - // skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()), - // timeout: 0, - // connectTimeout: parseInt(cmd.timeout) * 1000, - // blobDownloadLimit: parseInt(cmd.blobLimit), - // skipBlobContentTypes: cmd.contentTypes - // .split(';') - // .map((it) => it.trim()) - // .filter((it) => it.length > 0) - // }) - // } - // ) + program + .command('backup ') + .description('dump workspace transactions and minio resources') + .option('-i, --include ', 'A list of ; separated domain names to include during backup', '*') + .option('-s, --skip ', 'A list of ; separated domain names to skip during backup', '') + .option( + '-ct, --contentTypes ', + 'A list of ; separated content types for blobs to skip download if size >= limit', + '' + ) + .option('-bl, --blobLimit ', 'A blob size limit in megabytes (default 15mb)', '15') + .option('-f, --force', 'Force backup', false) + .option('-t, --timeout ', 'Connect timeout in seconds', '30') + .action( + async ( + dirName: string, + workspace: string, + cmd: { + skip: string + force: boolean + timeout: string + include: string + blobLimit: string + contentTypes: string + } + ) => { + const storage = await createFileBackupStorage(dirName) + await withAccountDatabase(async (db) => { + const ws = await getWorkspace(db, workspace) + if (ws === null) { + throw new Error(`workspace ${workspace} not found`) + } + const wsIds = { + uuid: ws.uuid, + dataId: ws.dataId, + url: ws.url + } + const endpoint = await getWorkspaceTransactorEndpoint(ws.uuid) + + await backup(toolCtx, endpoint, wsIds, storage, { + force: cmd.force, + include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())), + skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()), + timeout: 0, + connectTimeout: parseInt(cmd.timeout) * 1000, + blobDownloadLimit: parseInt(cmd.blobLimit), + skipBlobContentTypes: cmd.contentTypes + .split(';') + .map((it) => it.trim()) + .filter((it) => it.length > 0) + }) + }) + } + ) // program // .command('backup-find ') // .description('dump workspace transactions and minio resources') diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index 9236e5a688..e790dbc1fe 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -70,6 +70,15 @@ export async function diffWorkspace (mongoUrl: string, dbName: string, rawTxes: } } +function setByPath (obj: Record, path: string[], value: any): Record { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]] = current[path[i]] ?? {} + } + current[path[path.length - 1]] = value + return obj +} + export async function updateField ( workspaceId: WorkspaceUuid, transactorUrl: string, @@ -85,9 +94,10 @@ export async function updateField ( console.error('Document not found') process.exit(1) } - let valueToPut: string | number = cmd.value + let valueToPut: string | number | boolean = cmd.value if (cmd.type === 'number') valueToPut = parseFloat(valueToPut) - ;(doc as any)[cmd.attribute] = valueToPut + if (cmd.type === 'boolean') valueToPut = cmd.value === 'true' + setByPath(doc, cmd.attribute.split('.'), valueToPut) await connection.upload(connection.getHierarchy().getDomain(doc?._class), [doc]) } finally { diff --git a/models/activity/src/migration.ts b/models/activity/src/migration.ts index dd01bd667e..434b8b3138 100644 --- a/models/activity/src/migration.ts +++ b/models/activity/src/migration.ts @@ -21,11 +21,13 @@ import { } from '@hcengineering/activity' import contact from '@hcengineering/contact' import core, { + type AccountUuid, type Class, type Doc, type Domain, groupByArray, MeasureMetricsContext, + type PersonId, type Ref, type Space } from '@hcengineering/core' @@ -39,7 +41,11 @@ import { tryMigrate } from '@hcengineering/model' import { htmlToMarkup } from '@hcengineering/text' -import { getSocialIdByOldAccount } from '@hcengineering/model-core' +import { + getAccountUuidByOldAccount, + getAccountUuidBySocialId, + getSocialIdByOldAccount +} from '@hcengineering/model-core' import { activityId, DOMAIN_ACTIVITY, DOMAIN_REACTION, DOMAIN_USER_MENTION } from './index' import activity from './plugin' @@ -240,31 +246,51 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise { - const ctx = new MeasureMetricsContext('migrateAccountsInDocUpdates migrateAccountsToSocialIds', {}) + const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {}) const socialIdByAccount = await getSocialIdByOldAccount(client) + const accountUuidBySocialId = new Map() ctx.info('processing activity doc updates ', {}) - function migrateField

( + function getUpdatedClass (attrKey: string): string { + return ['members', 'owners', 'user'].includes(attrKey) ? core.class.TypeAccountUuid : core.class.TypePersonId + } + + async function getUpdatedVal (oldVal: string, attrKey: string): Promise { + if (['members', 'owners', 'user'].includes(attrKey)) { + return (await getAccountUuidByOldAccount(client, oldVal, socialIdByAccount, accountUuidBySocialId)) ?? oldVal + } else { + return socialIdByAccount[oldVal] ?? oldVal + } + } + + async function migrateField

( au: DocAttributeUpdates, update: MigrateUpdate['attributeUpdates'], field: P - ): void { + ): Promise { const oldValue = au?.[field] if (oldValue == null) return let changed = false let newValue: any if (Array.isArray(oldValue)) { - newValue = (oldValue as string[]).map((a) => { - const newA = a != null ? socialIdByAccount[a] ?? a : a + newValue = [] + for (const a of oldValue as any[]) { + const newA = a != null ? await getUpdatedVal(a, au.attrKey) : a if (newA !== a) { changed = true } - return newA - }) + newValue.push(newA) + } } else { - newValue = socialIdByAccount[oldValue] ?? oldValue + newValue = await getUpdatedVal(oldValue, au.attrKey) if (newValue !== oldValue) { changed = true } @@ -301,12 +327,113 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise 0) { + await client.bulk(DOMAIN_ACTIVITY, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await iterator.close() + } + + ctx.info('finished processing activity doc updates ', {}) +} + +/** + * Migrates social ids to new accounts where needed. + * Should only be applied to staging where old accounts have already been migrated to social ids. + * REMOVE IT BEFORE MERGING TO PRODUCTION + * @param client + * @returns + */ +async function migrateSocialIdsInDocUpdates (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('activity migrateSocialIdsInDocUpdates', {}) + const accountUuidBySocialId = new Map() + ctx.info('processing activity doc updates ', {}) + + async function getUpdatedVal (oldVal: string): Promise { + return (await getAccountUuidBySocialId(client, oldVal as PersonId, accountUuidBySocialId)) ?? oldVal + } + + async function migrateField

( + au: DocAttributeUpdates, + update: MigrateUpdate['attributeUpdates'], + field: P + ): Promise { + const oldValue = au?.[field] + if (oldValue == null) return + + let changed = false + let newValue: any + if (Array.isArray(oldValue)) { + newValue = [] + for (const a of oldValue as any[]) { + const newA = a != null ? await getUpdatedVal(a) : a + if (newA !== a) { + changed = true + } + newValue.push(newA) + } + } else { + newValue = await getUpdatedVal(oldValue) + if (newValue !== oldValue) { + changed = true + } + } + + if (changed) { + if (update == null) throw new Error('update is null') + + update[field] = newValue + } + } + + const iterator = await client.traverse(DOMAIN_ACTIVITY, { + _class: activity.class.DocUpdateMessage, + action: 'update', + 'attributeUpdates.attrClass': 'core:class:TypePersonId', + 'attributeUpdates.attrKey': { $in: ['members', 'owners', 'user'] } + }) + + 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 dum = doc as DocUpdateMessage + if (dum.attributeUpdates == null) continue + const update: any = { attributeUpdates: { ...dum.attributeUpdates } } + + await migrateField(dum.attributeUpdates, update.attributeUpdates, 'added') + await migrateField(dum.attributeUpdates, update.attributeUpdates, 'prevValue') + await migrateField(dum.attributeUpdates, update.attributeUpdates, 'removed') + await migrateField(dum.attributeUpdates, update.attributeUpdates, 'set') + + update.attributeUpdates.attrClass = core.class.TypeAccountUuid operations.push({ filter: { _id: dum._id }, @@ -378,6 +505,11 @@ export const activityOperation: MigrateOperation = { { state: 'accounts-in-doc-updates-v2', func: migrateAccountsInDocUpdates + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'social-ids-in-doc-updates', + func: migrateSocialIdsInDocUpdates } ]) }, diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index 0d13ff1316..c127aa3975 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -15,14 +15,14 @@ import { chunterId, type ThreadMessage } from '@hcengineering/chunter' import core, { - type PersonId, TxOperations, type Class, type Doc, type Domain, type Ref, type Space, - DOMAIN_TX + DOMAIN_TX, + notEmpty } from '@hcengineering/core' import { tryMigrate, @@ -33,12 +33,7 @@ import { } from '@hcengineering/model' import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import notification from '@hcengineering/notification' -import { - getAllEmployeesPrimarySocialStrings, - pickPrimarySocialId, - getSocialStringsByEmployee, - includesAny -} from '@hcengineering/contact' +import contact, { getAllAccounts } from '@hcengineering/contact' import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification' import { type DocUpdateMessage } from '@hcengineering/activity' @@ -54,27 +49,24 @@ export async function createDocNotifyContexts ( objectClass: Ref>, objectSpace: Ref ): Promise { - const socialStringsByEmployee = getSocialStringsByEmployee(tx) - const allSocialStrings = Object.values(socialStringsByEmployee).flat() + const employees = await client.findAll(contact.mixin.Employee, { active: true }) + const accounts = employees.map((it) => it.personUuid).filter(notEmpty) const docNotifyContexts = await client.findAll(notification.class.DocNotifyContext, { - user: { $in: allSocialStrings }, + user: { $in: accounts }, objectId }) + const existingDNCUsers = new Set(docNotifyContexts.map((it) => it.user)) - for (const userSocialStrings of Object.values(socialStringsByEmployee)) { - const docNotifyContext = docNotifyContexts.find((it) => userSocialStrings.includes(it.user)) - - if (docNotifyContext === undefined) { - await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { - user: pickPrimarySocialId(userSocialStrings), - objectId, - objectClass, - objectSpace, - hidden: false, - isPinned: false - }) - } + for (const account of accounts.filter((it) => !existingDNCUsers.has(it))) { + await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { + user: account, + objectId, + objectClass, + objectSpace, + hidden: false, + isPinned: false + }) } } @@ -102,7 +94,7 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera topic: 'General Channel', private: false, archived: false, - members: await getAllEmployeesPrimarySocialStrings(tx), + members: await getAllAccounts(tx), autoJoin: true }, chunter.space.General @@ -114,12 +106,12 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera } async function joinEmployees (current: Space, tx: TxOperations): Promise { - const byEmployee = await getSocialStringsByEmployee(tx) - const newMembers: PersonId[] = [...current.members] + const allAccounts = await getAllAccounts(tx) + const newMembers = [...current.members] - for (const socialStrings of Object.values(byEmployee)) { - if (!includesAny(newMembers, socialStrings)) { - newMembers.push(pickPrimarySocialId(socialStrings)) + for (const account of allAccounts) { + if (!newMembers.includes(account)) { + newMembers.push(account) } } @@ -152,7 +144,7 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat topic: 'Random Talks', private: false, archived: false, - members: await getAllEmployeesPrimarySocialStrings(tx), + members: await getAllAccounts(tx), autoJoin: true }, chunter.space.Random diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 93ca9c8582..54da4ab36b 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -46,7 +46,8 @@ import { type Timestamp, type SocialIdType, type PersonUuid, - type PersonId + type PersonId, + type AccountUuid } from '@hcengineering/core' import { Collection as CollectionType, @@ -241,6 +242,8 @@ export class TEmployee extends TPerson implements Employee { @Prop(TypeString(), contact.string.Position) @Hidden() position?: string | null + + declare personUuid?: AccountUuid } @Model(contact.class.ContactsTab, core.class.Doc, DOMAIN_MODEL) @@ -782,6 +785,10 @@ export function createModel (builder: Builder): void { }) builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.ArrayEditor, { + inlineEditor: contact.component.PersonIdArrayEditor + }) + + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.ArrayEditor, { inlineEditor: contact.component.AccountArrayEditor }) diff --git a/models/contact/src/migration.ts b/models/contact/src/migration.ts index 5f8d23cd64..dd801ba68c 100644 --- a/models/contact/src/migration.ts +++ b/models/contact/src/migration.ts @@ -1,6 +1,6 @@ // -import { AvatarType, type Contact, type SocialIdentity } from '@hcengineering/contact' +import { AvatarType, type Person, type Contact, type SocialIdentity } from '@hcengineering/contact' import { type AccountRole, buildSocialIdString, @@ -95,6 +95,62 @@ async function getOldPersonAccounts ( return getAccountsFromTxes(accountsTxes) } +async function fillAccountUuids (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('contact fillAccountUuids', {}) + ctx.info('filling account uuids...') + const iterator = await client.traverse(DOMAIN_CONTACT, { _class: contact.class.Person }) + + try { + let operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + while (true) { + const persons = await iterator.next(200) + if (persons === null || persons.length === 0) { + break + } + + for (const person of persons) { + const employee = client.hierarchy.as(person, contact.mixin.Employee) + if (employee === undefined || employee.personUuid !== undefined) { + continue + } + + const socialIdentity = ( + await client.find(DOMAIN_CHANNEL, { + _class: contact.class.SocialIdentity, + attachedTo: person._id + }) + )[0] + if (socialIdentity == null) continue + + const accountUuid = await client.accountClient.findPerson(socialIdentity.key) + if (accountUuid == null) { + continue + } + + operations.push({ + filter: { _id: person._id }, + update: { + personUuid: accountUuid + } + }) + } + + if (operations.length > 50) { + await client.bulk(DOMAIN_CONTACT, operations) + operations = [] + } + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_CONTACT, operations) + operations = [] + } + } finally { + await iterator.close() + } +} + async function assignWorkspaceRoles (client: MigrationClient): Promise { const ctx = new MeasureMetricsContext('contact assignWorkspaceRoles', {}) ctx.info('assigning workspace roles...') @@ -302,6 +358,10 @@ export const contactOperation: MigrateOperation = { { state: 'assign-workspace-roles', func: assignWorkspaceRoles + }, + { + state: 'fill-account-uuids', + func: fillAccountUuids } ]) }, diff --git a/models/controlled-documents/src/types.ts b/models/controlled-documents/src/types.ts index c450cf625d..1c0747e36a 100644 --- a/models/controlled-documents/src/types.ts +++ b/models/controlled-documents/src/types.ts @@ -57,7 +57,7 @@ import { type TypedSpace, type RolesAssignment, type Rank, - type PersonId + type AccountUuid } from '@hcengineering/core' import { ArrOf, @@ -472,7 +472,7 @@ export class TDocumentApprovalRequest extends TDocumentRequest implements Docume @Mixin(documents.mixin.DocumentSpaceTypeData, documents.class.DocumentSpace) @UX(getEmbeddedLabel('Default Documents'), documents.icon.Document) export class TDocumentSpaceTypeData extends TDocumentSpace implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } /** diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 1abcddced5..4b63730959 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -265,6 +265,10 @@ export class TTypeMarkup extends TType {} @Model(core.class.TypePersonId, core.class.Type) export class TTypePersonId extends TType {} +@UX(core.string.AccountId) +@Model(core.class.TypeAccountUuid, core.class.Type) +export class TTypeAccountUuid extends TType {} + @UX(core.string.Ref) @Model(core.class.RefTo, core.class.Type) export class TRefTo extends TType implements RefTo { diff --git a/models/core/src/index.ts b/models/core/src/index.ts index ee94511df3..270709024e 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -63,6 +63,7 @@ import { TTypeIntlString, TTypeMarkup, TTypePersonId, + TTypeAccountUuid, TTypeNumber, TTypeRank, TTypeRecord, @@ -81,7 +82,16 @@ import { TTx, TTxApplyIf, TTxCreateDoc, TTxCUD, TTxMixin, TTxRemoveDoc, TTxUpdat export { coreId, DOMAIN_SPACE } from '@hcengineering/core' export * from './core' -export { coreOperation, getSocialIdByOldAccount, getAccountsFromTxes, getSocialKeyByOldEmail } from './migration' +export { + coreOperation, + getSocialIdByOldAccount, + getAccountsFromTxes, + getSocialKeyByOldEmail, + getAccountUuidBySocialId, + getUniqueAccounts, + getAccountUuidByOldAccount, + getUniqueAccountsFromOldAccounts +} from './migration' export * from './security' export * from './status' export * from './tx' @@ -115,6 +125,7 @@ export function createModel (builder: Builder): void { TEnumOf, TTypeMarkup, TTypePersonId, + TTypeAccountUuid, TTypeCollaborativeDoc, TArrOf, TRefTo, diff --git a/models/core/src/migration.ts b/models/core/src/migration.ts index 5414ae5eb7..419bfade16 100644 --- a/models/core/src/migration.ts +++ b/models/core/src/migration.ts @@ -46,7 +46,10 @@ import core, { toIdMap, type TypedSpace, TxProcessor, - type SocialKey + type SocialKey, + type AccountUuid, + systemAccountUuid, + configUserAccountUuid } from '@hcengineering/core' import { createDefaultSpace, @@ -349,8 +352,14 @@ export function getSocialKeyByOldEmail (rawEmail: string): SocialKey { } } -async function migrateAccountsToSocialIds (client: MigrationClient): Promise { - const ctx = new MeasureMetricsContext('core migrateAccountsToSocialIds', {}) +/** + * Migrates old accounts to new accounts/social ids. + * Should be applied to prodcution directly without applying migrateSpaceMembersToAccountUuids + * @param client + * @returns + */ +async function migrateAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('core migrateAccounts', {}) const hierarchy = client.hierarchy const socialIdByAccount = await getSocialIdByOldAccount(client) @@ -427,6 +436,8 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise() + ctx.info('processing spaces members, owners and roles assignment', {}) let processedSpaces = 0 const spacesIterator = await client.traverse(DOMAIN_SPACE, {}) @@ -444,11 +455,21 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise socialIdByAccount[m] ?? m) - const newOwners = space.owners?.map((m) => socialIdByAccount[m] ?? m) + const newMembers = await getUniqueAccountsFromOldAccounts( + client, + space.members, + socialIdByAccount, + accountUuidBySocialId + ) + const newOwners = await getUniqueAccountsFromOldAccounts( + client, + space.owners ?? [], + socialIdByAccount, + accountUuidBySocialId + ) const update: MigrateUpdate = { - members: newMembers, - owners: newOwners + members: newMembers as any, + owners: newOwners as any } const type = spaceTypesById.get((space as TypedSpace).type) @@ -461,7 +482,12 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise 0) { - const newAssignees = oldAssignees.map((a) => socialIdByAccount[a]) + const newAssignees = await getUniqueAccountsFromOldAccounts( + client, + oldAssignees, + socialIdByAccount, + accountUuidBySocialId + ) update[`${type.targetClass}`] = { [role._id]: newAssignees @@ -495,7 +521,235 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise socialIdByAccount[m] ?? m) + const newMembers = await getUniqueAccountsFromOldAccounts( + client, + spaceType.members, + socialIdByAccount, + accountUuidBySocialId + ) + const tx: TxUpdateDoc = { + _id: generateId(), + _class: core.class.TxUpdateDoc, + space: core.space.Tx, + objectId: spaceType._id, + objectClass: spaceType._class, + objectSpace: spaceType.space, + operations: { + members: newMembers as any + }, + modifiedOn: Date.now(), + createdBy: core.account.ConfigUser, + createdOn: Date.now(), + modifiedBy: core.account.ConfigUser + } + + await client.create(DOMAIN_MODEL_TX, tx) + updatedSpaceTypes++ + } + ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes }) +} + +export async function getAccountUuidBySocialId ( + client: MigrationClient, + socialId: PersonId, + accountUuidBySocialId: Map +): Promise { + if (socialId === core.account.System) { + return systemAccountUuid + } + + if (socialId === core.account.ConfigUser) { + return configUserAccountUuid + } + + const cached = accountUuidBySocialId.has(socialId) + + if (!cached) { + const personUuid = await client.accountClient.findPerson(socialId) + if (personUuid === undefined) { + console.log('Could not find person for', socialId) + } + + accountUuidBySocialId.set(socialId, (personUuid as AccountUuid | undefined) ?? null) + } + + return accountUuidBySocialId.get(socialId) ?? null +} + +export async function getUniqueAccounts ( + client: MigrationClient, + persons: PersonId[], + accountUuidBySocialId = new Map() +): Promise { + const accounts = new Set() + for (const person of persons) { + let newAccount = await getAccountUuidBySocialId(client, person as unknown as PersonId, accountUuidBySocialId) + + if (newAccount == null && isUuid(person)) { + newAccount = person as unknown as AccountUuid + } + if (newAccount != null) { + accounts.add(newAccount) + } + } + + return Array.from(accounts) +} + +export async function getAccountUuidByOldAccount ( + client: MigrationClient, + oldAccount: string, + socialIdByOldAccount: Record, + accountUuidByOldAccount: Map +): Promise { + if (oldAccount === core.account.System) { + return systemAccountUuid + } + + if (oldAccount === core.account.ConfigUser) { + return configUserAccountUuid + } + + const cached = accountUuidByOldAccount.has(oldAccount) + + if (!cached) { + const socialId = socialIdByOldAccount[oldAccount] + if (socialId == null) { + accountUuidByOldAccount.set(oldAccount, null) + return null + } + + const personUuid = await client.accountClient.findPerson(socialId) + + accountUuidByOldAccount.set(oldAccount, (personUuid as AccountUuid | undefined) ?? null) + } + + return accountUuidByOldAccount.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) +} + +export async function getUniqueAccountsFromOldAccounts ( + client: MigrationClient, + oldAccounts: string[], + socialIdByOldAccount: Record, + accountUuidByOldAccount: Map = new Map() +): Promise { + const accounts = new Set() + for (const oldAcc of oldAccounts) { + let newAccount = await getAccountUuidByOldAccount(client, oldAcc, socialIdByOldAccount, accountUuidByOldAccount) + + if (newAccount == null && isUuid(oldAcc)) { + newAccount = oldAcc as unknown as AccountUuid + } + + if (newAccount != null) { + accounts.add(newAccount) + } + } + + return Array.from(accounts) +} + +/** + * Migrates social ids to new accounts where needed. + * Should only be applied to staging where old accounts have already been migrated to social ids. + * REMOVE IT BEFORE MERGING TO PRODUCTION + * @param client + * @returns + */ +async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('core migrateSpaceMembersToAccountUuids', {}) + const hierarchy = client.hierarchy + const accountUuidBySocialId = new Map() + + const spaceTypes = client.model.findAllSync(core.class.SpaceType, {}) + const spaceTypesById = toIdMap(spaceTypes) + const roles = client.model.findAllSync(core.class.Role, {}) + const rolesBySpaceType = new Map, Role[]>() + for (const role of roles) { + const spaceType = role.attachedTo + if (spaceType === undefined) continue + if (rolesBySpaceType.has(spaceType)) { + rolesBySpaceType.get(spaceType)?.push(role) + } else { + rolesBySpaceType.set(spaceType, [role]) + } + } + + ctx.info('processing spaces members, owners and roles assignment', {}) + let processedSpaces = 0 + const spacesIterator = await client.traverse(DOMAIN_SPACE, {}) + + try { + while (true) { + const spaces = await spacesIterator.next(200) + if (spaces === null || spaces.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + for (const s of spaces) { + 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) + } + + const type = spaceTypesById.get((space as TypedSpace).type) + + if (type !== undefined) { + const mixin = hierarchy.as(space, type.targetClass) + if (mixin !== undefined) { + const roles = rolesBySpaceType.get(type._id) + + 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) + + update[`${type.targetClass}`] = { + [role._id]: newAssignees + } + } + } + } + } + + operations.push({ + filter: { _id: space._id }, + update + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_SPACE, operations) + } + + processedSpaces += spaces.length + ctx.info('...spaces processed', { count: processedSpaces }) + } + + ctx.info('finished processing spaces members, owners and roles assignment', { processedSpaces }) + } finally { + await spacesIterator.close() + } + + ctx.info('processing space types members', {}) + let updatedSpaceTypes = 0 + for (const spaceType of spaceTypes) { + if (spaceType.members === undefined || spaceType.members.length === 0) continue + + const newMembers = await getUniqueAccounts( + client, + spaceType.members as unknown as PersonId[], + accountUuidBySocialId + ) const tx: TxUpdateDoc = { _id: generateId(), _class: core.class.TxUpdateDoc, @@ -721,7 +975,12 @@ export const coreOperation: MigrateOperation = { }, { state: 'accounts-to-social-ids', - func: migrateAccountsToSocialIds + func: migrateAccounts + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'space-members-to-account-uuids', + func: migrateSpaceMembersToAccountUuids } ]) }, diff --git a/models/core/src/security.ts b/models/core/src/security.ts index c1fd68e16e..a7ad3ea6dc 100644 --- a/models/core/src/security.ts +++ b/models/core/src/security.ts @@ -17,8 +17,6 @@ import { DOMAIN_MODEL, DOMAIN_SPACE, IndexKind, - type PersonId, - type Arr, type Class, type CollectionSize, type Permission, @@ -28,7 +26,8 @@ import { type Space, type SpaceType, type SpaceTypeDescriptor, - type TypedSpace + type TypedSpace, + type AccountUuid } from '@hcengineering/core' import { ArrOf, @@ -39,9 +38,9 @@ import { Model, Prop, TypeBoolean, + TypeAccountUuid, TypeRef, TypeString, - TypePersonId, UX } from '@hcengineering/model' import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform' @@ -68,12 +67,12 @@ export class TSpace extends TDoc implements Space { @Index(IndexKind.Indexed) archived!: boolean - @Prop(ArrOf(TypePersonId()), core.string.Members) + @Prop(ArrOf(TypeAccountUuid()), core.string.Members) @Index(IndexKind.Indexed) - members!: Arr + members!: AccountUuid[] - @Prop(ArrOf(TypePersonId()), core.string.Owners) - owners?: PersonId[] + @Prop(ArrOf(TypeAccountUuid()), core.string.Owners) + owners?: AccountUuid[] @Prop(TypeBoolean(), core.string.AutoJoin) autoJoin?: boolean @@ -119,8 +118,8 @@ export class TSpaceType extends TDoc implements SpaceType { @Prop(Collection(core.class.Role), core.string.Roles) roles!: CollectionSize - @Prop(ArrOf(TypePersonId()), core.string.Members) - members!: Arr + @Prop(ArrOf(TypeAccountUuid()), core.string.Members) + members!: AccountUuid[] @Prop(TypeBoolean(), core.string.AutoJoin) autoJoin?: boolean @@ -162,5 +161,5 @@ export class TPermission extends TDoc implements Permission { @Mixin(core.mixin.SpacesTypeData, core.class.Space) @UX(getEmbeddedLabel("All spaces' type")) // TODO: add icon? export class TSpacesTypeData extends TSpace implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } diff --git a/models/core/src/transient.ts b/models/core/src/transient.ts index 69639dab27..32f346cc8c 100644 --- a/models/core/src/transient.ts +++ b/models/core/src/transient.ts @@ -13,13 +13,13 @@ // limitations under the License. // -import { DOMAIN_TRANSIENT, type PersonUuid, type UserStatus } from '@hcengineering/core' +import { type AccountUuid, DOMAIN_TRANSIENT, type UserStatus } from '@hcengineering/core' import { Model } from '@hcengineering/model' import core from './component' import { TDoc } from './core' @Model(core.class.UserStatus, core.class.Doc, DOMAIN_TRANSIENT) export class TUserStatus extends TDoc implements UserStatus { - user!: PersonUuid + user!: AccountUuid online!: boolean } diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 0adc234d81..1ea4a4875a 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -14,8 +14,17 @@ // import activity from '@hcengineering/activity' -import type { CollectionSize, MarkupBlobRef, Domain, Rank, Ref, Role, RolesAssignment } from '@hcengineering/core' -import { PersonId, AccountRole, IndexKind } from '@hcengineering/core' +import type { + CollectionSize, + MarkupBlobRef, + Domain, + Rank, + Ref, + Role, + RolesAssignment, + PersonId +} from '@hcengineering/core' +import { AccountUuid, AccountRole, IndexKind } from '@hcengineering/core' import { type Document, type DocumentSnapshot, @@ -151,7 +160,7 @@ export class TTeamspace extends TTypedSpace implements Teamspace {} @Mixin(document.mixin.DefaultTeamspaceTypeData, document.class.Teamspace) @UX(getEmbeddedLabel('Default teamspace type'), document.icon.Document) export class TDefaultTeamspaceTypeData extends TTeamspace implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } function defineTeamspace (builder: Builder): void { diff --git a/models/drive/src/index.ts b/models/drive/src/index.ts index 5c87783d68..02ed40f0d4 100644 --- a/models/drive/src/index.ts +++ b/models/drive/src/index.ts @@ -24,10 +24,10 @@ import core, { type Ref, type Role, type RolesAssignment, - PersonId, AccountRole, IndexKind, - SortingOrder + SortingOrder, + type AccountUuid } from '@hcengineering/core' import { type Drive, @@ -81,7 +81,7 @@ export class TDrive extends TTypedSpace implements Drive {} @Mixin(drive.mixin.DefaultDriveTypeData, drive.class.Drive) @UX(getEmbeddedLabel('Default drive type')) export class TDefaultDriveTypeData extends TDrive implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } @Model(drive.class.Resource, core.class.Doc, DOMAIN_DRIVE) diff --git a/models/lead/src/types.ts b/models/lead/src/types.ts index 92cc09a750..2709c70835 100644 --- a/models/lead/src/types.ts +++ b/models/lead/src/types.ts @@ -15,14 +15,14 @@ import type { Employee } from '@hcengineering/contact' import { - PersonId, IndexKind, type MarkupBlobRef, type Role, type RolesAssignment, type Ref, type Status, - type Timestamp + type Timestamp, + type AccountUuid } from '@hcengineering/core' import { type Customer, type Funnel, type Lead } from '@hcengineering/lead' import { @@ -103,7 +103,7 @@ export class TCustomer extends TContact implements Customer { @Mixin(lead.mixin.DefaultFunnelTypeData, lead.class.Funnel) @UX(getEmbeddedLabel('Default funnel'), lead.icon.Funnel) export class TDefaultFunnelTypeData extends TFunnel implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } @Mixin(lead.mixin.LeadTypeData, lead.class.Lead) diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index f9eb7de0e8..c76d58e71b 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -34,7 +34,8 @@ import { type Timestamp, type Tx, type TxCUD, - DOMAIN_TRANSIENT + DOMAIN_TRANSIENT, + type AccountUuid } from '@hcengineering/core' import { ArrOf, @@ -47,9 +48,9 @@ import { TypeIntlString, TypeMarkup, TypeRef, - TypePersonId, UX, - type Builder + type Builder, + TypeAccountUuid } from '@hcengineering/model' import core, { TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' @@ -100,7 +101,7 @@ export class TBrowserNotification extends TDoc implements BrowserNotification { title!: string body!: string onClickLocation?: Location | undefined - user!: PersonId + user!: AccountUuid messageId?: Ref messageClass?: Ref> objectId!: Ref @@ -109,7 +110,7 @@ export class TBrowserNotification extends TDoc implements BrowserNotification { @Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY) export class TPushSubscription extends TDoc implements PushSubscription { - user!: PersonId + user!: AccountUuid endpoint!: string keys!: PushSubscriptionKeys } @@ -170,9 +171,9 @@ export class TClassCollaborators extends TClass { @Mixin(notification.mixin.Collaborators, core.class.Doc) @UX(notification.string.Collaborators) export class TCollaborators extends TDoc { - @Prop(ArrOf(TypePersonId()), notification.string.Collaborators) + @Prop(ArrOf(TypeAccountUuid()), notification.string.Collaborators) @Index(IndexKind.Indexed) - collaborators!: PersonId[] + collaborators!: AccountUuid[] } @Mixin(notification.mixin.NotificationObjectPresenter, core.class.Class) @@ -192,9 +193,9 @@ export class TNotificationContextPresenter extends TClass implements Notificatio @Model(notification.class.DocNotifyContext, core.class.Doc, DOMAIN_DOC_NOTIFY) export class TDocNotifyContext extends TDoc implements DocNotifyContext { - @Prop(TypePersonId(), core.string.Account) + @Prop(TypeAccountUuid(), core.string.Account) @Index(IndexKind.Indexed) - user!: PersonId + user!: AccountUuid @Prop(TypeRef(core.class.Doc), core.string.Object) @Index(IndexKind.Indexed) @@ -229,9 +230,9 @@ export class TInboxNotification extends TDoc implements InboxNotification { @Index(IndexKind.Indexed) docNotifyContext!: Ref - @Prop(TypePersonId(), core.string.Account) + @Prop(TypeAccountUuid(), core.string.Account) @Index(IndexKind.Indexed) - user!: PersonId + user!: AccountUuid @Prop(TypeBoolean(), core.string.Boolean) // @Index(IndexKind.Indexed) diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 9633400329..4730bd1c84 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -18,11 +18,13 @@ import contact, { type PersonSpace } from '@hcengineering/contact' import core, { DOMAIN_TX, MeasureMetricsContext, + type PersonId, type Class, type Doc, type DocumentQuery, type Ref, - type Space + type Space, + type AccountUuid } from '@hcengineering/core' import { migrateSpace, @@ -41,7 +43,14 @@ import notification, { } from '@hcengineering/notification' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' -import { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core' +import { + DOMAIN_SPACE, + getSocialIdByOldAccount, + getUniqueAccounts, + getAccountUuidBySocialId, + getAccountUuidByOldAccount, + getUniqueAccountsFromOldAccounts +} from '@hcengineering/model-core' import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY } from './index' export async function removeNotifications ( @@ -231,10 +240,17 @@ export async function migrateDuplicateContexts (client: MigrationClient): Promis } } -async function migrateAccountsToSocialIds (client: MigrationClient): Promise { - const ctx = new MeasureMetricsContext('notification migrateAccountsToSocialIds', {}) +/** + * Migrates old accounts to new accounts/social ids. + * Should be applied to prodcution directly without applying migrateSocialIdsToAccountUuids + * @param client + * @returns + */ +async function migrateAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('notification migrateAccounts', {}) const hierarchy = client.hierarchy const socialIdByAccount = await getSocialIdByOldAccount(client) + const accountUuidByOldAccount = new Map() ctx.info('processing collaborators ', {}) for (const domain of client.hierarchy.domains()) { @@ -257,7 +273,12 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise socialIdByAccount[c] ?? c) + const newCollaborators = await getUniqueAccountsFromOldAccounts( + client, + oldCollaborators, + socialIdByAccount, + accountUuidByOldAccount + ) operations.push({ filter: { _id: doc._id }, @@ -306,17 +327,14 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise(DOMAIN_NOTIFICATION, 'senderId', { - _class: notification.class.BrowserNotification - }) - groupByUser.forEach((_, accId) => { - const socialId = socialIdByAccount[accId] - if (socialId == null || accId === socialId) return + for (const oldAccId of groupByUser.keys()) { + const newAccId = await getAccountUuidByOldAccount(client, oldAccId, socialIdByAccount, accountUuidByOldAccount) + if (newAccId == null || oldAccId === newAccId) return operations.push({ filter: { - user: accId, + user: oldAccId, _class: { $in: [ notification.class.DocNotifyContext, @@ -329,9 +347,13 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise(DOMAIN_NOTIFICATION, 'senderId', { + _class: notification.class.BrowserNotification }) groupBySenderId.forEach((_, accId) => { @@ -385,10 +407,183 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise 0) { + await client.bulk(DOMAIN_DOC_NOTIFY, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await dncIterator.close() + } + ctx.info('finished processing doc notify contexts ', {}) +} + +/** + * Migrates social ids to new accounts where needed. + * Should only be applied to staging where old accounts have already been migrated to social ids. + * REMOVE IT BEFORE MERGING TO PRODUCTION + * @param client + * @returns + */ +async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('notification migrateSocialIdsToAccountUuids', {}) + const hierarchy = client.hierarchy + const accountUuidBySocialId = new Map() + + ctx.info('processing collaborators ', {}) + for (const domain of client.hierarchy.domains()) { + ctx.info('processing domain ', { domain }) + let processed = 0 + const iterator = await client.traverse(domain, {}) + + try { + 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 mixin = hierarchy.as(doc, notification.mixin.Collaborators) + const oldCollaborators = mixin.collaborators as unknown as PersonId[] + + if (oldCollaborators === undefined || oldCollaborators.length === 0) continue + + const newCollaborators = await getUniqueAccounts(client, oldCollaborators, accountUuidBySocialId) + + operations.push({ + filter: { _id: doc._id }, + update: { + [`${notification.mixin.Collaborators}`]: { + collaborators: newCollaborators + } + } + }) + } + + if (operations.length > 0) { + await client.bulk(domain, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + + ctx.info('finished processing domain ', { domain, processed }) + } finally { + await iterator.close() + } + } + ctx.info('finished processing collaborators ', {}) + + ctx.info('processing notifications fields ', {}) + 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 groupByUser = await client.groupBy(DOMAIN_NOTIFICATION, 'user', { + _class: { + $in: [ + notification.class.DocNotifyContext, + notification.class.BrowserNotification, + notification.class.PushSubscription, + notification.class.InboxNotification, + notification.class.ActivityInboxNotification, + notification.class.CommonInboxNotification + ] + } + }) + + for (const socialId of groupByUser.keys()) { + const account = await getAccountUuidBySocialId(client, socialId, accountUuidBySocialId) + + if (account == null || (account as unknown as PersonId) === socialId) continue + + operations.push({ + filter: { + user: socialId, + _class: { + $in: [ + notification.class.DocNotifyContext, + notification.class.BrowserNotification, + notification.class.PushSubscription, + notification.class.InboxNotification, + notification.class.ActivityInboxNotification, + notification.class.CommonInboxNotification + ] + } + }, + update: { + user: account + } + }) + } + + 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 user social ids to migrate') + } + + ctx.info('finished processing notifications fields ', {}) + + ctx.info('processing doc notify contexts ', {}) + // If there's more than one DNC for a user it's not a problem. + // We'll migrate all of them but only one will be used going further. + // Also, it's only possible on front so we don't need to worry about it. + const dncIterator = await client.traverse(DOMAIN_DOC_NOTIFY, { + _class: notification.class.DocNotifyContext + }) + try { + let processed = 0 + while (true) { + const docs = await dncIterator.next(200) + if (docs === null || docs.length === 0) { + break + } + + const operations: { + filter: MigrationDocumentQuery + update: MigrateUpdate + }[] = [] + + for (const doc of docs) { + const oldUser: any = doc.user + const newUser = await getAccountUuidBySocialId(client, oldUser, accountUuidBySocialId) + + if (newUser != null && newUser !== oldUser) { operations.push({ filter: { _id: doc._id }, update: { @@ -663,7 +858,12 @@ export const notificationOperation: MigrateOperation = { // }, { state: 'accounts-to-social-ids', - func: migrateAccountsToSocialIds + func: migrateAccounts + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'migrate-social-ids-to-account-uuids', + func: migrateSocialIdsToAccountUuids } ]) }, diff --git a/models/products/src/index.ts b/models/products/src/index.ts index e14c533f35..46c7bb19fd 100644 --- a/models/products/src/index.ts +++ b/models/products/src/index.ts @@ -22,8 +22,8 @@ import { type Attachment } from '@hcengineering/attachment' import contact from '@hcengineering/contact' import chunter from '@hcengineering/chunter' import { getRoleAttributeProps } from '@hcengineering/setting' -import type { Type, Ref, CollectionSize, Markup, Arr, RolesAssignment, Permission, Role } from '@hcengineering/core' -import { IndexKind, PersonId } from '@hcengineering/core' +import type { Type, Ref, CollectionSize, Markup, RolesAssignment, Permission, Role } from '@hcengineering/core' +import { IndexKind, AccountUuid } from '@hcengineering/core' import { type Builder, Model, @@ -39,8 +39,8 @@ import { ArrOf, TypeAny, ReadOnly, - TypePersonId, - Mixin + Mixin, + TypeAccountUuid } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import core, { TType } from '@hcengineering/model-core' @@ -79,8 +79,8 @@ export class TTypeProductVersionState extends TType {} @Model(products.class.Product, documents.class.ExternalSpace) @UX(products.string.Product, products.icon.Product, 'Product', 'name', undefined, products.string.Products) export class TProduct extends TExternalSpace implements Product { - @Prop(ArrOf(TypePersonId()), core.string.Members) - declare members: Arr + @Prop(ArrOf(TypeAccountUuid()), core.string.Members) + declare members: AccountUuid[] @Prop(TypeMarkup(), products.string.Description) @Index(IndexKind.FullText) @@ -146,7 +146,7 @@ export class TProductVersion extends TProject implements ProductVersion { @Mixin(products.mixin.ProductTypeData, products.class.Product) @UX(getEmbeddedLabel('Default Products'), products.icon.ProductVersion) export class TProductTypeData extends TProduct implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } function defineProduct (builder: Builder): void { diff --git a/models/recruit/src/types.ts b/models/recruit/src/types.ts index 850d634780..dd41c9e01a 100644 --- a/models/recruit/src/types.ts +++ b/models/recruit/src/types.ts @@ -15,7 +15,6 @@ import type { Employee, Organization } from '@hcengineering/contact' import { - PersonId, IndexKind, type Collection, type MarkupBlobRef, @@ -25,7 +24,8 @@ import { type Role, type RolesAssignment, type Status, - type Timestamp + type Timestamp, + type AccountUuid } from '@hcengineering/core' import { Collection as TypeCollection, @@ -251,7 +251,7 @@ export class TOpinion extends TAttachedDoc implements Opinion { @Mixin(recruit.mixin.DefaultVacancyTypeData, recruit.class.Vacancy) @UX(getEmbeddedLabel('Default vacancy'), recruit.icon.Vacancy) export class TDefaultVacancyTypeData extends TVacancy implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } @Mixin(recruit.mixin.ApplicantTypeData, recruit.class.Applicant) diff --git a/models/server-contact/src/index.ts b/models/server-contact/src/index.ts index 381587053d..a3fe2c204e 100644 --- a/models/server-contact/src/index.ts +++ b/models/server-contact/src/index.ts @@ -71,14 +71,6 @@ export function createModel (builder: Builder): void { } }) - builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverContact.trigger.OnSocialIdentityCreate, - txMatch: { - _class: core.class.TxCreateDoc, - objectClass: contact.class.SocialIdentity - } - }) - builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverContact.trigger.OnEmployeeCreate, txMatch: { diff --git a/models/server-controlled-documents/src/index.ts b/models/server-controlled-documents/src/index.ts index 756e923188..51fb2672a6 100644 --- a/models/server-controlled-documents/src/index.ts +++ b/models/server-controlled-documents/src/index.ts @@ -15,10 +15,12 @@ export { serverDocumentsId } from '@hcengineering/server-controlled-documents/sr export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverDocuments.trigger.OnSocialIdentityCreate, + trigger: serverDocuments.trigger.OnEmployeeCreate, txMatch: { - _class: core.class.TxCreateDoc, - objectClass: contact.class.SocialIdentity + objectClass: contact.class.Person, + _class: core.class.TxMixin, + mixin: contact.mixin.Employee, + 'attributes.active': true } }) diff --git a/models/server-lead/src/index.ts b/models/server-lead/src/index.ts index 298142fc76..a7a30d6c22 100644 --- a/models/server-lead/src/index.ts +++ b/models/server-lead/src/index.ts @@ -44,10 +44,12 @@ export function createModel (builder: Builder): void { ) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverLead.trigger.OnSocialIdentityCreate, + trigger: serverLead.trigger.OnEmployeeCreate, txMatch: { - _class: core.class.TxCreateDoc, - objectClass: contact.class.SocialIdentity + objectClass: contact.class.Person, + _class: core.class.TxMixin, + mixin: contact.mixin.Employee, + 'attributes.active': true } }) } diff --git a/models/server-tracker/src/index.ts b/models/server-tracker/src/index.ts index 7758af3fdd..4db8db71ac 100644 --- a/models/server-tracker/src/index.ts +++ b/models/server-tracker/src/index.ts @@ -52,10 +52,12 @@ export function createModel (builder: Builder): void { }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverTracker.trigger.OnSocialIdentityCreate, + trigger: serverTracker.trigger.OnEmployeeCreate, txMatch: { - _class: core.class.TxCreateDoc, - objectClass: contact.class.SocialIdentity + objectClass: contact.class.Person, + _class: core.class.TxMixin, + mixin: contact.mixin.Employee, + 'attributes.active': true } }) diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index a28457be42..e1cc0e71b0 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -15,7 +15,7 @@ import activity from '@hcengineering/activity' import contact from '@hcengineering/contact' -import { AccountRole, DOMAIN_MODEL, type PersonId, type Blob, type Domain, type Ref } from '@hcengineering/core' +import { AccountRole, DOMAIN_MODEL, type Blob, type Domain, type Ref, type AccountUuid } from '@hcengineering/core' import { Mixin, Model, type Builder, UX } from '@hcengineering/model' import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core' import view, { createAction } from '@hcengineering/model-view' @@ -53,7 +53,7 @@ export class TIntegration extends TDoc implements Integration { type!: Ref disabled!: boolean value!: string - shared!: PersonId[] + shared!: AccountUuid[] error?: IntlString | null } @Model(setting.class.SettingsCategory, core.class.Doc, DOMAIN_MODEL) diff --git a/models/setting/src/migration.ts b/models/setting/src/migration.ts index da8ac6d128..75bea82d56 100644 --- a/models/setting/src/migration.ts +++ b/models/setting/src/migration.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import core, { MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core' +import core, { type AccountUuid, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core' import { migrateSpace, type MigrateUpdate, @@ -24,13 +24,20 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import setting, { type Integration, settingId } from '@hcengineering/setting' -import { getSocialIdByOldAccount } from '@hcengineering/model-core' +import { getSocialIdByOldAccount, getUniqueAccounts, getUniqueAccountsFromOldAccounts } from '@hcengineering/model-core' import { DOMAIN_SETTING } from '.' -async function migrateAccountsToSocialIds (client: MigrationClient): Promise { - const ctx = new MeasureMetricsContext('setting migrateAccountsToSocialIds', {}) +/** + * Migrates old accounts to new accounts + * Should be applied to prodcution directly without applying migrateSocialIdsToAccountUuids + * @param client + * @returns + */ +async function migrateAccounts (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('setting migrateAccounts', {}) const socialIdByAccount = await getSocialIdByOldAccount(client) + const accountUuidByOldAccount = new Map() ctx.info('processing setting integration shared ', {}) const iterator = await client.traverse(DOMAIN_SETTING, { _class: setting.class.Integration }) @@ -50,7 +57,68 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise socialIdByAccount[s] ?? s) + const newShared = await getUniqueAccountsFromOldAccounts( + client, + integration.shared, + socialIdByAccount, + accountUuidByOldAccount + ) + + operations.push({ + filter: { _id: integration._id }, + update: { + shared: newShared + } + }) + } + + if (operations.length > 0) { + await client.bulk(DOMAIN_SETTING, operations) + } + + processed += docs.length + ctx.info('...processed', { count: processed }) + } + } finally { + await iterator.close() + } + ctx.info('finished processing setting integration shared ', {}) +} + +/** + * Migrates social ids to new accounts where needed. + * Should only be applied to staging where old accounts have already been migrated to social ids. + * REMOVE IT BEFORE MERGING TO PRODUCTION + * @param client + * @returns + */ +async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise { + const ctx = new MeasureMetricsContext('setting migrateAccounts', {}) + const accountUuidBySocialId = new Map() + + ctx.info('processing setting integration shared ', {}) + const iterator = await client.traverse(DOMAIN_SETTING, { _class: setting.class.Integration }) + + 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 integration = doc as Integration + + if (integration.shared === undefined || integration.shared.length === 0) continue + + const newShared = await getUniqueAccounts( + client, + integration.shared as unknown as PersonId[], + accountUuidBySocialId + ) operations.push({ filter: { _id: integration._id }, @@ -84,7 +152,12 @@ export const settingOperation: MigrateOperation = { }, { state: 'accounts-to-social-ids', - func: migrateAccountsToSocialIds + func: migrateAccounts + }, + // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION + { + state: 'migrate-social-ids-to-account-uuids', + func: migrateSocialIdsToAccountUuids } ]) }, diff --git a/models/test-management/src/types.ts b/models/test-management/src/types.ts index 8742ecd660..084f2d1617 100644 --- a/models/test-management/src/types.ts +++ b/models/test-management/src/types.ts @@ -43,7 +43,7 @@ import { type CollectionSize, type MarkupBlobRef, type Class, - type PersonId + type AccountUuid } from '@hcengineering/core' import { Mixin, @@ -107,7 +107,7 @@ export class TTestProject extends TTypedSpace implements TestProject { @Mixin(testManagement.mixin.DefaultProjectTypeData, testManagement.class.TestProject) @UX(getEmbeddedLabel('Default project'), testManagement.icon.TestProject) export class TDefaultProjectTypeData extends TTestProject implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } /** diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts index e90298eef6..cb6dbb7786 100644 --- a/models/tracker/src/types.ts +++ b/models/tracker/src/types.ts @@ -29,7 +29,7 @@ import { type RolesAssignment, type Role, type CollectionSize, - PersonId + type AccountUuid } from '@hcengineering/core' import { ArrOf, @@ -422,7 +422,7 @@ export class TProjectTargetPreference extends TPreference implements ProjectTarg @Mixin(tracker.mixin.ClassicProjectTypeData, tracker.class.Project) @UX(getEmbeddedLabel('Classic project'), tracker.icon.Issues) export class TClassicProjectTypeData extends TProject implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } @Mixin(tracker.mixin.IssueTypeData, tracker.class.Issue) diff --git a/models/training/src/types.ts b/models/training/src/types.ts index 052ef8e895..b06f61bc4d 100644 --- a/models/training/src/types.ts +++ b/models/training/src/types.ts @@ -37,7 +37,7 @@ import core, { type TypedSpace, RolesAssignment, Role, - PersonId + type AccountUuid } from '@hcengineering/core' import { ArrOf, @@ -262,5 +262,5 @@ export class TTrainingAttempt extends TAttachedDoc implements TrainingAttempt { @Mixin(training.mixin.TrainingsTypeData, core.class.TypedSpace) @UX(getEmbeddedLabel('Default Trainings'), training.icon.TrainingApplication) export class TTrainingsTypeData extends TTypedSpace implements RolesAssignment { - [key: Ref]: PersonId[] + [key: Ref]: AccountUuid[] } diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 6c26dcc62d..f70ffc1074 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -947,6 +947,10 @@ export function createModel (builder: Builder): void { component: view.component.ValueFilter }) + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + builder.createDoc( view.class.FilterMode, core.space.Model, @@ -1187,6 +1191,10 @@ export function createModel (builder: Builder): void { presenter: view.component.StringFilterPresenter }) + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributeFilterPresenter, { + presenter: view.component.StringFilterPresenter + }) + classPresenter(builder, core.class.EnumOf, view.component.EnumPresenter, view.component.EnumEditor) createAction( @@ -1240,6 +1248,15 @@ export function createModel (builder: Builder): void { builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeFilterPresenter, { presenter: view.component.PersonIdFilterValuePresenter }) + + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributePresenter, { + presenter: view.component.PersonIdPresenter, + arrayPresenter: view.component.PersonArrayEditor + }) + + builder.mixin(core.class.TypeAccountUuid, core.class.Class, view.mixin.AttributeFilterPresenter, { + presenter: view.component.PersonIdFilterValuePresenter + }) } export default view diff --git a/packages/account-client/src/client.ts b/packages/account-client/src/client.ts index 479cae39b2..709d7b8bc2 100644 --- a/packages/account-client/src/client.ts +++ b/packages/account-client/src/client.ts @@ -64,13 +64,7 @@ export interface AccountClient { inviteId: string ) => Promise join: (email: string, password: string, inviteId: string) => Promise - createInviteLink: ( - exp: number, - emailMask: string, - limit: number, - role: AccountRole, - personId?: any - ) => Promise + createInviteLink: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise checkJoin: (inviteId: string) => Promise getWorkspaceInfo: (updateLastVisit?: boolean) => Promise getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise @@ -86,7 +80,7 @@ export interface AccountClient { updateWorkspaceRole: (account: string, role: AccountRole) => Promise updateWorkspaceName: (name: string) => Promise deleteWorkspace: () => Promise - findPerson: (socialString: string) => Promise + findPerson: (socialString: PersonId) => Promise // Service methods workerHandshake: (region: string, version: Data, operation: WorkspaceOperation) => Promise @@ -350,16 +344,10 @@ class AccountClientImpl implements AccountClient { return await this.rpc(request) } - async createInviteLink ( - exp: number, - emailMask: string, - limit: number, - role: AccountRole, - personId?: any - ): Promise { + async createInviteLink (exp: number, emailMask: string, limit: number, role: AccountRole): Promise { const request = { method: 'createInviteLink' as const, - params: { exp, emailMask, limit, role, personId } + params: { exp, emailMask, limit, role } } return await this.rpc(request) diff --git a/packages/account-client/src/types.ts b/packages/account-client/src/types.ts index e403cf5890..89b36740f2 100644 --- a/packages/account-client/src/types.ts +++ b/packages/account-client/src/types.ts @@ -1,14 +1,14 @@ import { + type AccountUuid, PersonId, WorkspaceDataId, WorkspaceUuid, type AccountRole, - type PersonUuid, type Timestamp } from '@hcengineering/core' export interface LoginInfo { - account: PersonUuid + account: AccountUuid name?: string socialId?: PersonId token?: string diff --git a/packages/core/lang/cs.json b/packages/core/lang/cs.json index 53f70d1301..3007c2cd4f 100644 --- a/packages/core/lang/cs.json +++ b/packages/core/lang/cs.json @@ -66,6 +66,7 @@ "Relation": "Vztah", "Relations": "Vztahy", "AddRelation": "Přidat vztah", - "PersonId": "Osoba" + "PersonId": "Osoba", + "AccountId": "Účet" } } \ No newline at end of file diff --git a/packages/core/lang/de.json b/packages/core/lang/de.json index af3cfe95d8..cc7acb9ded 100644 --- a/packages/core/lang/de.json +++ b/packages/core/lang/de.json @@ -65,6 +65,8 @@ "BlobContentType": "Inhaltstyp", "Relation": "Beziehung", "Relations": "Beziehungen", - "AddRelation": "Beziehung hinzufügen" + "AddRelation": "Beziehung hinzufügen", + "PersonId": "Person", + "AccountId": "Konto" } } diff --git a/packages/core/lang/en.json b/packages/core/lang/en.json index d9cd134a8b..9cbb35b14c 100644 --- a/packages/core/lang/en.json +++ b/packages/core/lang/en.json @@ -66,6 +66,7 @@ "Relation": "Relation", "Relations": "Relations", "AddRelation": "Add relation", - "PersonId": "Person" + "PersonId": "Person", + "AccountId": "Account" } } diff --git a/packages/core/lang/es.json b/packages/core/lang/es.json index bdb702591d..0452e20070 100644 --- a/packages/core/lang/es.json +++ b/packages/core/lang/es.json @@ -59,6 +59,7 @@ "Relation": "Relación", "Relations": "Relaciones", "AddRelation": "Añadir relación", - "PersonId": "Id. de persona" + "PersonId": "Id. de persona", + "AccountId": "Cuenta" } } diff --git a/packages/core/lang/fr.json b/packages/core/lang/fr.json index 51502b8bee..187972dd03 100644 --- a/packages/core/lang/fr.json +++ b/packages/core/lang/fr.json @@ -66,6 +66,7 @@ "Relation": "Relation", "Relations": "Relations", "AddRelation": "Ajouter une relation", - "PersonId": "Id de personne" + "PersonId": "Id de personne", + "AccountId": "Compte" } } \ No newline at end of file diff --git a/packages/core/lang/it.json b/packages/core/lang/it.json index a977dc4aa8..cf24f5a8c8 100644 --- a/packages/core/lang/it.json +++ b/packages/core/lang/it.json @@ -66,6 +66,7 @@ "Relation": "Relazione", "Relations": "Relazioni", "AddRelation": "Aggiungi relazione", - "PersonId": "ID persona" + "PersonId": "ID persona", + "AccountId": "ID account" } } diff --git a/packages/core/lang/pt.json b/packages/core/lang/pt.json index eb7d5613d8..87911c8648 100644 --- a/packages/core/lang/pt.json +++ b/packages/core/lang/pt.json @@ -59,6 +59,7 @@ "Relation": "Relação", "Relations": "Relações", "AddRelation": "Adicionar relação", - "PersonId": "ID de pessoa" + "PersonId": "ID de pessoa", + "AccountId": "Conta" } } diff --git a/packages/core/lang/ru.json b/packages/core/lang/ru.json index dc4042229a..81c0f1f3a8 100644 --- a/packages/core/lang/ru.json +++ b/packages/core/lang/ru.json @@ -66,6 +66,7 @@ "Relation": "Связь", "Relations": "Связи", "AddRelation": "Добавить связь", - "PersonId": "Персона" + "PersonId": "Персона", + "AccountId": "Аккаунт" } } diff --git a/packages/core/lang/zh.json b/packages/core/lang/zh.json index 2c3c6856f4..a8e9d195dd 100644 --- a/packages/core/lang/zh.json +++ b/packages/core/lang/zh.json @@ -66,6 +66,7 @@ "Relation": "关系", "Relations": "关系", "AddRelation": "添加关系", - "PersonId": "人员 ID" + "PersonId": "人员 ID", + "AccountId": "帐户" } } diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 346abc96f3..cc52f7a442 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -70,7 +70,7 @@ export interface Obj { } export interface Account { - uuid: PersonUuid + uuid: AccountUuid role: AccountRole primarySocialId: PersonId socialIds: PersonId[] @@ -82,6 +82,13 @@ export interface Account { */ export type PersonUuid = string & { __personUuid: true } +/** + * @public + * Global person account UUID. + * The same UUID as PersonUuid but for when account exists. + */ +export type AccountUuid = PersonUuid & { __accountUuid: true } + /** * @public * String representation of a social id linked to a global person. @@ -431,9 +438,9 @@ export interface Space extends Doc { name: string description: string private: boolean - members: Arr + members: AccountUuid[] archived: boolean - owners?: PersonId[] + owners?: AccountUuid[] autoJoin?: boolean } @@ -474,7 +481,7 @@ export interface SpaceType extends Doc { name: string shortDescription?: string descriptor: Ref - members?: PersonId[] // this members will be added automatically to new space, also change this fiield will affect existing spaces + members?: AccountUuid[] // this members will be added automatically to new space, also change this fiield will affect existing spaces autoJoin?: boolean // if true, all new users will be added to space automatically targetClass: Ref> // A dynamic mixin for Spaces to hold custom attributes and roles assignment of the space type roles: CollectionSize @@ -493,7 +500,7 @@ export interface Role extends AttachedDoc { * @public * Defines assignment of employees to a role within a space */ -export type RolesAssignment = Record, PersonId[] | undefined> +export type RolesAssignment = Record, AccountUuid[] | undefined> /** * @public @@ -548,7 +555,7 @@ export interface PersonInfo extends BasePerson { // TODO: move to contact export interface UserStatus extends Doc { online: boolean - user: PersonUuid + user: AccountUuid } /** diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 2f7e93655b..3b2e32e59d 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -60,8 +60,8 @@ import type { TypeAny, TypedSpace, UserStatus, - PersonUuid, - Version + Version, + AccountUuid } from './classes' import { Status, StatusCategory } from './status' import type { @@ -86,7 +86,7 @@ export const coreId = 'core' as Plugin */ // TODO: consider removing email? export const systemAccountEmail = 'anticrm@hc.engineering' -export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as PersonUuid +export const systemAccountUuid = '1749089e-22e6-48de-af4e-165e18fbd2f9' as AccountUuid export const systemAccount: Account = { uuid: systemAccountUuid, role: AccountRole.Owner, @@ -94,6 +94,8 @@ export const systemAccount: Account = { socialIds: [] } +export const configUserAccountUuid = '0d94731c-0787-4bcd-aefe-304efc3706b1' as AccountUuid + export default plugin(coreId, { class: { Obj: '' as Ref>, @@ -136,6 +138,7 @@ export default plugin(coreId, { TypeDate: '' as Ref>>, TypeCollaborativeDoc: '' as Ref>>, TypePersonId: '' as Ref>>, + TypeAccountUuid: '' as Ref>>, RefTo: '' as Ref>>, ArrOf: '' as Ref>>, Enum: '' as Ref>, @@ -224,6 +227,7 @@ export default plugin(coreId, { CollaborativeDoc: '' as IntlString, MarkupBlobRef: '' as IntlString, PersonId: '' as IntlString, + AccountId: '' as IntlString, Number: '' as IntlString, Boolean: '' as IntlString, Timestamp: '' as IntlString, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e782327639..f8aeb3eee1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,7 +17,7 @@ import core from './component' export * from './classes' export * from './client' export * from './collaboration' -export { coreId, systemAccountUuid, systemAccount } from './component' +export { coreId, systemAccountUuid, systemAccount, configUserAccountUuid } from './component' export * from './hierarchy' export * from './measurements' export * from './memdb' diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 2e9e72b2dc..0286defb6d 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import type { Account, Doc, DocIndexState, Domain, PersonId, PersonUuid, Ref } from './classes' +import type { Account, AccountUuid, Doc, DocIndexState, Domain, PersonId, Ref } from './classes' import { MeasureContext } from './measurements' import { DocumentQuery, FindOptions } from './storage' import type { DocumentUpdate, Tx } from './tx' @@ -51,7 +51,7 @@ export interface SessionData { isTriggerCtx?: boolean workspace: WorkspaceIds branding: Branding | null - socialStringsToUsers: Map + socialStringsToUsers: Map fulltextUpdates?: Map, DocIndexState> asyncRequests?: (() => Promise)[] diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 4cc075f84e..0cff96ae71 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -568,7 +568,7 @@ export async function checkPermission ( const me = getCurrentAccount() const asMixin = client.getHierarchy().as(space, mixin) - const myRoles = type.$lookup?.roles?.filter((role) => includesAny((asMixin as any)[role._id], me.socialIds)) as Role[] + const myRoles = type.$lookup?.roles?.filter((role) => ((asMixin as any)[role._id] ?? []).includes(me.uuid)) as Role[] if (myRoles === undefined) { return false diff --git a/packages/importer/src/huly/unified.ts b/packages/importer/src/huly/unified.ts index e5fcc7559c..823d506fc7 100644 --- a/packages/importer/src/huly/unified.ts +++ b/packages/importer/src/huly/unified.ts @@ -14,8 +14,9 @@ // /* eslint-disable @typescript-eslint/no-unused-vars */ import { type Attachment } from '@hcengineering/attachment' -import contact, { Employee, type Person } from '@hcengineering/contact' +import contact, { Employee, SocialIdentity, type Person } from '@hcengineering/contact' import { + AccountUuid, buildSocialIdString, type Class, type Doc, @@ -341,6 +342,7 @@ export class UnifiedFormatImporter { private personsByName = new Map>() private employeesByName = new Map>() + private accountsByEmail = new Map() constructor ( private readonly client: TxOperations, @@ -352,6 +354,7 @@ export class UnifiedFormatImporter { private async initCaches (): Promise { await this.cachePersonsByNames() + await this.cacheAccountsByEmails() await this.cacheEmployeesByName() } @@ -611,6 +614,14 @@ export class UnifiedFormatImporter { return buildSocialIdString({ type: SocialIdType.EMAIL, value: email }) } + private findAccountByEmail (email: string): AccountUuid { + const account = this.accountsByEmail.get(email) + if (account === undefined) { + throw new Error(`Account not found: ${email}`) + } + return account + } + private findEmployeeByName (name: string): Ref { const employee = this.employeesByName.get(name) if (employee === undefined) { @@ -793,9 +804,9 @@ export class UnifiedFormatImporter { defaultIssueStatus: projectHeader.defaultIssueStatus !== undefined ? { name: projectHeader.defaultIssueStatus } : undefined, owners: - projectHeader.owners !== undefined ? projectHeader.owners.map((email) => this.getSocialIdByEmail(email)) : [], + projectHeader.owners !== undefined ? projectHeader.owners.map((email) => this.findAccountByEmail(email)) : [], members: - projectHeader.members !== undefined ? projectHeader.members.map((email) => this.getSocialIdByEmail(email)) : [], + projectHeader.members !== undefined ? projectHeader.members.map((email) => this.findAccountByEmail(email)) : [], docs: [] } } @@ -809,9 +820,9 @@ export class UnifiedFormatImporter { archived: spaceHeader.archived ?? false, description: spaceHeader.description, emoji: spaceHeader.emoji, - owners: spaceHeader.owners !== undefined ? spaceHeader.owners.map((email) => this.getSocialIdByEmail(email)) : [], + owners: spaceHeader.owners !== undefined ? spaceHeader.owners.map((email) => this.findAccountByEmail(email)) : [], members: - spaceHeader.members !== undefined ? spaceHeader.members.map((email) => this.getSocialIdByEmail(email)) : [], + spaceHeader.members !== undefined ? spaceHeader.members.map((email) => this.findAccountByEmail(email)) : [], docs: [] } } @@ -823,11 +834,11 @@ export class UnifiedFormatImporter { private: spaceHeader.private ?? false, archived: spaceHeader.archived ?? false, description: spaceHeader.description, - owners: spaceHeader.owners?.map((email) => this.getSocialIdByEmail(email)) ?? [], - members: spaceHeader.members?.map((email) => this.getSocialIdByEmail(email)) ?? [], - qualified: spaceHeader.qualified !== undefined ? this.getSocialIdByEmail(spaceHeader.qualified) : undefined, - manager: spaceHeader.manager !== undefined ? this.getSocialIdByEmail(spaceHeader.manager) : undefined, - qara: spaceHeader.qara !== undefined ? this.getSocialIdByEmail(spaceHeader.qara) : undefined, + owners: spaceHeader.owners?.map((email) => this.findAccountByEmail(email)) ?? [], + members: spaceHeader.members?.map((email) => this.findAccountByEmail(email)) ?? [], + qualified: spaceHeader.qualified !== undefined ? this.findAccountByEmail(spaceHeader.qualified) : undefined, + manager: spaceHeader.manager !== undefined ? this.findAccountByEmail(spaceHeader.manager) : undefined, + qara: spaceHeader.qara !== undefined ? this.findAccountByEmail(spaceHeader.qara) : undefined, docs: [] } } @@ -940,6 +951,24 @@ export class UnifiedFormatImporter { return match != null ? match[1] : content } + private async cacheAccountsByEmails (): 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/packages/importer/src/importer/importer.ts b/packages/importer/src/importer/importer.ts index 4a79ce908e..2ab4e1b103 100644 --- a/packages/importer/src/importer/importer.ts +++ b/packages/importer/src/importer/importer.ts @@ -47,7 +47,8 @@ import core, { type Status, type Timestamp, type TxOperations, - type PersonId + type PersonId, + type AccountUuid } from '@hcengineering/core' import document, { type Document, getFirstRank, type Teamspace } from '@hcengineering/document' import task, { @@ -103,8 +104,8 @@ export interface ImportSpace { archived?: boolean description?: string emoji?: string - owners?: PersonId[] - members?: PersonId[] + owners?: AccountUuid[] + members?: AccountUuid[] docs: T[] } export interface ImportDoc { @@ -176,9 +177,9 @@ export interface ImportDrawing { export type ImportControlledDoc = ImportControlledDocument | ImportControlledDocumentTemplate // todo: rename export interface ImportOrgSpace extends ImportSpace { class: Ref> - qualified?: PersonId - manager?: PersonId - qara?: PersonId + qualified?: AccountUuid + manager?: AccountUuid + qara?: AccountUuid } export interface ImportControlledDocumentTemplate extends ImportDoc { diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index 2f13509b7f..0422d9d0ea 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -50,7 +50,8 @@ import core, { ArrOf as TypeArrOf, Collection as TypeCollection, TypeDate as TypeDateType, - generateId + generateId, + type AccountUuid } from '@hcengineering/core' import type { Asset, IntlString } from '@hcengineering/platform' import toposort from 'toposort' @@ -527,3 +528,7 @@ export function TypeRank (): Type { export function TypePersonId (): Type { return { _class: core.class.TypePersonId, label: core.string.PersonId } } + +export function TypeAccountUuid (): Type { + return { _class: core.class.TypeAccountUuid, label: core.string.AccountId } +} diff --git a/packages/presentation/src/components/SpacesPopup.svelte b/packages/presentation/src/components/SpacesPopup.svelte index d1f9ef632a..fdc1cbeb6f 100644 --- a/packages/presentation/src/components/SpacesPopup.svelte +++ b/packages/presentation/src/components/SpacesPopup.svelte @@ -49,7 +49,7 @@ $: query.query( _class, { - members: { $in: getCurrentAccount().socialIds }, + members: getCurrentAccount().uuid, ...(spaceQuery ?? {}), ...(search !== undefined && search !== '' ? { diff --git a/packages/query/src/__tests__/minmodel.ts b/packages/query/src/__tests__/minmodel.ts index 57ce373345..6cb584cc6e 100644 --- a/packages/query/src/__tests__/minmodel.ts +++ b/packages/query/src/__tests__/minmodel.ts @@ -25,7 +25,8 @@ import type { Ref, Space, TxCreateDoc, - TxCUD + TxCUD, + AccountUuid } from '@hcengineering/core' import core, { AttachedDoc, ClassifierKind, DOMAIN_MODEL, DOMAIN_TX, TxFactory } from '@hcengineering/core' import type { IntlString, Plugin } from '@hcengineering/platform' @@ -240,8 +241,8 @@ export function genMinModel (): TxCUD[] { }) ) - const u1 = 'User1' as PersonId - const u2 = 'User2' as PersonId + const u1 = 'User1' as AccountUuid + const u2 = 'User2' as AccountUuid // TODO: fixme! txes.push( // createDoc(core.class.Account, { email: 'user1@site.com', role: AccountRole.User }, u1), diff --git a/packages/query/src/__tests__/query.test.ts b/packages/query/src/__tests__/query.test.ts index dcea4759a6..bd6ea9526c 100644 --- a/packages/query/src/__tests__/query.test.ts +++ b/packages/query/src/__tests__/query.test.ts @@ -21,6 +21,7 @@ import core, { Ref, SortingOrder, Space, + systemAccountUuid, Tx, TxCreateDoc, TxOperations, @@ -352,7 +353,7 @@ describe('query', () => { attempt++ await factory.updateDoc(space._class, space.space, space._id, { name: attempt.toString(), - $push: { members: core.account.System } + $push: { members: systemAccountUuid } }) } await pp diff --git a/plugins/ai-bot/src/index.ts b/plugins/ai-bot/src/index.ts index a85d3bf0f7..58b6d0ce3e 100644 --- a/plugins/ai-bot/src/index.ts +++ b/plugins/ai-bot/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { buildSocialIdString, SocialIdType } from '@hcengineering/core' +import { AccountUuid, buildSocialIdString, SocialIdType } from '@hcengineering/core' import type { Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -21,6 +21,7 @@ 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({ type: SocialIdType.EMAIL, diff --git a/plugins/board-resources/src/utils/BoardUtils.ts b/plugins/board-resources/src/utils/BoardUtils.ts index a798ab1787..f03fbaa195 100644 --- a/plugins/board-resources/src/utils/BoardUtils.ts +++ b/plugins/board-resources/src/utils/BoardUtils.ts @@ -27,7 +27,7 @@ export async function createBoard ( description, private: false, archived: false, - members: [getCurrentAccount().primarySocialId], + members: [getCurrentAccount().uuid], type }) diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte index 07e48ef53e..04af876efc 100644 --- a/plugins/chunter-resources/src/components/Channel.svelte +++ b/plugins/chunter-resources/src/components/Channel.svelte @@ -76,7 +76,7 @@ context ?? (await client.findOne(notification.class.DocNotifyContext, { objectId: object._id, - user: { $in: getCurrentAccount().socialIds } + user: getCurrentAccount().uuid })) const hasRefs = ((object as WithReferences).references ?? 0) > 0 refsLoaded = hasRefs diff --git a/plugins/chunter-resources/src/components/ChannelView.svelte b/plugins/chunter-resources/src/components/ChannelView.svelte index 70951d70e2..7e4ca3321d 100644 --- a/plugins/chunter-resources/src/components/ChannelView.svelte +++ b/plugins/chunter-resources/src/components/ChannelView.svelte @@ -31,7 +31,6 @@ import view from '@hcengineering/view' import { messageInFocus } from '@hcengineering/activity-resources' import { Presence } from '@hcengineering/presence-resources' - import { includesAny } from '@hcengineering/contact' import ChannelComponent from './Channel.svelte' import ChannelHeader from './ChannelHeader.svelte' @@ -71,14 +70,14 @@ if (hierarchy.isDerived(object._class, core.class.Space)) { const space = object as Space - return !includesAny(space.members, acc.socialIds) + return !space.members.includes(acc.uuid) } return false } async function join (): Promise { - await client.update(object as Space, { $push: { members: acc.primarySocialId } }) + await client.update(object as Space, { $push: { members: acc.uuid } }) } defineSeparators('aside', panelSeparators) diff --git a/plugins/chunter-resources/src/components/DmHeader.svelte b/plugins/chunter-resources/src/components/DmHeader.svelte index 745aa75c02..55fe2d1b26 100644 --- a/plugins/chunter-resources/src/components/DmHeader.svelte +++ b/plugins/chunter-resources/src/components/DmHeader.svelte @@ -15,8 +15,8 @@ {#if showStatus && person} diff --git a/plugins/contact-resources/src/components/PersonIdArrayEditor.svelte b/plugins/contact-resources/src/components/PersonIdArrayEditor.svelte new file mode 100644 index 0000000000..b6aa0c04ee --- /dev/null +++ b/plugins/contact-resources/src/components/PersonIdArrayEditor.svelte @@ -0,0 +1,149 @@ + + + + diff --git a/plugins/contact-resources/src/components/SpaceMembers.svelte b/plugins/contact-resources/src/components/SpaceMembers.svelte index f9f77dd5ae..7fd157b90f 100644 --- a/plugins/contact-resources/src/components/SpaceMembers.svelte +++ b/plugins/contact-resources/src/components/SpaceMembers.svelte @@ -15,29 +15,30 @@ @@ -48,7 +47,7 @@ {label} {value} {onChange} - readonly={readonly || !hasAccountRole(getCurrentAccount(), AccountRole.User)} + readonly={readonly || !hasAccountRole(myAcc, AccountRole.User)} {kind} {size} {width} diff --git a/plugins/contact-resources/src/components/UsersList.svelte b/plugins/contact-resources/src/components/UsersList.svelte index d6b2550269..339c211a4e 100644 --- a/plugins/contact-resources/src/components/UsersList.svelte +++ b/plugins/contact-resources/src/components/UsersList.svelte @@ -31,6 +31,7 @@ export let disableDeselectFor: Ref[] = [] export let showStatus = true export let skipInactive = false + export let skipOnlyLocal = true const dispatch = createEventDispatcher() const query = createQuery() @@ -52,7 +53,8 @@ : { [searchField]: { $like: '%' + search + '%' } } : {}), ...(skipCurrentAccount ? { _id: { $ne: me } } : {}), - ...(skipInactive ? { active: true } : {}) + ...(skipInactive ? { active: true } : {}), + ...(skipOnlyLocal ? { personUuid: { $exists: true } } : {}) }, (result) => { result.sort((a, b) => { diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 7f7977ba86..6bae3c536e 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -50,6 +50,7 @@ import { type TooltipAlignment } from '@hcengineering/ui' import { AggregationManager } from '@hcengineering/view-resources' +import PersonIdArrayEditor from './components/PersonIdArrayEditor.svelte' import AccountArrayEditor from './components/AccountArrayEditor.svelte' import AccountBox from './components/AccountBox.svelte' import AssigneeBox from './components/AssigneeBox.svelte' @@ -152,6 +153,7 @@ export * from './utils' export { employeeByIdStore } from './utils' export * from './assignee' export { + PersonIdArrayEditor, AccountArrayEditor, AccountBox, AssigneeBox, @@ -367,6 +369,7 @@ export default async (): Promise => ({ EmployeeArrayEditor, EmployeeEditor, CreateEmployee, + PersonIdArrayEditor, AccountArrayEditor, ChannelFilter, MergePersons, diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 160b0c22ba..f953d12a80 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -32,8 +32,7 @@ import { type SocialIdentity, type PermissionsStore, type PermissionsBySpace, - type PersonsByPermission, - includesAny + type PersonsByPermission } from '@hcengineering/contact' import core, { type AggregateValue, @@ -45,7 +44,6 @@ import core, { type IdMap, type ObjQueryType, type Permission, - type PersonId, type Ref, SocialIdType, type Space, @@ -55,7 +53,9 @@ import core, { type TypedSpace, type UserStatus, type WithLookup, - type PersonUuid + type AccountUuid, + notEmpty, + getCurrentAccount } from '@hcengineering/core' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import { type IntlString, getEmbeddedLabel, getResource, translate } from '@hcengineering/platform' @@ -372,6 +372,10 @@ export const personRefByPersonIdStore = derived(socialIdsStore, (socialIds) => { const mapped = socialIds.map((si) => [si.key, si.attachedTo] as const) return new Map(mapped) }) +/** + * [AccountUuid => Ref] mapping + */ +export const personRefByAccountUuidStore = writable>>(new Map()) /** * [PersonId (social string) => Person] mapping */ @@ -386,7 +390,25 @@ export const personByPersonIdStore = derived( } return [personId, person] as const }) - .filter((it) => it !== undefined) as Array]> + .filter(notEmpty) + return new Map(mapped) + } +) +/** + * [AccountUuid => Person] mapping + */ +export const employeeByAccountStore = derived( + [personRefByAccountUuidStore, employeeByIdStore], + ([personRefByAccount, employeeById]) => { + const mapped = Array.from(personRefByAccount.entries()) + .map(([account, employeeRef]) => { + const employee = employeeById.get(employeeRef) + if (employee === undefined) { + return undefined + } + return [account, employee] as const + }) + .filter(notEmpty) return new Map(mapped) } ) @@ -414,7 +436,7 @@ export const primarySocialIdByPersonIdStore = derived(socialIdsByPersonIdStore, }) export const channelProviders = writable([]) -export const statusByUserStore = writable>(new Map()) +export const statusByUserStore = writable>(new Map()) const providerQuery = createQuery(true) const employeesQuery = createQuery(true) @@ -430,6 +452,11 @@ onClient(() => { // We may need to extend this later with guests and github users personByIdStore.set(toIdMap(res)) + personRefByAccountUuidStore.set( + new Map( + res.filter((p) => p.active && p.personUuid != null).map((p) => [p.personUuid as AccountUuid, p._id] as const) + ) + ) }) siQuery.query(contact.class.SocialIdentity, {}, (res) => { @@ -441,7 +468,7 @@ const userStatusesQuery = createQuery(true) export function loadUsersStatus (): void { userStatusesQuery.query(core.class.UserStatus, {}, (res) => { - statusByUserStore.set(new Map(res.map((it) => [it.user, it] as [PersonUuid, UserStatus]))) + statusByUserStore.set(new Map(res.map((it) => [it.user, it]))) }) } @@ -661,66 +688,62 @@ export function checkMyPermission (_id: Ref, space: Ref, const spacesStore = writable([]) -export const permissionsStore = derived( - [mySocialIdsStore, spacesStore, personRefByPersonIdStore], - ([mySocialIds, spaces, personRefByPersonId]) => { - const whitelistedSpaces = new Set>() - const permissionsBySpace: PermissionsBySpace = {} - const employeesByPermission: PersonsByPermission = {} - const client = getClient() - const hierarchy = client.getHierarchy() - const mySocialStrings = mySocialIds.map((si) => si.key) +export const permissionsStore = derived([spacesStore, personRefByAccountUuidStore], ([spaces, personRefByAccount]) => { + const whitelistedSpaces = new Set>() + const permissionsBySpace: PermissionsBySpace = {} + const employeesByPermission: PersonsByPermission = {} + const client = getClient() + const hierarchy = client.getHierarchy() - for (const s of spaces) { - if (hierarchy.isDerived(s._class, core.class.TypedSpace)) { - const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0] - const mixin = type?.targetClass + for (const s of spaces) { + if (hierarchy.isDerived(s._class, core.class.TypedSpace)) { + const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0] + const mixin = type?.targetClass - if (mixin === undefined) { - permissionsBySpace[s._id] = new Set() - employeesByPermission[s._id] = {} + if (mixin === undefined) { + permissionsBySpace[s._id] = new Set() + employeesByPermission[s._id] = {} + continue + } + + const asMixin = hierarchy.as(s, mixin) + const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id }) + const myRoles = roles.filter((r) => ((asMixin as any)[r._id] ?? []).includes(getCurrentAccount().uuid)) + permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions)) + + employeesByPermission[s._id] = {} + + for (const role of roles) { + const assignment: AccountUuid[] = (asMixin as any)[role._id] ?? [] + + if (assignment.length === 0) { continue } - const asMixin = hierarchy.as(s, mixin) - const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id }) - const myRoles = roles.filter((r) => includesAny((asMixin as any)[r._id] ?? [], mySocialStrings)) - permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions)) - - employeesByPermission[s._id] = {} - - for (const role of roles) { - const assignment: PersonId[] = (asMixin as any)[role._id] ?? [] - - if (assignment.length === 0) { - continue + for (const permissionId of role.permissions) { + if (employeesByPermission[s._id][permissionId] === undefined) { + employeesByPermission[s._id][permissionId] = new Set() } - for (const permissionId of role.permissions) { - if (employeesByPermission[s._id][permissionId] === undefined) { - employeesByPermission[s._id][permissionId] = new Set() + assignment.forEach((acc) => { + const personRef = personRefByAccount.get(acc) + if (personRef !== undefined) { + employeesByPermission[s._id][permissionId].add(personRef) } - - assignment.forEach((pid) => { - const personRef = personRefByPersonId.get(pid) - if (personRef !== undefined) { - employeesByPermission[s._id][permissionId].add(personRef) - } - }) - } + }) } - } else { - whitelistedSpaces.add(s._id) } - } - - return { - ps: permissionsBySpace, - ap: employeesByPermission, - whitelist: whitelistedSpaces + } else { + whitelistedSpaces.add(s._id) } } -) + + return { + ps: permissionsBySpace, + ap: employeesByPermission, + whitelist: whitelistedSpaces + } +}) const spaceTypesQuery = createQuery(true) const permissionsQuery = createQuery(true) diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts index 01a258e5e7..e05e2a3728 100644 --- a/plugins/contact/src/index.ts +++ b/plugins/contact/src/index.ts @@ -29,7 +29,8 @@ import { type Blob, type MarkupBlobRef, type Data, - type WithLookup + type WithLookup, + AccountUuid } from '@hcengineering/core' import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform' @@ -172,6 +173,7 @@ export interface Employee extends Person { active: boolean statuses?: number position?: string | null + personUuid?: AccountUuid } /** @@ -224,6 +226,7 @@ export const contactPlugin = plugin(contactId, { ChannelPresenter: '' as AnyComponent, SpaceMembers: '' as AnyComponent, DeleteConfirmationPopup: '' as AnyComponent, + PersonIdArrayEditor: '' as AnyComponent, AccountArrayEditor: '' as AnyComponent, PersonIcon: '' as AnyComponent, EditOrganizationPanel: '' as AnyComponent, diff --git a/plugins/contact/src/utils.ts b/plugins/contact/src/utils.ts index 01159090f4..00c984c79e 100644 --- a/plugins/contact/src/utils.ts +++ b/plugins/contact/src/utils.ts @@ -29,7 +29,9 @@ import { Ref, SocialId, TxFactory, - Person as GlobalPerson + Person as GlobalPerson, + AccountUuid, + notEmpty } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import { ColorDefinition } from '@hcengineering/ui' @@ -372,12 +374,24 @@ export async function getSocialStringsByEmployee (client: Client): Promise { + const employees = await client.findAll(contact.mixin.Employee, { active: true }) + + return employees.map((it) => it.personUuid).filter(notEmpty) +} + export async function getAllEmployeesPrimarySocialStrings (client: Client): Promise { const socialStringsByPerson = getSocialStringsByEmployee(client) return Object.values(socialStringsByPerson).map((it) => pickPrimarySocialId(it)) } +export async function getAllUserAccounts (client: Client): Promise { + const employees = await client.findAll(contact.mixin.Employee, { active: true }) + + return employees.map((it) => it.personUuid).filter(notEmpty) +} + export async function ensureEmployee ( ctx: MeasureContext, me: Account, @@ -436,32 +450,6 @@ export async function ensureEmployee ( await client.tx(updatePersonTx) } - if (me.role !== AccountRole.Guest) { - const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref }) - - if (employee === undefined || !Hierarchy.hasMixin(employee, contact.mixin.Employee) || !employee.active) { - await ctx.with('create-employee', {}, async () => { - if (personRef === undefined) { - // something went wrong - console.error('Person not found') - return null - } - - const createEmployeeTx = txFactory.createTxMixin( - personRef, - contact.class.Person, - contact.space.Contacts, - contact.mixin.Employee, - { - active: true - } - ) - - await client.tx(createEmployeeTx) - }) - } - } - const existingIdentifiers = await client.findAll(contact.class.SocialIdentity, { attachedTo: personRef, attachedToClass: contact.class.Person @@ -498,6 +486,34 @@ export async function ensureEmployee ( } } + // NOTE: it is important to create Employee after Person and SocialIdentities are ensured so all the triggers applied + // on Employee creation will be able to properly map things + if (me.role !== AccountRole.Guest) { + const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref }) + + if (employee === undefined || !Hierarchy.hasMixin(employee, contact.mixin.Employee) || !employee.active) { + await ctx.with('create-employee', {}, async () => { + if (personRef === undefined) { + // something went wrong + console.error('Person not found') + return null + } + + const createEmployeeTx = txFactory.createTxMixin( + personRef, + contact.class.Person, + contact.space.Contacts, + contact.mixin.Employee, + { + active: true + } + ) + + await client.tx(createEmployeeTx) + }) + } + } + // TODO: check for merged persons with this one and do the merge return personRef as Ref } diff --git a/plugins/controlled-documents-resources/src/components/docspace/CreateDocumentsSpace.svelte b/plugins/controlled-documents-resources/src/components/docspace/CreateDocumentsSpace.svelte index 915db0d201..3855d0c22d 100644 --- a/plugins/controlled-documents-resources/src/components/docspace/CreateDocumentsSpace.svelte +++ b/plugins/controlled-documents-resources/src/components/docspace/CreateDocumentsSpace.svelte @@ -15,9 +15,8 @@ diff --git a/plugins/document-resources/src/components/NewDocumentHeader.svelte b/plugins/document-resources/src/components/NewDocumentHeader.svelte index d394a9f316..38921c34c1 100644 --- a/plugins/document-resources/src/components/NewDocumentHeader.svelte +++ b/plugins/document-resources/src/components/NewDocumentHeader.svelte @@ -39,13 +39,12 @@ const client = getClient() const query = createQuery() const myAcc = getCurrentAccount() - const socialStrings = myAcc.socialIds let loading = true let hasTeamspace = false query.query( document.class.Teamspace, - { archived: false, members: { $in: socialStrings } }, + { archived: false, members: myAcc.uuid }, (res) => { hasTeamspace = res.length > 0 loading = false diff --git a/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte b/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte index 5ae381f810..39537cb8ea 100644 --- a/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte +++ b/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte @@ -14,9 +14,8 @@ --> @@ -319,7 +323,7 @@ /> { handleRoleAssignmentChanged(role._id, refs) diff --git a/plugins/products-resources/src/utils.ts b/plugins/products-resources/src/utils.ts index df316da956..539eb8edc1 100644 --- a/plugins/products-resources/src/utils.ts +++ b/plugins/products-resources/src/utils.ts @@ -20,7 +20,6 @@ import core, { checkPermission, getCurrentAccount } from '@hcengineering/core' -import { includesAny } from '@hcengineering/contact' import { getClient } from '@hcengineering/presentation' import { type KeyFilter } from '@hcengineering/view' import documents from '@hcengineering/controlled-documents' @@ -47,7 +46,7 @@ export async function canEditProduct (doc?: Product): Promise { return false } - if (includesAny(doc.owners ?? [], getCurrentAccount().socialIds)) { + if ((doc.owners ?? []).includes(getCurrentAccount().uuid)) { return true } diff --git a/plugins/recruit-resources/src/components/CreateApplication.svelte b/plugins/recruit-resources/src/components/CreateApplication.svelte index c1c831ae6a..34fc551111 100644 --- a/plugins/recruit-resources/src/components/CreateApplication.svelte +++ b/plugins/recruit-resources/src/components/CreateApplication.svelte @@ -217,10 +217,9 @@ let vacancy: Vacancy | undefined const acc = getCurrentAccount() - const socialIds = acc.socialIds $: if (_space) { - spaceQuery.query(recruit.class.Vacancy, { _id: _space, members: { $in: socialIds } }, (res) => { + spaceQuery.query(recruit.class.Vacancy, { _id: _space, members: acc.uuid }, (res) => { vacancy = res.shift() }) } @@ -329,7 +328,7 @@ _class={recruit.class.Vacancy} spaceQuery={{ archived: false, - members: { $in: socialIds }, + members: acc.uuid, ...($selectedTypeStore !== undefined ? { type: $selectedTypeStore } : {}) }} spaceOptions={orgOptions} diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index cf5c81d21b..4a98c347e1 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -17,7 +17,6 @@ import contact, { Organization } from '@hcengineering/contact' import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources' import core, { - PersonId, AttachedData, Data, Ref, @@ -27,7 +26,8 @@ fillDefaults, generateId, getCurrentAccount, - makeCollabId + makeCollabId, + AccountUuid } from '@hcengineering/core' import { getEmbeddedLabel } from '@hcengineering/platform' import { @@ -73,7 +73,7 @@ let issueTemplates: IssueTemplate[] = [] let fullDescription: string = '' - let members = [getCurrentAccount().primarySocialId] + let members = [getCurrentAccount().uuid] let membersChanged: boolean = false $: setDefaultMembers(typeType) @@ -249,7 +249,7 @@ company, members, autoJoin: typeType.autoJoin ?? false, - owners: [getCurrentAccount().primarySocialId], + owners: [getCurrentAccount().uuid], type: typeId } @@ -323,7 +323,7 @@ ) } - function handleRoleAssignmentChanged (roleId: Ref, newMembers: PersonId[]): void { + function handleRoleAssignmentChanged (roleId: Ref, newMembers: AccountUuid[]): void { if (rolesAssignment === undefined) { rolesAssignment = {} } diff --git a/plugins/setting-resources/src/components/Spaces.svelte b/plugins/setting-resources/src/components/Spaces.svelte index b63526211b..39774c118f 100644 --- a/plugins/setting-resources/src/components/Spaces.svelte +++ b/plugins/setting-resources/src/components/Spaces.svelte @@ -14,7 +14,7 @@ --> {#if object !== undefined} diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index 9d55a69971..74e513bbda 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import type { PersonId, AccountRole, Blob, Class, Configuration, Doc, Mixin, Ref } from '@hcengineering/core' +import type { AccountRole, Blob, Class, Configuration, Doc, Mixin, Ref, AccountUuid } from '@hcengineering/core' import type { Metadata, Plugin } from '@hcengineering/platform' import { Asset, IntlString, Resource, plugin } from '@hcengineering/platform' import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates' @@ -55,7 +55,7 @@ export interface Integration extends Doc { disabled: boolean value: string error?: IntlString | null - shared?: PersonId[] + shared?: AccountUuid[] } /** diff --git a/plugins/task-resources/src/index.ts b/plugins/task-resources/src/index.ts index 11772bcc60..09491259e8 100644 --- a/plugins/task-resources/src/index.ts +++ b/plugins/task-resources/src/index.ts @@ -307,7 +307,7 @@ onClient((client, user) => { projectQuery.query( task.class.Project, - { members: { $in: getCurrentAccount().socialIds } }, + { members: getCurrentAccount().uuid }, (res) => { typesOfJoinedProjectsStore.set(res.map((r) => r.type).filter((it, idx, arr) => arr.indexOf(it) === idx)) joinedProjectsStore.set(res) diff --git a/plugins/templates-resources/src/components/CreateTemplateCategory.svelte b/plugins/templates-resources/src/components/CreateTemplateCategory.svelte index d52565dccc..3fda044783 100644 --- a/plugins/templates-resources/src/components/CreateTemplateCategory.svelte +++ b/plugins/templates-resources/src/components/CreateTemplateCategory.svelte @@ -15,7 +15,7 @@ diff --git a/plugins/test-management-resources/src/components/TestManagementSpaceHeader.svelte b/plugins/test-management-resources/src/components/TestManagementSpaceHeader.svelte index f3864eeab8..7b21a07498 100644 --- a/plugins/test-management-resources/src/components/TestManagementSpaceHeader.svelte +++ b/plugins/test-management-resources/src/components/TestManagementSpaceHeader.svelte @@ -25,8 +25,6 @@ export let currentSpace: Ref | undefined const myAcc = getCurrentAccount() - const socialStrings = myAcc.socialIds - const query = createQuery() let hasProject = currentSpace !== undefined @@ -34,7 +32,7 @@ if (!hasProject) { query.query( testManagement.class.TestProject, - { archived: false, members: { $in: socialStrings } }, + { archived: false, members: myAcc.uuid }, (res) => { hasProject = res.length > 0 loading = false diff --git a/plugins/test-management-resources/src/components/project/CreateProject.svelte b/plugins/test-management-resources/src/components/project/CreateProject.svelte index a65d19f2c2..1b36bb61a3 100644 --- a/plugins/test-management-resources/src/components/project/CreateProject.svelte +++ b/plugins/test-management-resources/src/components/project/CreateProject.svelte @@ -15,7 +15,7 @@ { const space = doc as Space - if (includesAny(space.owners ?? [], getCurrentAccount().socialIds)) { + if ((space.owners ?? []).includes(getCurrentAccount().uuid)) { return true } @@ -84,7 +83,7 @@ export async function canArchiveSpace (doc?: Doc | Doc[]): Promise { const space = doc as Space - if (includesAny(space.owners ?? [], getCurrentAccount().socialIds)) { + if ((space.owners ?? []).includes(getCurrentAccount().uuid)) { return true } @@ -110,7 +109,7 @@ export async function canDeleteSpace (doc?: Doc | Doc[]): Promise { const space = doc as Space - if (includesAny(space.owners ?? [], getCurrentAccount().socialIds)) { + if ((space.owners ?? []).includes(getCurrentAccount().uuid)) { return true } @@ -132,7 +131,7 @@ export async function canJoinSpace (doc?: Doc | Doc[]): Promise { const space = doc as Space - return !includesAny(space.members ?? [], getCurrentAccount().socialIds) + return !(space.members ?? []).includes(getCurrentAccount().uuid) } export async function canLeaveSpace (doc?: Doc | Doc[]): Promise { @@ -142,7 +141,7 @@ export async function canLeaveSpace (doc?: Doc | Doc[]): Promise { const space = doc as Space - return includesAny(space.members ?? [], getCurrentAccount().socialIds) + return (space.members ?? []).includes(getCurrentAccount().uuid) } export function isClipboardAvailable (doc?: Doc | Doc[]): boolean { diff --git a/plugins/workbench-resources/src/components/Navigator.svelte b/plugins/workbench-resources/src/components/Navigator.svelte index 936090a148..5af238156a 100644 --- a/plugins/workbench-resources/src/components/Navigator.svelte +++ b/plugins/workbench-resources/src/components/Navigator.svelte @@ -52,7 +52,7 @@ !adminUser ? { ...(classes.length === 1 ? {} : { _class: { $in: classes } }), - members: { $in: getCurrentAccount().socialIds } + members: getCurrentAccount().uuid } : { ...(classes.length === 1 ? {} : { _class: { $in: classes } }) }, (result) => { diff --git a/qms-tests/restore-workspace.sh b/qms-tests/restore-workspace.sh index 7ae6f2bb6a..ba74ebb6a8 100755 --- a/qms-tests/restore-workspace.sh +++ b/qms-tests/restore-workspace.sh @@ -16,3 +16,7 @@ ./tool.sh configure sanity-ws-qms --enable=* ./tool.sh configure sanity-ws-qms --list + +# resets employee active status so it can be set again and trigger filling default spaces owners +# can be removed once we merge prod and develop and update the sanity workspace backup +./tool.sh change-field sanity-ws-qms --objectId 65a04887e1043543cd5f21a5 --objectClass contact:class:Person --attribute contact:mixin:Employee.active --value false --type boolean \ No newline at end of file diff --git a/qms-tests/sanity/tests/model/contact-page.ts b/qms-tests/sanity/tests/model/contact-page.ts new file mode 100644 index 0000000000..8149bb630d --- /dev/null +++ b/qms-tests/sanity/tests/model/contact-page.ts @@ -0,0 +1,28 @@ +import { expect, type Locator, type Page } from '@playwright/test' + +export class ContactPage { + page: Page + + constructor (page: Page) { + this.page = page + } + + readonly appContact = (): Locator => this.page.locator('[id="app-contact\\:string\\:Contacts"]') + readonly employeeNavElement = (Employee: string): Locator => + this.page.locator(`.hulyNavItem-container:has-text("${Employee}")`) + + readonly employeeEntry = (first: string, last: string): Locator => + this.page.locator(`td:has-text("${last} ${first}")`) + + async clickAppContact (): Promise { + await this.appContact().click() + } + + async clickEmployeeNavElement (Employee: string): Promise { + await this.employeeNavElement(Employee).click() + } + + async checkIfPersonIsCreated (first: string, last: string): Promise { + await expect(this.employeeEntry(first, last)).toBeVisible() + } +} diff --git a/server-plugins/activity-resources/package.json b/server-plugins/activity-resources/package.json index 43c4b8c1bf..e488a81cac 100644 --- a/server-plugins/activity-resources/package.json +++ b/server-plugins/activity-resources/package.json @@ -42,6 +42,7 @@ "@hcengineering/notification": "^0.6.23", "@hcengineering/platform": "^0.6.11", "@hcengineering/server-activity": "^0.6.0", + "@hcengineering/server-contact": "^0.6.1", "@hcengineering/server-core": "^0.6.1", "@hcengineering/server-notification-resources": "^0.6.0", "@hcengineering/text-core": "^0.6.0", diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index 0826cb92b5..ee1a9b503b 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -37,6 +37,7 @@ import core, { TxCUD, TxProcessor } from '@hcengineering/core' +import { getAccountBySocialId } from '@hcengineering/server-contact' import notification, { NotificationContent } from '@hcengineering/notification' import { getResource, translate } from '@hcengineering/platform' import { ActivityControl, DocObjectCache } from '@hcengineering/server-activity' @@ -108,9 +109,9 @@ export async function createReactionNotifications (tx: TxCUD, control: return [] } - const user = parentMessage.createdBy + const userSocialId = parentMessage.createdBy - if (user === undefined || user === core.account.System || user === tx.modifiedBy) { + if (userSocialId === undefined || userSocialId === core.account.System || userSocialId === tx.modifiedBy) { return [] } @@ -136,9 +137,14 @@ export async function createReactionNotifications (tx: TxCUD, control: res.push(messageTx) const docUpdateMessage = TxProcessor.createDoc2Doc(messageTx as TxCreateDoc) + const account = await getAccountBySocialId(control, userSocialId) + + if (account == null) { + return [] + } res = res.concat( - await createCollabDocInfo(control.ctx, res, [user], control, tx, parentMessage, [docUpdateMessage], { + await createCollabDocInfo(control.ctx, res, [account], control, tx, parentMessage, [docUpdateMessage], { isOwn: true, isSpace: false, shouldUpdateTimestamp: false diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index db1bf3ba58..695887391b 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -14,7 +14,7 @@ // import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity' -import contact, { Employee, Person, pickPrimarySocialId, includesAny } from '@hcengineering/contact' +import contact, { Employee, Person, pickPrimarySocialId } from '@hcengineering/contact' import core, { PersonId, Blob, @@ -22,7 +22,6 @@ import core, { Data, Doc, generateId, - buildSocialIdString, parseSocialIdString, Hierarchy, Markup, @@ -37,7 +36,9 @@ import core, { TxRemoveDoc, TxUpdateDoc, Type, - type MeasureContext + type MeasureContext, + AccountUuid, + buildSocialIdString } from '@hcengineering/core' import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification' import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' @@ -79,7 +80,6 @@ export async function getPersonNotificationTxes ( originTx: TxCUD, notificationControl: NotificationProviderControl ): Promise { - // TODO: FIXME const receiver = reference.attachedTo as Ref const receiverSocialIds = await control.findAll(ctx, contact.class.SocialIdentity, { attachedTo: receiver }) const receiverSocialStrings = receiverSocialIds.map(buildSocialIdString) @@ -88,8 +88,19 @@ export async function getPersonNotificationTxes ( return [] } + const employee = await control.findAll( + ctx, + contact.mixin.Employee, + { _id: receiver as Ref, active: true }, + { limit: 1 } + ) + const account = employee[0]?.personUuid + if (account == null) { + return [] + } + const res: Tx[] = [] - const isAvailable = await checkSpace(receiverSocialStrings, space, control, res) + const isAvailable = await checkSpace(account, space, control, res) if (!isAvailable) { return [] @@ -97,16 +108,10 @@ export async function getPersonNotificationTxes ( const doc = (await control.findAll(ctx, reference.srcDocClass, { _id: reference.srcDocId }))[0] - const receiverPerson = ( - await control.findAll(ctx, contact.mixin.Employee, { _id: receiver as Ref, active: true }, { limit: 1 }) - )[0] - if (receiverPerson === undefined) return res - const receiverSpace = (await control.findAll(ctx, contact.class.PersonSpace, { person: receiver }, { limit: 1 }))[0] if (receiverSpace === undefined) return res - // TODO: Do we need for all or just one? - const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiverSocialStrings, doc) + const collaboratorsTx = await getCollaboratorsTxes(reference, control, account, doc) res.push(...collaboratorsTx) @@ -139,8 +144,6 @@ export async function getPersonNotificationTxes ( ) } - const receiverSocialString = pickPrimarySocialId(receiverSocialStrings) - // TODO: Select a proper reciever const data: Omit, 'docNotifyContext'> = { header: activity.string.MentionedYouIn, messageHtml: reference.message, @@ -148,7 +151,7 @@ export async function getPersonNotificationTxes ( mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass, objectId: reference.srcDocId, objectClass: reference.srcDocClass, - user: receiverSocialString, + user: account, isViewed: false, archived: false } @@ -163,9 +166,10 @@ export async function getPersonNotificationTxes ( ? (await control.findAll(ctx, contact.class.Person, { _id: senderSocialId.attachedTo }, { limit: 1 }))[0] : undefined + const receiverSocialString = pickPrimarySocialId(receiverSocialStrings) const receiverInfo = toReceiverInfo(control.hierarchy, { _id: receiverSocialString, - person: receiverPerson, + person: employee[0], space: receiverSpace._id, socialStrings: receiverSocialStrings }) @@ -185,6 +189,7 @@ export async function getPersonNotificationTxes ( ) const messageNotifyResult = await getMessageNotifyResult( reference, + account, receiverSocialStrings, control, originTx, @@ -220,7 +225,7 @@ export async function getPersonNotificationTxes ( await control.findAll( ctx, notification.class.DocNotifyContext, - { objectId: reference.srcDocId, user: { $in: receiverSocialStrings } }, + { objectId: reference.srcDocId, user: account }, { projection: { _id: 1 } } ) )[0] @@ -258,27 +263,20 @@ export async function getPersonNotificationTxes ( } async function checkSpace ( - personIds: PersonId[], + account: AccountUuid, spaceId: Ref, control: TriggerControl, res: Tx[] ): Promise { - if (personIds.length === 0) { - return false - } - const space = (await control.findAll(control.ctx, core.class.Space, { _id: spaceId }, { limit: 1 }))[0] - const ids = new Set(personIds) - const isMember = space.members.some((member) => ids.has(member)) + const isMember = space.members.includes(account) if (space.private) { return isMember } - const id = pickPrimarySocialId(personIds) - if (!isMember) { - res.push(control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: id } })) + res.push(control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: account } })) } return true @@ -287,7 +285,7 @@ async function checkSpace ( async function getCollaboratorsTxes ( reference: Data, control: TriggerControl, - receiverSocialStrings: PersonId[], + receiver: AccountUuid, object?: Doc ): Promise[]> { const { hierarchy } = control @@ -295,7 +293,7 @@ async function getCollaboratorsTxes ( if (object !== undefined) { // Add user to collaborators of object where user is mentioned - const objectTx = getPushCollaboratorTx(control, receiverSocialStrings, object) + const objectTx = getPushCollaboratorTx(control, receiver, object) if (objectTx !== undefined) { res.push(objectTx) @@ -326,7 +324,7 @@ async function getCollaboratorsTxes ( } // Add user to collaborators of message where user is mentioned - const messageTx = getPushCollaboratorTx(control, receiverSocialStrings, message) + const messageTx = getPushCollaboratorTx(control, receiver, message) if (messageTx !== undefined) { res.push(messageTx) @@ -337,6 +335,7 @@ async function getCollaboratorsTxes ( async function getMessageNotifyResult ( reference: Data, + account: AccountUuid, personIds: PersonId[], control: TriggerControl, tx: TxCUD, @@ -355,7 +354,7 @@ async function getMessageNotifyResult ( const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) - if (mixin === undefined || !includesAny(mixin.collaborators, personIds)) { + if (mixin === undefined || !mixin.collaborators.includes(account)) { return new Map() } diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts index d4ecd3720e..8a64dac1fb 100644 --- a/server-plugins/activity-resources/src/utils.ts +++ b/server-plugins/activity-resources/src/utils.ts @@ -1,6 +1,5 @@ import { ActivityMessageControl, DocAttributeUpdates, DocUpdateAction } from '@hcengineering/activity' import core, { - PersonId, AttachedDoc, type Attribute, Class, @@ -17,7 +16,8 @@ import core, { TxProcessor, TxUpdateDoc, combineAttributes, - ArrOf + ArrOf, + AccountUuid } from '@hcengineering/core' import notification from '@hcengineering/notification' import { translate } from '@hcengineering/platform' @@ -149,7 +149,7 @@ async function getCollaboratorsDiff ( const { hierarchy } = control const value = hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] - let prevValue: PersonId[] = [] + let prevValue: AccountUuid[] = [] if (prevDoc !== undefined && hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { prevValue = hierarchy.as(prevDoc, notification.mixin.Collaborators).collaborators ?? [] diff --git a/server-plugins/ai-bot-resources/src/index.ts b/server-plugins/ai-bot-resources/src/index.ts index 4182c12938..ea4e883482 100644 --- a/server-plugins/ai-bot-resources/src/index.ts +++ b/server-plugins/ai-bot-resources/src/index.ts @@ -15,7 +15,6 @@ import core, { Doc, - PersonUuid, systemAccountUuid, Tx, TxCreateDoc, @@ -25,7 +24,7 @@ import core, { UserStatus } from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' -import { getPerson, getPersons } from '@hcengineering/server-contact' +import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact' import { aiBotEmailSocialId, AIEventRequest } from '@hcengineering/ai-bot' import { createAccountRequest, hasAiEndpoint, sendAIEvents } from './utils' @@ -36,7 +35,9 @@ async function OnUserStatus (txes: TxCUD[], control: TriggerControl) return [] } - if (control.txFactory.account === aiBotEmailSocialId) { + const account = await getAccountBySocialId(control, aiBotEmailSocialId) + + if (control.ctx.contextData.account.uuid === account) { return [] } @@ -152,18 +153,17 @@ async function getMessageDoc (message: ChatMessage, control: TriggerControl): Pr async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise { const { members } = direct + const account = await getAccountBySocialId(control, aiBotEmailSocialId) - if (!members.includes(aiBotEmailSocialId)) { + if (account == null) { return false } - const persons = await getPersons(control, members) + if (!members.includes(account)) { + return false + } - const uuids = new Set( - persons.map((account) => account.personUuid).filter((uuid): uuid is PersonUuid => uuid !== undefined) - ) - - return uuids.size === 2 + return members.length === 2 } async function onBotDirectMessageSend (control: TriggerControl, message: ChatMessage): Promise { diff --git a/server-plugins/analytics-collector-resources/src/utils.ts b/server-plugins/analytics-collector-resources/src/utils.ts index 8ea0edca48..5bff931d59 100644 --- a/server-plugins/analytics-collector-resources/src/utils.ts +++ b/server-plugins/analytics-collector-resources/src/utils.ts @@ -14,7 +14,7 @@ // import chunter, { Channel } from '@hcengineering/chunter' import core, { MeasureContext, PersonId, Ref, TxOperations, type WorkspaceUuid } from '@hcengineering/core' -import { getAllEmployeesPrimarySocialStrings, getAllSocialStringsByPersonId, Person } from '@hcengineering/contact' +import { getAllUserAccounts, getAllSocialStringsByPersonId, Person } from '@hcengineering/contact' import analyticsCollector, { getOnboardingChannelName, OnboardingChannel } from '@hcengineering/analytics-collector' import { translate } from '@hcengineering/platform' @@ -31,6 +31,7 @@ export async function getOrCreateOnboardingChannel ( workspace: WorkspaceInfo, person?: Person ): Promise<[Ref | undefined, boolean]> { + // TODO: FIXME const personIds = await getAllSocialStringsByPersonId(client, socialString) const channel = await client.findOne(analyticsCollector.class.OnboardingChannel, { workspaceId: workspace.workspaceId, @@ -80,7 +81,7 @@ export async function createGeneralOnboardingChannel ( ctx.info('Creating general onboarding channel') - const primarySocialIds = await getAllEmployeesPrimarySocialStrings(client) + const userAccounts = await getAllUserAccounts(client) await client.createDoc( chunter.class.Channel, core.space.Space, @@ -89,7 +90,7 @@ export async function createGeneralOnboardingChannel ( topic: '', description: '', private: false, - members: primarySocialIds, + members: userAccounts, autoJoin: true, archived: false }, diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index d7b7e96c02..a5ebb84b54 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -16,7 +16,7 @@ import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity' import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter' import contact, { Person } from '@hcengineering/contact' -import { getPerson, getSocialStrings } from '@hcengineering/server-contact' +import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact' import core, { PersonId, Class, @@ -35,7 +35,8 @@ import core, { TxUpdateDoc, UserStatus, type MeasureContext, - combineAttributes + combineAttributes, + AccountUuid } from '@hcengineering/core' import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' @@ -173,10 +174,15 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro } const isChannel = hierarchy.isDerived(targetDoc._class, chunter.class.Channel) const res: Tx[] = [] + const account = await getAccountBySocialId(control, message.modifiedBy) + + if (account == null) { + return [] + } if (hierarchy.hasMixin(targetDoc, notification.mixin.Collaborators)) { const collaboratorsMixin = hierarchy.as(targetDoc, notification.mixin.Collaborators) - if (!collaboratorsMixin.collaborators.includes(message.modifiedBy)) { + if (!collaboratorsMixin.collaborators.includes(account)) { res.push( control.txFactory.createTxMixin( targetDoc._id, @@ -185,7 +191,7 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro notification.mixin.Collaborators, { $push: { - collaborators: message.modifiedBy + collaborators: account } } ) @@ -193,14 +199,14 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro } } else { const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control) - if (!collaborators.includes(message.modifiedBy)) { - collaborators.push(message.modifiedBy) + if (!collaborators.includes(account)) { + collaborators.push(account) } res.push(getMixinTx(tx, control, collaborators)) } - if (isChannel && !(targetDoc as Channel).members.includes(message.modifiedBy)) { - res.push(...joinChannel(control, targetDoc as Channel, message.modifiedBy)) + if (isChannel && !(targetDoc as Channel).members.includes(account)) { + res.push(...joinChannel(control, targetDoc as Channel, account)) } return res @@ -222,7 +228,7 @@ async function ChatNotificationsHandler (txes: TxCUD[], control: TriggerCon return result } -function joinChannel (control: TriggerControl, channel: Channel, user: PersonId): Tx[] { +function joinChannel (control: TriggerControl, channel: Channel, user: AccountUuid): Tx[] { if (channel.members.includes(user)) { return [] } @@ -410,9 +416,8 @@ export async function syncChat (control: TriggerControl, status: UserStatus, dat const shouldSync = syncInfo === undefined || date - syncInfo.timestamp > updateChatInfoDelay if (!shouldSync) return - const socialIds = await getSocialStrings(control, person._id) const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { - user: { $in: socialIds }, + user: status.user, hidden: false, isPinned: false }) diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index 0697fd9b58..cbb55722f4 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -26,13 +26,9 @@ import contact, { formatName, getFirstName, getLastName, - getName, - pickPrimarySocialId, - includesAny, - SocialIdentity + getName } from '@hcengineering/contact' import core, { - PersonId, Doc, Hierarchy, Ref, @@ -43,15 +39,13 @@ import core, { TxRemoveDoc, TxUpdateDoc, concatLink, - buildSocialIdString, type Space, SocialIdType, - TxProcessor, - TxCreateDoc + AccountUuid } from '@hcengineering/core' import notification, { Collaborators } from '@hcengineering/notification' import { getMetadata } from '@hcengineering/platform' -import { getTriggerCurrentPerson } from '@hcengineering/server-contact' +import { getAccountBySocialId, getTriggerCurrentPerson } from '@hcengineering/server-contact' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { workbenchId } from '@hcengineering/workbench' @@ -59,7 +53,7 @@ export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): const result: Tx[] = [] for (const tx of txes) { const ctx = tx as TxUpdateDoc - const newMember = ctx.operations.$push?.members as PersonId + const newMember = ctx.operations.$push?.members as AccountUuid if (newMember !== undefined) { const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) for (const space of spaces) { @@ -72,7 +66,7 @@ export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): result.push(pushTx) } } - const oldMember = ctx.operations.$pull?.members as PersonId + const oldMember = ctx.operations.$pull?.members as AccountUuid if (ctx.operations.$pull?.members !== undefined) { const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId }) for (const space of spaces) { @@ -89,71 +83,27 @@ export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): return result } -export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerControl): Promise { - const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true }) - if (spaces.length === 0) return [] - - const result: Tx[] = [] - for (const tx of _txes) { - const ctx = tx as TxCreateDoc - if (ctx._class !== core.class.TxCreateDoc) continue - - const socialId = TxProcessor.createDoc2Doc(ctx) - const employee = ( - await control.findAll(control.ctx, contact.mixin.Employee, { _id: socialId.attachedTo as Ref }) - )[0] - if (employee === undefined || !employee.active) continue - const socialString = buildSocialIdString(socialId) - const txes = await createPersonSpace(socialString, employee._id, control) - result.push(...txes) - - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { - attachedTo: socialId.attachedTo - }) - if (socialIds.every((si) => si._id !== socialId._id)) { - socialIds.push(socialId) - } - const socialStrings = socialIds.map(buildSocialIdString) - - for (const space of spaces) { - if (includesAny(space.members, socialStrings)) continue - - const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { - members: socialString - } - }) - result.push(pushTx) - space.members.push(socialString) - } - } - return result -} - export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] for (const tx of _txes) { const mixinTx = tx as TxMixin if (mixinTx.attributes.active !== true) continue - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { - attachedTo: mixinTx.objectId, - attachedToClass: contact.class.Person - }) - const socialStrings = socialIds.map(buildSocialIdString) - if (socialStrings.length === 0) continue - const socialId = pickPrimarySocialId(socialStrings) + const person = (await control.findAll(control.ctx, contact.class.Person, { _id: mixinTx.objectId }))[0] + const account = person?.personUuid as AccountUuid + if (account === undefined) continue + const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true }) - const txes = await createPersonSpace(socialId, mixinTx.objectId, control) + const txes = await createPersonSpace(account, mixinTx.objectId, control) result.push(...txes) for (const space of spaces) { - if (includesAny(space.members, socialStrings)) continue + if (space.members.includes(account)) continue const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { - members: socialId + members: account } }) result.push(pushTx) @@ -163,7 +113,7 @@ export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): P } async function createPersonSpace ( - socialId: PersonId, + account: AccountUuid, person: Ref, control: TriggerControl ): Promise[]> { @@ -177,7 +127,7 @@ async function createPersonSpace ( private: true, archived: false, person, - members: [socialId] + members: [account] }) ] } @@ -220,19 +170,21 @@ export async function OnContactDelete ( */ export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] + for (const tx of txes) { const uTx = tx as TxUpdateDoc if (uTx.operations.$inc?.items !== undefined) { const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0] - if (doc !== undefined) { + const account = await getAccountBySocialId(control, tx.modifiedBy) + if (doc !== undefined && account != null) { if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { const collab = control.hierarchy.as(doc, notification.mixin.Collaborators) as Doc as Collaborators - if (collab.collaborators.includes(tx.modifiedBy)) { + if (collab.collaborators.includes(account)) { result.push( control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { $push: { - collaborators: tx.modifiedBy + collaborators: account } }) ) @@ -244,7 +196,7 @@ export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Pro doc.space, notification.mixin.Collaborators, { - collaborators: [tx.modifiedBy] + collaborators: [account] } ) result.push(res) @@ -384,7 +336,6 @@ export async function getContactFirstName ( // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { - OnSocialIdentityCreate, OnEmployeeCreate, OnContactDelete, OnChannelUpdate, diff --git a/server-plugins/contact/src/index.ts b/server-plugins/contact/src/index.ts index f249704173..472558f6ff 100644 --- a/server-plugins/contact/src/index.ts +++ b/server-plugins/contact/src/index.ts @@ -34,7 +34,6 @@ export default plugin(serverContactId, { trigger: { OnContactDelete: '' as Resource, OnChannelUpdate: '' as Resource, - OnSocialIdentityCreate: '' as Resource, OnEmployeeCreate: '' as Resource, OnSpaceTypeMembers: '' as Resource }, diff --git a/server-plugins/contact/src/utils.ts b/server-plugins/contact/src/utils.ts index c6bc1c31eb..162e44832c 100644 --- a/server-plugins/contact/src/utils.ts +++ b/server-plugins/contact/src/utils.ts @@ -14,8 +14,8 @@ // import { TriggerControl } from '@hcengineering/server-core' -import contact, { Employee, type Person } from '@hcengineering/contact' -import { parseSocialIdString, PersonId, type Ref, toIdMap } from '@hcengineering/core' +import contact, { Employee, pickPrimarySocialId, type Person } from '@hcengineering/contact' +import { AccountUuid, parseSocialIdString, PersonId, type Ref, toIdMap } from '@hcengineering/core' export async function getTriggerCurrentPerson (control: TriggerControl): Promise { const { type, value } = parseSocialIdString(control.txFactory.account) @@ -88,25 +88,16 @@ export async function getPerson (control: TriggerControl, personId: PersonId): P return (await control.findAll(control.ctx, contact.class.Person, { _id: socialId.attachedTo }))[0] } -export async function getPersons (control: TriggerControl, personIds: PersonId[]): Promise { - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } }) - const persons = await control.findAll(control.ctx, contact.class.Person, { - _id: { $in: socialIds.map((s) => s.attachedTo) } - }) - - return persons -} - export async function getPersonsBySocialIds ( control: TriggerControl, personIds: PersonId[] -): Promise> { +): Promise> { const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } }) const persons = toIdMap( await control.findAll(control.ctx, contact.class.Person, { _id: { $in: socialIds.map((s) => s.attachedTo) } }) ) - return socialIds.reduce>((acc, s) => { + return socialIds.reduce>((acc, s) => { const person = persons.get(s.attachedTo) if (person !== undefined) { acc[s.key] = person @@ -121,16 +112,24 @@ export async function getPersonsBySocialIds ( export async function getEmployee (control: TriggerControl, personId: PersonId): Promise { const socialId = (await control.findAll(control.ctx, contact.class.SocialIdentity, { key: personId }))[0] const employee = ( - await control.findAll(control.ctx, contact.mixin.Employee, { _id: socialId.attachedTo as Ref }) + await control.findAll( + control.ctx, + contact.mixin.Employee, + { _id: socialId.attachedTo as Ref }, + { limit: 1 } + ) )[0] return employee } -export async function getEmployees (control: TriggerControl, personIds: PersonId[]): Promise { - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } }) +export async function getEmployeeByAcc (control: TriggerControl, account: AccountUuid): Promise { + return (await control.findAll(control.ctx, contact.mixin.Employee, { personUuid: account }, { limit: 1 }))[0] +} + +export async function getEmployees (control: TriggerControl, accounts: AccountUuid[]): Promise { const employees = await control.findAll(control.ctx, contact.mixin.Employee, { - _id: { $in: socialIds.map((s) => s.attachedTo as Ref) } + personUuid: { $in: accounts } }) return employees @@ -153,3 +152,63 @@ export async function getEmployeesBySocialIds ( return acc }, {}) } + +export async function getSocialIdsByAccounts ( + control: TriggerControl, + accounts: AccountUuid[] +): Promise> { + const employeesMap = toIdMap( + await control.findAll(control.ctx, contact.mixin.Employee, { personUuid: { $in: accounts } }) + ) + const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { + attachedTo: { $in: Array.from(employeesMap.keys()) }, + attachedToClass: contact.class.Person + }) + + return socialIds.reduce>((acc, sid) => { + const employee = employeesMap.get(sid.attachedTo as Ref) + if (employee?.personUuid === undefined) return acc + + if (acc[employee.personUuid] === undefined) { + acc[employee.personUuid] = [] + } + + acc[employee.personUuid].push(sid.key) + return acc + }, {}) +} + +export async function getPrimarySocialIdsByAccounts ( + control: TriggerControl, + accounts: AccountUuid[] +): Promise> { + return Object.entries(await getSocialIdsByAccounts(control, accounts)).reduce>( + (acc, [account, sids]) => { + acc[account as AccountUuid] = pickPrimarySocialId(sids) + return acc + }, + {} + ) +} + +export async function getAccountBySocialId (control: TriggerControl, socialId: PersonId): Promise { + const socialIdentity = await control.findAll( + control.ctx, + contact.class.SocialIdentity, + { key: socialId }, + { limit: 1 } + ) + + if (socialIdentity.length === 0) { + return null + } + + const employee = await control.findAll( + control.ctx, + contact.mixin.Employee, + { _id: socialIdentity[0].attachedTo as Ref }, + { limit: 1 } + ) + + return employee[0]?.personUuid ?? null +} diff --git a/server-plugins/controlled-documents-resources/src/index.ts b/server-plugins/controlled-documents-resources/src/index.ts index 764c81b452..2fb3a9b996 100644 --- a/server-plugins/controlled-documents-resources/src/index.ts +++ b/server-plugins/controlled-documents-resources/src/index.ts @@ -17,7 +17,8 @@ import core, { type Doc, type RolesAssignment, type Timestamp, - type TxCUD + type TxCUD, + systemAccountUuid } from '@hcengineering/core' import { NotificationType } from '@hcengineering/notification' import { getEmployees, getSocialStrings, getSocialStringsByPersons } from '@hcengineering/server-contact' @@ -167,8 +168,8 @@ async function createDocumentTrainingRequest (doc: ControlledDocument, control: } const mixin = control.hierarchy.as(space, spaceType.targetClass) as unknown as RolesAssignment - const personIds = roles.map((roleId) => mixin[roleId] ?? []).flat() - const employees = await getEmployees(control, personIds) + const accounts = roles.map((roleId) => mixin[roleId] ?? []).flat() + const employees = await getEmployees(control, accounts) for (const employee of employees) { traineesMap.set(employee._id, true) @@ -286,7 +287,7 @@ export async function OnDocHasBecomeEffective ( return result } -export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerControl): Promise { +export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise { // Fill owner of default space with the very first owner account creating a social identity const account = control.ctx.contextData.account if (account.role !== AccountRole.Owner) return [] @@ -299,9 +300,9 @@ export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerContr const owners = defaultSpace.owners ?? [] - if (owners.length === 0 || (owners.length === 1 && owners[0] === core.account.System)) { + if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) { const setOwnerTx = control.txFactory.createTxUpdateDoc(defaultSpace._class, defaultSpace.space, defaultSpace._id, { - owners: [account.primarySocialId] + owners: [account.uuid] }) return [setOwnerTx] @@ -418,7 +419,7 @@ async function CoAuthorsTypeMatch ( // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { - OnSocialIdentityCreate, + OnEmployeeCreate, OnDocDeleted, OnDocPlannedEffectiveDateChanged, OnDocApprovalRequestApproved, diff --git a/server-plugins/controlled-documents/src/index.ts b/server-plugins/controlled-documents/src/index.ts index 607fba0c52..260a1ba70b 100644 --- a/server-plugins/controlled-documents/src/index.ts +++ b/server-plugins/controlled-documents/src/index.ts @@ -18,7 +18,7 @@ export const serverDocumentsId = 'server-documents' as Plugin */ export default plugin(serverDocumentsId, { trigger: { - OnSocialIdentityCreate: '' as Resource, + OnEmployeeCreate: '' as Resource, OnDocDeleted: '' as Resource, OnDocPlannedEffectiveDateChanged: '' as Resource, OnDocApprovalRequestApproved: '' as Resource, diff --git a/server-plugins/lead-resources/src/index.ts b/server-plugins/lead-resources/src/index.ts index 16c42e3d15..5ccfe7dbbf 100644 --- a/server-plugins/lead-resources/src/index.ts +++ b/server-plugins/lead-resources/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import core, { AccountRole, concatLink, Doc, Tx } from '@hcengineering/core' +import { AccountRole, concatLink, Doc, systemAccountUuid, Tx } from '@hcengineering/core' import lead, { Lead, leadId } from '@hcengineering/lead' import { getMetadata } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' @@ -31,7 +31,7 @@ export async function leadHTMLPresenter (doc: Doc, control: TriggerControl): Pro return `${lead.title}` } -export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerControl): Promise { +export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise { // Fill owner of default space with the very first owner account creating a social identity const account = control.ctx.contextData.account if (account.role !== AccountRole.Owner) return [] @@ -42,9 +42,9 @@ export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerContr const owners = defaultSpace.owners ?? [] - if (owners.length === 0 || (owners.length === 1 && owners[0] === core.account.System)) { + if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) { const setOwnerTx = control.txFactory.createTxUpdateDoc(defaultSpace._class, defaultSpace.space, defaultSpace._id, { - owners: [account.primarySocialId] + owners: [account.uuid] }) return [setOwnerTx] @@ -68,6 +68,6 @@ export default async () => ({ LeadTextPresenter: leadTextPresenter }, trigger: { - OnSocialIdentityCreate + OnEmployeeCreate } }) diff --git a/server-plugins/lead/src/index.ts b/server-plugins/lead/src/index.ts index 9a89d566b7..6ec0c508ed 100644 --- a/server-plugins/lead/src/index.ts +++ b/server-plugins/lead/src/index.ts @@ -32,6 +32,6 @@ export default plugin(serverLeadId, { LeadTextPresenter: '' as Resource }, trigger: { - OnSocialIdentityCreate: '' as Resource + OnEmployeeCreate: '' as Resource } }) diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index c0b5da4494..a59357d54a 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -27,7 +27,8 @@ import core, { TxUpdateDoc, UserStatus, combineAttributes, - type PersonUuid + type PersonUuid, + type AccountUuid } from '@hcengineering/core' import love, { Invite, @@ -125,7 +126,7 @@ async function createUserInfo (user: PersonUuid, control: TriggerControl): Promi return [ptx] } -async function removeUserInfo (user: PersonUuid, control: TriggerControl): Promise { +async function removeUserInfo (user: AccountUuid, control: TriggerControl): Promise { const person = (await control.findAll(control.ctx, contact.class.Person, { personUuid: user }))[0] if (person === undefined) return [] @@ -331,11 +332,19 @@ export async function OnKnock (txes: Tx[], control: TriggerControl): Promise }, + { limit: 1 } + ) + const account = employee[0]?.personUuid + if (account === undefined) continue const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: { $in: socialStrings } + user: account }) - await createPushNotification(control, socialStrings, title, body, request._id, subscriptions, from, path) + await createPushNotification(control, account, title, body, request._id, subscriptions, from, path) } } } @@ -352,7 +361,12 @@ export async function OnInvite (txes: Tx[], control: TriggerControl): Promise }) + await control.findAll( + control.ctx, + contact.mixin.Employee, + { _id: invite.target as Ref }, + { limit: 1 } + ) )[0] if (target === undefined) { continue @@ -381,10 +395,12 @@ export async function OnInvite (txes: Tx[], control: TriggerControl): Promise | undefined { const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) - if (mixin.collaborators === undefined || !includesAny(mixin.collaborators, socialStrings)) { + if (mixin.collaborators === undefined || !mixin.collaborators.includes(account)) { return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { $push: { - collaborators: pickPrimarySocialId(socialStrings) + collaborators: account } }) } @@ -257,45 +265,51 @@ export async function getContentByTemplate ( } } -async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise { +async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise { const hierarchy = control.hierarchy if (attr.type._class === core.class.RefTo) { const to = (attr.type as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { - attachedTo: value, - attachedToClass: contact.class.Person - }) + const employee = await control.findAll( + control.ctx, + contact.mixin.Employee, + { + _id: value, + active: true + }, + { limit: 1 } + ) - return [pickPrimarySocialId(socialIds.map((it) => it.key))] + return employee[0]?.personUuid != null ? [employee[0].personUuid] : [] } - } else if (attr.type._class === core.class.TypePersonId) { + } else if (attr.type._class === core.class.TypeAccountUuid) { return [value] + } else if (attr.type._class === core.class.TypePersonId) { + const acc = await getAccountBySocialId(control, value) + return acc == null ? [] : [acc] } else if (attr.type._class === core.class.ArrOf) { const arrOf = (attr.type as ArrOf>).of if (arrOf._class === core.class.RefTo) { const to = (arrOf as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { - const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { - attachedTo: { $in: value }, - attachedToClass: contact.class.Person + const employees = await control.findAll(control.ctx, contact.mixin.Employee, { + _id: { $in: value }, + active: true }) - const byPerson = socialIds.reduce, PersonId[]>>((map, it) => { - if (map[it.attachedTo] === undefined) { - map[it.attachedTo] = [] - } - - map[it.attachedTo].push(it.key) - return map - }, {}) - - return Object.values(byPerson).map((socialStrings) => pickPrimarySocialId(socialStrings)) + return employees.map((e) => e.personUuid).filter(notEmpty) } - } else if (arrOf._class === core.class.TypePersonId) { + } else if (arrOf._class === core.class.TypeAccountUuid) { return Array.isArray(value) ? value : [value] + } else if (arrOf._class === core.class.TypePersonId) { + const socialIds = Array.isArray(value) ? value : [value] + const personsBySocialIds = await getEmployeesBySocialIds(control, socialIds) + + return Object.values(personsBySocialIds) + .map((p) => p?.personUuid) + .filter(notEmpty) } } return [] @@ -306,7 +320,7 @@ async function getKeyCollaborators ( value: any, field: string, control: TriggerControl -): Promise { +): Promise { if (value !== undefined && value !== null) { const attr = control.hierarchy.findAttribute(docClass, field) if (attr !== undefined) { @@ -324,8 +338,8 @@ export async function getDocCollaborators ( doc: Doc, mixin: ClassCollaborators, control: TriggerControl -): Promise { - const collaborators = new Set() +): Promise { + const collaborators = new Set() for (const field of mixin.fields) { const value = (doc as any)[field] const newCollaborators = await ctx.with('getKeyCollaborators', {}, (ctx) => @@ -356,7 +370,7 @@ export async function pushInboxNotifications ( shouldUpdateTimestamp = true, tx?: TxCUD ): Promise | undefined> { - const context = getDocNotifyContext(control, contexts, objectId, receiver._id) + const context = getDocNotifyContext(control, contexts, objectId, receiver.account) let docNotifyContextId: Ref if (context === undefined) { @@ -376,7 +390,7 @@ export async function pushInboxNotifications ( } const notificationData = { - user: receiver._id, + user: receiver.account, isViewed: false, docNotifyContext: docNotifyContextId, archived: false, @@ -580,7 +594,7 @@ async function createNotifyContext ( } const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, { - user: receiver._id, + user: receiver.account, objectId, objectClass, objectSpace, @@ -675,7 +689,7 @@ export async function getNotificationTxes ( ) } } else { - const context = getDocNotifyContext(control, docNotifyContexts, message.attachedTo, receiver._id) + const context = getDocNotifyContext(control, docNotifyContexts, message.attachedTo, receiver.account) if (context === undefined) { await createNotifyContext( @@ -704,14 +718,19 @@ async function updateContextsTimestamp ( ): Promise { if (contexts.length === 0) return const res: Tx[] = [] + const socialIdsByAccounts = await getSocialIdsByAccounts( + control, + contexts.map((it) => it.user) + ) for (const context of contexts) { const isViewed = context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp + const ctxUserSocialIds = socialIdsByAccounts[context.user] ?? [] const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false, lastUpdateTimestamp: timestamp, - ...(isViewed && modifiedBy === context.user + ...(isViewed && ctxUserSocialIds.includes(modifiedBy) ? { lastViewedTimestamp: timestamp } @@ -720,12 +739,9 @@ async function updateContextsTimestamp ( res.push(updateTx) - const personUuid = (await getPerson(control, context.user))?.personUuid - if (personUuid !== undefined) { - control.ctx.contextData.broadcast.targets['docNotifyContext' + updateTx._id] = (it) => { - if (it._id === updateTx._id) { - return [personUuid] - } + control.ctx.contextData.broadcast.targets['docNotifyContext' + updateTx._id] = (it) => { + if (it._id === updateTx._id) { + return [context.user] } } } @@ -738,7 +754,7 @@ async function updateContextsTimestamp ( async function removeContexts ( ctx: MeasureContext, contexts: DocNotifyContext[], - unsubscribe: PersonId[], + unsubscribe: AccountUuid[], control: TriggerControl ): Promise { if (contexts.length === 0) return @@ -755,12 +771,9 @@ async function removeContexts ( res.push(removeTx) - const personUuid = (await getPerson(control, context.user))?.personUuid - if (personUuid !== undefined) { - control.ctx.contextData.broadcast.targets['docNotifyContext' + removeTx._id] = (it) => { - if (it._id === removeTx._id) { - return [personUuid] - } + control.ctx.contextData.broadcast.targets['docNotifyContext' + removeTx._id] = (it) => { + if (it._id === removeTx._id) { + return [context.user] } } } @@ -771,13 +784,13 @@ async function removeContexts ( export async function createCollabDocInfo ( ctx: MeasureContext, currentRes: Tx[], - collaborators: PersonId[], + collaborators: AccountUuid[], control: TriggerControl, tx: TxCUD, object: Doc, activityMessages: ActivityMessage[], params: NotifyParams, - unsubscribe: PersonId[] = [], + unsubscribe: AccountUuid[] = [], cache = new Map, Doc>() ): Promise { let res: Tx[] = [] @@ -843,20 +856,21 @@ export async function createCollabDocInfo ( const targets = new Set(filteredCollaborators) // user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc) - if (control.hierarchy.isDerived(object._class, contact.class.Person)) { - const socialStrings = await getSocialStrings(control, object._id as Ref) + if (control.hierarchy.isDerived(object._class, contact.mixin.Employee)) { + const account = (object as Employee).personUuid - if (socialStrings.length > 0) { - targets.add(pickPrimarySocialId(socialStrings)) + if (account != null) { + targets.add(account) } } if (targets.size === 0) { return res } + const targetPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, Array.from(targets)) const usersInfo = await ctx.with('get-user-info', {}, (ctx) => - getUsersInfo(ctx, [...Array.from(targets), tx.modifiedBy], control) + getUsersInfo(ctx, [...Object.values(targetPrimarySocialStringsByAccounts), tx.modifiedBy], control) ) const sender: SenderInfo = usersInfo.get(tx.modifiedBy) ?? { _id: tx.modifiedBy, @@ -865,7 +879,8 @@ export async function createCollabDocInfo ( const settings = await getNotificationProviderControl(ctx, control) for (const target of targets) { - const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) + const targetSocialString = targetPrimarySocialStringsByAccounts[target] + const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(targetSocialString)) if (info === undefined) continue @@ -902,7 +917,7 @@ export async function createCollabDocInfo ( export function getMixinTx ( actualTx: TxCUD, control: TriggerControl, - collaborators: PersonId[] + collaborators: AccountUuid[] ): TxMixin { return control.txFactory.createTxMixin( actualTx.objectId, @@ -921,9 +936,9 @@ async function getTxCollabs ( control: TriggerControl, doc: Doc ): Promise<{ - added: PersonId[] - removed: PersonId[] - result: PersonId[] + added: AccountUuid[] + removed: AccountUuid[] + result: AccountUuid[] }> { const { hierarchy } = control const mixin = hierarchy.classHierarchyMixin( @@ -1003,7 +1018,7 @@ async function getSpaceCollabTxes ( async function pushCollaboratorsToPublicSpace ( control: TriggerControl, doc: Doc, - collaborators: PersonId[], + collaborators: AccountUuid[], cache: Map, Doc> ): Promise { const space = await getObjectSpace(control, doc, cache) @@ -1105,9 +1120,9 @@ async function updateCollaboratorsMixin ( objectId: tx.objectId }) const prevDoc = TxProcessor.buildDoc2Doc([createTx, ...mixinTxes].filter((t) => t._id !== tx._id)) as Doc - const newCollabs: PersonId[] = [] + const newCollabs: AccountUuid[] = [] - let prevCollabs: Set + let prevCollabs: Set if (hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { const prevDocMixin = control.hierarchy.as(prevDoc, notification.mixin.Collaborators) @@ -1126,12 +1141,13 @@ async function updateCollaboratorsMixin ( } const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) + const modifiedByAccount = await getAccountBySocialId(control, tx.modifiedBy) + const socialIdsByAccounts = await getSocialIdsByAccounts(control, tx.attributes.collaborators) for (const collab of tx.attributes.collaborators) { - if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { + if (!prevCollabs.has(collab) && modifiedByAccount !== collab) { for (const provider of providers) { - const socialStrings = await getAllSocialStringsByPersonId(control, [collab]) - if (isAllowed(control, socialStrings, type, provider, notificationControl)) { + if (isAllowed(control, socialIdsByAccounts[collab], type, provider, notificationControl)) { newCollabs.push(collab) break } @@ -1147,21 +1163,21 @@ async function updateCollaboratorsMixin ( cache.set(object._id, object) cache.set(space._id, space) - const allStringsNewCollabs = await getAllSocialStringsByPersonId(control, newCollabs) const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { - user: { $in: allStringsNewCollabs }, + user: { $in: newCollabs }, objectId: tx.objectId }) + const newCollabsPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, newCollabs) const infos = await ctx.with('get-user-info', {}, (ctx) => - getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy], control) + getUsersInfo(ctx, [...Object.values(newCollabsPrimarySocialStringsByAccounts), originTx.modifiedBy], control) ) const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy, socialStrings: [] } for (const collab of newCollabs) { - const target = toReceiverInfo(hierarchy, infos.get(collab)) + const target = toReceiverInfo(hierarchy, infos.get(newCollabsPrimarySocialStringsByAccounts[collab])) if (target === undefined) continue - const isMember = includesAny(space.members, target.socialStrings) + const isMember = space.members.includes(collab) if (space.private && !isMember) continue if (!hierarchy.isDerived(space._class, core.class.SystemSpace) && !isMember) { @@ -1322,8 +1338,8 @@ async function getNewCollaborators ( mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl -): Promise { - const newCollaborators = new Set() +): Promise { + const newCollaborators = new Set() if (ops.$push !== undefined) { for (const key in ops.$push) { if (mixin.fields.includes(key)) { @@ -1352,7 +1368,8 @@ async function getNewCollaborators ( } } } - return Array.from(newCollaborators.values()) + + return Array.from(newCollaborators) } async function getRemovedMembers ( @@ -1360,8 +1377,8 @@ async function getRemovedMembers ( mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl -): Promise { - const removedCollaborators: PersonId[] = [] +): Promise { + const removedCollaborators: AccountUuid[] = [] if (ops.$pull !== undefined && 'members' in ops.$pull) { const key = 'members' if (mixin.fields.includes(key)) { @@ -1376,7 +1393,7 @@ async function getRemovedMembers ( } } - return Array.from(new Set(removedCollaborators).values()) + return Array.from(new Set(removedCollaborators)) } async function updateCollaboratorDoc ( @@ -1523,9 +1540,9 @@ async function applyUserTxes ( ctx: MeasureContext, control: TriggerControl, txes: Tx[], - cache: Map = new Map() + cache: Map = new Map() ): Promise { - const map: Map = new Map() + const map: Map = new Map() const res: Tx[] = [] for (const tx of txes) { @@ -1558,7 +1575,7 @@ async function applyUserTxes ( } for (const [user, txs] of map.entries()) { - const person = (cache.get(user) as Person) ?? (await getPerson(control, user)) + const person = (cache.get(user) as Person) ?? (await getEmployeeByAcc(control, user)) const personUuid = person?.personUuid if (personUuid !== undefined) { @@ -1606,12 +1623,7 @@ async function updateCollaborators ( if (doc === undefined) return [] const res: Tx[] = [] - const currentCollaborators = new Set( - await getAllSocialStringsByPersonId( - control, - hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] - ) - ) + const currentCollaborators = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators) const toAdd = addedCollaborators.filter((p) => !currentCollaborators.has(p)) if (toAdd.length === 0 && removedCollaborators.length === 0) return [] @@ -1628,12 +1640,13 @@ async function updateCollaborators ( if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId }) - const addedInfo = await getUsersInfo(ctx, toAdd, control) + const toAddPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, toAdd) + const addedInfo = await getUsersInfo(ctx, Object.values(toAddPrimarySocialStringsByAccounts), control) for (const addedUser of addedInfo.values()) { const info = toReceiverInfo(hierarchy, addedUser) if (info === undefined) continue - const context = getDocNotifyContext(control, contexts, objectId, info._id) + const context = getDocNotifyContext(control, contexts, objectId, info.account) if (context !== undefined) { if (context.hidden) { res.push(control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false })) @@ -1727,7 +1740,7 @@ export async function getCollaborators ( control: TriggerControl, tx: TxCUD, res: Tx[] -): Promise { +): Promise { const mixin = control.hierarchy.classHierarchyMixin(doc._class, notification.mixin.ClassCollaborators) if (mixin === undefined) { @@ -1748,7 +1761,7 @@ function getDocNotifyContext ( control: TriggerControl, contexts: DocNotifyContext[], objectId: Ref, - user: PersonId + user: AccountUuid ): DocNotifyContext | undefined { const context = contexts.find((it) => it.objectId === objectId && it.user === user) @@ -1814,18 +1827,15 @@ async function OnEmployeeDeactivate (txes: TxCUD[], control: TriggerControl if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) { return [] } - - const socialStrings = await getSocialStrings(control, ctx.objectId) - if (socialStrings.length === 0) return [] + const person = (await control.findAll(control.ctx, contact.class.Person, { _id: ctx.objectId }))[0] + if (person?.personUuid === undefined) return [] const res: Tx[] = [] - for (const socialString of socialStrings) { - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: socialString - }) - for (const sub of subscriptions) { - res.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id)) - } + const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { + user: person.personUuid as AccountUuid + }) + for (const sub of subscriptions) { + res.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id)) } } return result diff --git a/server-plugins/notification-resources/src/push.ts b/server-plugins/notification-resources/src/push.ts index 19f6d11b06..b84c976f43 100644 --- a/server-plugins/notification-resources/src/push.ts +++ b/server-plugins/notification-resources/src/push.ts @@ -16,12 +16,12 @@ import serverCore, { TriggerControl } from '@hcengineering/server-core' import serverNotification, { PUSH_NOTIFICATION_TITLE_SIZE } from '@hcengineering/server-notification' import { + AccountUuid, Class, concatLink, Data, Doc, Hierarchy, - PersonId, Ref, Tx, TxCreateDoc, @@ -44,17 +44,16 @@ import contact, { type AvatarInfo, getAvatarProviderId, getGravatarUrl, - pickPrimarySocialId, Person, PersonSpace } from '@hcengineering/contact' import { AvailableProvidersCache, AvailableProvidersCacheKey, getTranslatedNotificationContent } from './index' -import { getAllSocialStringsByPersonId, getPerson } from '@hcengineering/server-contact' +import { getPerson } from '@hcengineering/server-contact' async function createPushFromInbox ( control: TriggerControl, n: InboxNotification, - receiver: PersonId[], + receiver: AccountUuid, receiverSpace: Ref, subscriptions: PushSubscription[], senderPerson?: Person @@ -91,9 +90,8 @@ async function createPushFromInbox ( await createPushNotification(control, receiver, title, body, n._id, subscriptions, senderPerson, path) const messageInfo = getMessageInfo(n, control.hierarchy) - const primarySocialString = pickPrimarySocialId(receiver) return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiverSpace, { - user: primarySocialString, + user: receiver, title, body, senderId: n.createdBy ?? n.modifiedBy, @@ -149,7 +147,7 @@ function getMessageInfo ( export async function createPushNotification ( control: TriggerControl, - target: PersonId[], + target: AccountUuid, title: string, body: string, _id: string, @@ -161,7 +159,7 @@ export async function createPushNotification ( // TODO: Remove auth token after migration to new services const authToken: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken) if (pushURL === undefined || pushURL === '') return - const userSubscriptions = subscriptions.filter((it) => target.includes(it.user)) + const userSubscriptions = subscriptions.filter((it) => it.user === target) const data: PushData = { title, body @@ -194,7 +192,7 @@ async function sendPushToSubscription ( pushURL: string, sesAuth: string | undefined, control: TriggerControl, - targetUser: PersonId[], + targetUser: AccountUuid, subscriptions: PushSubscription[], data: PushData ): Promise { @@ -256,8 +254,7 @@ export async function PushNotificationsHandler ( for (const inboxNotification of all) { const { user } = inboxNotification - const userSocialStrings = await getAllSocialStringsByPersonId(control, [user]) - const userSubscriptions = subscriptions.filter((it) => userSocialStrings.includes(it.user)) + const userSubscriptions = subscriptions.filter((it) => it.user === user) if (userSubscriptions.length === 0) continue const senderSocialString = inboxNotification.createdBy ?? inboxNotification.modifiedBy @@ -265,7 +262,7 @@ export async function PushNotificationsHandler ( const tx = await createPushFromInbox( control, inboxNotification, - userSocialStrings, + user, inboxNotification.space, userSubscriptions, senderPerson diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index ff483c7116..1428fc4564 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -33,7 +33,8 @@ import core, { TxCUD, TxMixin, TxUpdateDoc, - type MeasureContext + type MeasureContext, + AccountUuid } from '@hcengineering/core' import notification, { BaseNotificationType, @@ -494,6 +495,7 @@ export async function getUsersInfo ( person, socialStrings, space: space?._id, + account: employee?.personUuid, employee } ] @@ -511,13 +513,14 @@ export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | Receiv if (!isEmployee) return undefined const employee = hierarchy.as(info.person, contact.mixin.Employee) - if (!employee.active) return undefined + if (!employee.active || employee.personUuid == null) return undefined return { _id: info._id, person: employee, space: info.space, socialStrings: info.socialStrings, + account: employee.personUuid, employee } } @@ -527,7 +530,7 @@ export function createPushCollaboratorsTx ( objectId: Ref, objectClass: Ref>, space: Ref, - collaborators: PersonId[] + collaborators: AccountUuid[] ): TxMixin { return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { $push: { @@ -544,7 +547,7 @@ export function createPullCollaboratorsTx ( objectId: Ref, objectClass: Ref>, space: Ref, - collaborators: PersonId[] + collaborators: AccountUuid[] ): TxMixin { return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { $pull: { collaborators: { $in: collaborators } } diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 1ba795ddc7..a182c9f2cb 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -16,7 +16,7 @@ import { ActivityMessage } from '@hcengineering/activity' import { Employee, Person, PersonSpace } from '@hcengineering/contact' -import { PersonId, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' +import { PersonId, Class, Doc, Mixin, Ref, Tx, TxCUD, AccountUuid } from '@hcengineering/core' import { BaseNotificationType, InboxNotification, @@ -96,6 +96,7 @@ export interface ReceiverInfo { socialStrings: PersonId[] space: Ref + account: AccountUuid employee: Employee } diff --git a/server-plugins/request-resources/package.json b/server-plugins/request-resources/package.json index 1d3954fe10..a904b915f3 100644 --- a/server-plugins/request-resources/package.json +++ b/server-plugins/request-resources/package.json @@ -41,6 +41,7 @@ "@hcengineering/platform": "^0.6.11", "@hcengineering/server-core": "^0.6.1", "@hcengineering/server-request": "^0.6.0", + "@hcengineering/server-contact": "^0.6.1", "@hcengineering/request": "^0.6.14", "@hcengineering/view": "^0.6.13", "@hcengineering/contact": "^0.6.24", diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index 9d88188a20..c750af0124 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -16,6 +16,7 @@ import { DocUpdateMessage } from '@hcengineering/activity' import core, { Doc, Tx, TxCUD, TxCreateDoc, TxProcessor, TxUpdateDoc, type MeasureContext } from '@hcengineering/core' import notification from '@hcengineering/notification' +import { getPrimarySocialIdsByAccounts } from '@hcengineering/server-contact' import { getResource, translate } from '@hcengineering/platform' import request, { Request, RequestStatus } from '@hcengineering/request' import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources' @@ -145,7 +146,15 @@ async function getRequestNotificationTx ( const notifyContexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId: doc._id }) - const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy], control) + const collaboratorsPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts( + control, + Array.from(collaborators) + ) + const usersInfo = await getUsersInfo( + control.ctx, + [...Object.values(collaboratorsPrimarySocialStringsByAccounts), tx.modifiedBy], + control + ) const senderInfo = usersInfo.get(tx.modifiedBy) ?? { _id: tx.modifiedBy, socialStrings: [] @@ -154,7 +163,10 @@ async function getRequestNotificationTx ( const notificationControl = await getNotificationProviderControl(ctx, control) for (const target of collaborators) { - const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target)) + const targetInfo = toReceiverInfo( + control.hierarchy, + usersInfo.get(collaboratorsPrimarySocialStringsByAccounts[target]) + ) if (targetInfo === undefined) continue const txes = await getNotificationTxes( diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index fe80b0496f..8308676b69 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -32,8 +32,7 @@ import core, { TxProcessor, TxUpdateDoc, toIdMap, - Space, - includesAny + Space } from '@hcengineering/core' import notification, { CommonInboxNotification } from '@hcengineering/notification' import { getResource } from '@hcengineering/platform' @@ -272,7 +271,7 @@ export async function OnToDoCreate (txes: TxCUD[], control: TriggerControl) if ( !hierarchy.isDerived(objectSpace._class, core.class.SystemSpace) && - !includesAny(objectSpace.members, currentAcc.socialIds) + !objectSpace.members.includes(currentAcc.uuid) ) { continue } @@ -298,6 +297,11 @@ export async function OnToDoCreate (txes: TxCUD[], control: TriggerControl) const socialStrings = await getSocialStrings(control, employee._id) const primarySocialString = pickPrimarySocialId(socialStrings) + const account = employee.personUuid + + if (account == null) { + continue + } // TODO: Select a proper account const receiverInfo: ReceiverInfo = { @@ -306,6 +310,7 @@ export async function OnToDoCreate (txes: TxCUD[], control: TriggerControl) socialStrings, employee, + account, space: personSpace._id } diff --git a/server-plugins/tracker-resources/src/index.ts b/server-plugins/tracker-resources/src/index.ts index ce5d0bc5aa..4b5d0948a1 100644 --- a/server-plugins/tracker-resources/src/index.ts +++ b/server-plugins/tracker-resources/src/index.ts @@ -23,6 +23,7 @@ import core, { PersonId, Ref, Space, + systemAccountUuid, Tx, TxCreateDoc, TxCUD, @@ -159,7 +160,7 @@ export async function getIssueNotificationContent ( } } -export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerControl): Promise { +export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise { // Fill owner of default space with the very first owner account creating a social identity const account = control.ctx.contextData.account if (account.role !== AccountRole.Owner) return [] @@ -172,9 +173,9 @@ export async function OnSocialIdentityCreate (_txes: Tx[], control: TriggerContr const owners = defaultSpace.owners ?? [] - if (owners.length === 0 || (owners.length === 1 && owners[0] === core.account.System)) { + if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) { const setOwnerTx = control.txFactory.createTxUpdateDoc(defaultSpace._class, defaultSpace.space, defaultSpace._id, { - owners: [account.primarySocialId] + owners: [account.uuid] }) return [setOwnerTx] @@ -543,7 +544,7 @@ export default async () => ({ IssueLinkIdProvider: issueLinkIdProvider }, trigger: { - OnSocialIdentityCreate, + OnEmployeeCreate, OnIssueUpdate, OnComponentRemove, OnProjectRemove diff --git a/server-plugins/tracker/src/index.ts b/server-plugins/tracker/src/index.ts index 23128d0883..dedd06deec 100644 --- a/server-plugins/tracker/src/index.ts +++ b/server-plugins/tracker/src/index.ts @@ -35,7 +35,7 @@ export default plugin(serverTrackerId, { IssueLinkIdProvider: '' as Resource<(doc: Doc) => Promise> }, trigger: { - OnSocialIdentityCreate: '' as Resource, + OnEmployeeCreate: '' as Resource, OnIssueUpdate: '' as Resource, OnComponentRemove: '' as Resource, OnProjectRemove: '' as Resource diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 375663d73c..69a2e08add 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -14,6 +14,7 @@ // import { + type AccountUuid, type Account, type Branding, type Class, @@ -29,7 +30,6 @@ import { type ModelDb, type Obj, type PersonId, - type PersonUuid, type Ref, type SearchOptions, type SearchQuery, @@ -513,7 +513,7 @@ export interface ClientSessionCtx { ctx: MeasureContext pipeline: Pipeline - socialStringsToUsers: Map + socialStringsToUsers: Map requestId: ReqId | undefined sendResponse: (id: ReqId | undefined, msg: any) => Promise sendPong: () => void @@ -551,7 +551,7 @@ export interface Session { // Client methods ping: (ctx: ClientSessionCtx) => Promise - getUser: () => PersonUuid + getUser: () => AccountUuid getUserSocialIds: () => PersonId[] loadModel: (ctx: ClientSessionCtx, lastModelTx: Timestamp, hash?: string) => Promise diff --git a/server/core/src/utils.ts b/server/core/src/utils.ts index 346494b2a9..bd2e4b9840 100644 --- a/server/core/src/utils.ts +++ b/server/core/src/utils.ts @@ -28,7 +28,7 @@ import core, { type TxWorkspaceEvent, type PersonId, systemAccount, - type PersonUuid + type AccountUuid } from '@hcengineering/core' import { PlatformError, unknownError } from '@hcengineering/platform' import { createHash, type Hash } from 'crypto' @@ -171,7 +171,7 @@ export class SessionDataImpl implements SessionData { _removedMap: Map, Doc> | undefined, _contextCache: Map | undefined, readonly modelDb: ModelDb, - readonly socialStringsToUsers: Map + readonly socialStringsToUsers: Map ) { this._removedMap = _removedMap this._contextCache = _contextCache diff --git a/server/middleware/src/notifications.ts b/server/middleware/src/notifications.ts index 50461c6bf7..b20e3d45ab 100644 --- a/server/middleware/src/notifications.ts +++ b/server/middleware/src/notifications.ts @@ -22,7 +22,7 @@ import core, { type SessionData, TxApplyIf, systemAccountUuid, - type PersonUuid + AccountUuid } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import { BaseMiddleware, Middleware, TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core' @@ -56,7 +56,7 @@ export class NotificationsMiddleware extends BaseMiddleware implements Middlewar } processTx (ctx: MeasureContext, tx: Tx): void { - let target: PersonUuid[] | undefined + let target: AccountUuid[] | undefined if (this.isTargetDomain(tx)) { const account = ctx.contextData.account if (!account.socialIds.includes(tx.modifiedBy) && account.uuid !== systemAccountUuid) { diff --git a/server/middleware/src/spacePermissions.ts b/server/middleware/src/spacePermissions.ts index 6795cae418..4d27800d61 100644 --- a/server/middleware/src/spacePermissions.ts +++ b/server/middleware/src/spacePermissions.ts @@ -13,7 +13,6 @@ // limitations under the License. // import core, { - PersonId, Class, Doc, Permission, @@ -32,7 +31,8 @@ import core, { TxUpdateDoc, TypedSpace, type MeasureContext, - type SessionData + type SessionData, + type AccountUuid } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import { Middleware, TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core' @@ -45,7 +45,7 @@ import { BaseMiddleware } from '@hcengineering/server-core' export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware { private whitelistSpaces = new Set>() private assignmentBySpace: Record, RolesAssignment> = {} - private permissionsBySpace: Record, Record>>> = {} + private permissionsBySpace: Record, Record>>> = {} private typeBySpace: Record, Ref> = {} wasInit: Promise | boolean = false @@ -86,7 +86,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle private setPermissions (spaceId: Ref, roles: Role[], assignment: RolesAssignment): void { for (const role of roles) { - const roleMembers: PersonId[] = assignment[role._id] ?? [] + const roleMembers: AccountUuid[] = assignment[role._id] ?? [] for (const member of roleMembers) { if (this.permissionsBySpace[spaceId][member] === undefined) { @@ -147,10 +147,9 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle */ private checkPermission (ctx: MeasureContext, space: Ref, id: Ref): boolean { const account = ctx.contextData.account - const socialStrings = account.socialIds - const permissions = socialStrings.map((si) => this.permissionsBySpace[space]?.[si] ?? null) + const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? null - return permissions.some((ps) => ps !== null && ps.has(id)) + return permissions !== null && permissions.has(id) } private throwForbidden (): void { diff --git a/server/middleware/src/spaceSecurity.ts b/server/middleware/src/spaceSecurity.ts index 7a9c113a7e..50584b2b69 100644 --- a/server/middleware/src/spaceSecurity.ts +++ b/server/middleware/src/spaceSecurity.ts @@ -15,6 +15,7 @@ import core, { Account, AccountRole, + AccountUuid, AttachedDoc, Class, DOMAIN_MODEL, @@ -25,7 +26,6 @@ import core, { LookupData, MeasureContext, ObjQueryType, - PersonId, Position, PullArray, Ref, @@ -46,7 +46,6 @@ import core, { shouldShowArchived, systemAccountUuid, toFindResult, - type PersonUuid, type SessionData } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' @@ -64,7 +63,7 @@ type SpaceWithMembers = Pick[]> = {} + private allowedSpaces: Record[]> = {} private readonly spacesMap = new Map, SpaceWithMembers>() private readonly privateSpaces = new Set>() private readonly _domainSpaces = new Map> | Promise>>>() @@ -103,7 +102,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar this.wasInit = false } - private addMemberSpace (member: PersonId, space: Ref): void { + private addMemberSpace (member: AccountUuid, space: Ref): void { const arr = this.allowedSpaces[member] ?? [] arr.push(space) this.allowedSpaces[member] = arr @@ -163,7 +162,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - private removeMemberSpace (member: PersonId, space: Ref): void { + private removeMemberSpace (member: AccountUuid, space: Ref): void { const arr = this.allowedSpaces[member] if (arr !== undefined) { const index = arr.findIndex((p) => p === space) @@ -197,7 +196,11 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - private pushMembersHandle (ctx: MeasureContext, addedMembers: PersonId | Position, space: Ref): void { + private pushMembersHandle ( + ctx: MeasureContext, + addedMembers: AccountUuid | Position, + space: Ref + ): void { if (typeof addedMembers === 'object') { for (const member of addedMembers.$each) { this.addMemberSpace(member, space) @@ -211,11 +214,11 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar private pullMembersHandle ( ctx: MeasureContext, - removedMembers: Partial | PullArray, + removedMembers: Partial | PullArray, space: Ref ): void { if (typeof removedMembers === 'object') { - const { $in } = removedMembers as PullArray + const { $in } = removedMembers as PullArray if ($in !== undefined) { for (const member of $in) { this.removeMemberSpace(member, space) @@ -228,10 +231,10 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - private syncMembers (ctx: MeasureContext, members: PersonId[], space: SpaceWithMembers): void { + private syncMembers (ctx: MeasureContext, members: AccountUuid[], space: SpaceWithMembers): void { const oldMembers = new Set(space.members) const newMembers = new Set(members) - const changed: PersonId[] = [] + const changed: AccountUuid[] = [] for (const old of oldMembers) { if (!newMembers.has(old)) { this.removeMemberSpace(old, space._id) @@ -250,8 +253,8 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - private brodcastEvent (ctx: MeasureContext, users: PersonId[], space?: Ref): void { - const targets = this.getTargets(users, ctx.contextData.socialStringsToUsers) + private brodcastEvent (ctx: MeasureContext, users: AccountUuid[], space?: Ref): void { + const targets = this.getTargets(users) const tx: TxWorkspaceEvent = { _class: core.class.TxWorkspaceEvent, _id: generateId(), @@ -272,18 +275,16 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } private broadcastNonMembers (ctx: MeasureContext, space: SpaceWithMembers): void { - const { socialStringsToUsers } = ctx.contextData const members = space?.members ?? [] - const users = Array.from(socialStringsToUsers.keys()).filter((si) => !members.includes(si)) - this.brodcastEvent(ctx, users, space._id) + this.brodcastEvent(ctx, members, space._id) } private broadcastAll (ctx: MeasureContext, space: SpaceWithMembers): void { const { socialStringsToUsers } = ctx.contextData - const users = Array.from(socialStringsToUsers.keys()) + const accounts = Array.from(new Set(socialStringsToUsers.values())) - this.brodcastEvent(ctx, users, space._id) + this.brodcastEvent(ctx, accounts, space._id) } private async handleUpdate (ctx: MeasureContext, tx: TxCUD): Promise { @@ -342,13 +343,12 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - getTargets (socialStrings: PersonId[], socialStringsToUsers: Map): string[] { - const users = new Set( - socialStrings.map((s) => socialStringsToUsers.get(s)).filter((u) => u !== undefined) as string[] - ) + getTargets (accounts: AccountUuid[]): string[] { + const res = Array.from(new Set(accounts)) // We need to add system account for targets for integrations to work properly - users.add(systemAccountUuid) - return Array.from(users) + res.push(systemAccountUuid) + + return res } private async processTxSpaceDomain (sctx: MeasureContext, actualTx: TxCUD): Promise { @@ -433,17 +433,14 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar if (space === undefined) return undefined if (this.systemSpaces.has(space._id) || this.mainSpaces.has(space._id)) return undefined - return space.members.length === 0 - ? undefined - : this.getTargets(space?.members, ctx.contextData.socialStringsToUsers) + return space.members.length === 0 ? undefined : this.getTargets(space?.members) } await this.next?.handleBroadcast(ctx) } private getAllAllowedSpaces (account: Account, isData: boolean, showArchived: boolean): Ref[] { - const userSocialStrings = account.socialIds - const userSpaces = new Set(userSocialStrings.map((s) => this.allowedSpaces[s] ?? []).flat()) + const userSpaces = this.allowedSpaces[account.uuid] ?? [] const res = [ ...Array.from(userSpaces), account.uuid as unknown as Ref, diff --git a/server/mongo/src/__tests__/minmodel.ts b/server/mongo/src/__tests__/minmodel.ts index bf0f1539b8..a59cd3c806 100644 --- a/server/mongo/src/__tests__/minmodel.ts +++ b/server/mongo/src/__tests__/minmodel.ts @@ -30,7 +30,8 @@ import core, { type Ref, type TxCreateDoc, type TxCUD, - TxFactory + TxFactory, + type AccountUuid } from '@hcengineering/core' import type { IntlString, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -198,8 +199,8 @@ export function genMinModel (): TxCUD[] { }) ) - const u1 = 'User1' as PersonId - const u2 = 'User2' as PersonId + const u1 = 'User1' as AccountUuid + const u2 = 'User2' as AccountUuid txes.push( createDoc(core.class.Space, { name: 'Sp1', diff --git a/server/postgres/src/__tests__/minmodel.ts b/server/postgres/src/__tests__/minmodel.ts index 3eb179e622..41a9596717 100644 --- a/server/postgres/src/__tests__/minmodel.ts +++ b/server/postgres/src/__tests__/minmodel.ts @@ -14,6 +14,7 @@ // import core, { + type AccountUuid, type Arr, type AttachedDoc, type Class, @@ -234,8 +235,8 @@ export function genMinModel (): TxCUD[] { }) ) - const u1 = 'User1' as PersonId - const u2 = 'User2' as PersonId + const u1 = 'User1' as AccountUuid + const u2 = 'User2' as AccountUuid txes.push( createDoc(core.class.Space, { name: 'Sp1', diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index cc0cf4466f..ce4f71be38 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -782,7 +782,7 @@ abstract class PostgresAdapterBase implements DbAdapter { const key = domain === DOMAIN_SPACE ? '_id' : domain === DOMAIN_TX ? "data ->> 'objectSpace'" : 'space' const privateCheck = domain === DOMAIN_SPACE ? ' OR sec.private = false' : '' const archivedCheck = showArchived ? '' : ' AND sec.archived = false' - const q = `(sec.members && (${vars.addArray(acc.socialIds)}) OR sec."_class" = '${core.class.SystemSpace}'${privateCheck})${archivedCheck}` + const q = `(sec.members @> '{"${acc.uuid}"}' OR sec."_class" = '${core.class.SystemSpace}'${privateCheck})${archivedCheck}` return `INNER JOIN ${translateDomain(DOMAIN_SPACE)} AS sec ON sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q}` } } diff --git a/server/server/src/client.ts b/server/server/src/client.ts index 6852175c5f..e0b1a1e99f 100644 --- a/server/server/src/client.ts +++ b/server/server/src/client.ts @@ -14,6 +14,7 @@ // import { + AccountUuid, generateId, TxProcessor, type Account, @@ -26,7 +27,6 @@ import { type LoadModelResponse, type MeasureContext, type PersonId, - type PersonUuid, type Ref, type SearchOptions, type SearchQuery, @@ -86,7 +86,7 @@ export class ClientSession implements Session { this.isAdmin = this.token.extra?.admin === 'true' } - getUser (): PersonUuid { + getUser (): AccountUuid { return this.token.account } diff --git a/server/server/src/sessionManager.ts b/server/server/src/sessionManager.ts index 9bb1d25f50..a609b6eae2 100644 --- a/server/server/src/sessionManager.ts +++ b/server/server/src/sessionManager.ts @@ -38,11 +38,11 @@ import core, { buildSocialIdString, type PersonId, type WorkspaceDataId, - type PersonUuid, Data, Version, platformNow, - platformNowDiff + platformNowDiff, + AccountUuid } from '@hcengineering/core' import { getClient as getAccountClient, isWorkspaceLoginInfo } from '@hcengineering/account-client' import { unknownError, type Status } from '@hcengineering/platform' @@ -1023,13 +1023,13 @@ export class TSessionManager implements SessionManager { } // TODO: cache this map and update when sessions created/closed - getActiveSocialStringsToUsersMap (workspace: WorkspaceUuid): Map { + getActiveSocialStringsToUsersMap (workspace: WorkspaceUuid): Map { const ws = this.workspaces.get(workspace) if (ws === undefined) { return new Map() } - const res = new Map() + const res = new Map() for (const s of ws.sessions.values()) { const sessionAccount = s.session.getUser() if (sessionAccount === systemAccountUuid) { @@ -1059,7 +1059,7 @@ export class TSessionManager implements SessionManager { return userCtx .with('🧭 handleRequest', {}, async (ctx) => { if (request.time != null) { - const delta = Date.now() - request.time + const delta = platformNow() - request.time requestCtx.measure('msg-receive-delta', delta) } if (service.workspace.closing !== undefined) { diff --git a/server/token/src/token.ts b/server/token/src/token.ts index 580813ab51..7b1eafe7c0 100644 --- a/server/token/src/token.ts +++ b/server/token/src/token.ts @@ -1,4 +1,4 @@ -import { MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core' +import { AccountUuid, MeasureContext, PersonUuid, WorkspaceUuid } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import { decode, encode } from 'jwt-simple' import serverPlugin from './plugin' @@ -7,7 +7,7 @@ import serverPlugin from './plugin' * @public */ export interface Token { - account: PersonUuid + account: AccountUuid workspace: WorkspaceUuid extra?: Record } diff --git a/server/ws/src/__tests__/minmodel.ts b/server/ws/src/__tests__/minmodel.ts index 6cf3ca8c10..625e0d06d6 100644 --- a/server/ws/src/__tests__/minmodel.ts +++ b/server/ws/src/__tests__/minmodel.ts @@ -28,7 +28,8 @@ import core, { type Obj, type Ref, type TxCUD, - type TxCreateDoc + type TxCreateDoc, + type AccountUuid } from '@hcengineering/core' import type { IntlString, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -179,8 +180,8 @@ export function genMinModel (): TxCUD[] { }) ) - const u1 = 'User1' as PersonId - const u2 = 'User2' as PersonId + const u1 = 'User1' as AccountUuid + const u2 = 'User2' as AccountUuid txes.push( createDoc(core.class.Space, { name: 'Sp1', diff --git a/services/ai-bot/pod-ai-bot/src/controller.ts b/services/ai-bot/pod-ai-bot/src/controller.ts index fe080f2be8..6ce317b1b0 100644 --- a/services/ai-bot/pod-ai-bot/src/controller.ts +++ b/services/ai-bot/pod-ai-bot/src/controller.ts @@ -22,7 +22,7 @@ import { TranslateRequest, TranslateResponse } from '@hcengineering/ai-bot' -import { MeasureContext, PersonUuid, Ref, SocialId, type WorkspaceIds, type WorkspaceUuid } from '@hcengineering/core' +import { AccountUuid, MeasureContext, Ref, SocialId, type WorkspaceIds, type WorkspaceUuid } from '@hcengineering/core' import { Room } from '@hcengineering/love' import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot' import { getAccountClient } from '@hcengineering/server-client' @@ -53,7 +53,7 @@ export class AIControl { private readonly openaiEncoding = encodingForModel(config.OpenAIModel) constructor ( - readonly personUuid: PersonUuid, + readonly personUuid: AccountUuid, readonly socialIds: SocialId[], private readonly storage: DbStorage, private readonly ctx: MeasureContext diff --git a/services/ai-bot/pod-ai-bot/src/start.ts b/services/ai-bot/pod-ai-bot/src/start.ts index e5c8bae01c..dce724bbf9 100644 --- a/services/ai-bot/pod-ai-bot/src/start.ts +++ b/services/ai-bot/pod-ai-bot/src/start.ts @@ -18,7 +18,7 @@ import serverToken, { generateToken } from '@hcengineering/server-token' import { initStatisticsContext } from '@hcengineering/server-core' import config from './config' -import { getPersonUuid } from './utils/account' +import { getAccountUuid } from './utils/account' import { registerLoaders } from './loaders' import { getDbStorage } from './storage' import { AIControl } from './controller' @@ -37,7 +37,7 @@ export const start = async (): Promise => { ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName }) const personUuid = await withRetry( - async () => await getPersonUuid(ctx), + async () => await getAccountUuid(ctx), (_, attempt) => attempt >= 5, 5000 )() diff --git a/services/ai-bot/pod-ai-bot/src/utils/account.ts b/services/ai-bot/pod-ai-bot/src/utils/account.ts index b4744b7f7d..0619120208 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/account.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/account.ts @@ -14,11 +14,12 @@ // import { - WorkspaceInfoWithStatus, + type WorkspaceInfoWithStatus, isWorkspaceCreating, type WorkspaceUuid, AccountRole, - Person as GlobalPerson + type Person as GlobalPerson, + type AccountUuid } from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' import { getAccountClient, withRetry } from '@hcengineering/server-client' @@ -129,14 +130,14 @@ async function confirmAccount (uuid: PersonUuid): Promise { } } -export async function getPersonUuid (ctx: MeasureContext): Promise { +export async function getAccountUuid (ctx?: MeasureContext): Promise { const token = generateToken(systemAccountUuid, undefined, { service: 'aibot', confirmEmail: aiBotAccountEmail }) const accountClient = getAccountClient(token) const personUuid = await accountClient.findPerson(aiBotEmailSocialId) if (personUuid !== undefined) { await confirmAccount(personUuid) - return personUuid + return personUuid as AccountUuid } const result = await accountClient.signUp(aiBotEmailSocialId, config.Password, config.FirstName, config.LastName) diff --git a/services/ai-bot/pod-ai-bot/src/utils/openai.ts b/services/ai-bot/pod-ai-bot/src/utils/openai.ts index 0a291ec1f7..e38a669ff7 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/openai.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/openai.ts @@ -16,7 +16,7 @@ import { countTokens } from '@hcengineering/openai' import { Tiktoken } from 'js-tiktoken' import OpenAI from 'openai' -import { PersonId } from '@hcengineering/core' +import { AccountUuid } from '@hcengineering/core' import config from '../config' import { HistoryRecord } from '../types' @@ -73,7 +73,7 @@ export async function createChatCompletionWithTools ( workspaceClient: WorkspaceClient, client: OpenAI, message: OpenAI.ChatCompletionMessageParam, - user?: PersonId, + user?: AccountUuid, history: OpenAI.ChatCompletionMessageParam[] = [], skipCache = true ): Promise< diff --git a/services/ai-bot/pod-ai-bot/src/utils/platform.ts b/services/ai-bot/pod-ai-bot/src/utils/platform.ts index 3f48894a07..141f3ca42a 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/platform.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/platform.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import core, { PersonId, Client, Ref, TxOperations } from '@hcengineering/core' +import core, { Client, Ref, TxOperations, AccountUuid, PersonId } from '@hcengineering/core' import { createClient } from '@hcengineering/server-client' -import contact, { getAllSocialStringsByPersonId, Person } from '@hcengineering/contact' +import contact, { Employee, Person } from '@hcengineering/contact' import chunter, { DirectMessage } from '@hcengineering/chunter' import { aiBotEmailSocialId } from '@hcengineering/ai-bot' import notification from '@hcengineering/notification' @@ -23,19 +23,28 @@ export async function connectPlatform (token: string, endpoint: string): Promise return await createClient(endpoint, token) } -export async function getDirect ( - client: TxOperations, - personId: PersonId, - aiPerson?: Ref -): Promise | undefined> { - const personIds = new Set(await getAllSocialStringsByPersonId(client, personId)) +export async function getAccountBySocialId (client: TxOperations, socialId: PersonId): Promise { + const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: socialId }) - if (personIds.size === 0) { - return + if (socialIdentity === undefined) { + return null } - const existingDm = (await client.findAll(chunter.class.DirectMessage, { members: aiBotEmailSocialId })).find((dm) => - dm.members.every((m) => m === aiBotEmailSocialId || personIds.has(m)) + const employee = await client.findOne(contact.mixin.Employee, { _id: socialIdentity.attachedTo as Ref }) + + return employee?.personUuid ?? null +} + +export async function getDirect ( + client: TxOperations, + account: AccountUuid, + aiPerson?: Ref +): Promise | undefined> { + const aibotAccount = await getAccountBySocialId(client, aiBotEmailSocialId) + if (aibotAccount == null) return undefined + + const existingDm = (await client.findAll(chunter.class.DirectMessage, { members: aibotAccount })).find((dm) => + dm.members.every((m) => m === aibotAccount || m === account) ) if (existingDm !== undefined) { @@ -47,7 +56,7 @@ export async function getDirect ( description: '', private: true, archived: false, - members: [aiBotEmailSocialId, personId] + members: [aibotAccount, account] }) if (aiPerson === undefined) return dmId @@ -55,7 +64,7 @@ export async function getDirect ( const space = await client.findOne(contact.class.PersonSpace, { person: aiPerson }) if (space === undefined) return dmId await client.createDoc(notification.class.DocNotifyContext, space._id, { - user: aiBotEmailSocialId, + user: aibotAccount, objectId: dmId, objectClass: chunter.class.DirectMessage, objectSpace: core.space.Space, diff --git a/services/ai-bot/pod-ai-bot/src/utils/tools.ts b/services/ai-bot/pod-ai-bot/src/utils/tools.ts index e003108a84..e6731b3736 100644 --- a/services/ai-bot/pod-ai-bot/src/utils/tools.ts +++ b/services/ai-bot/pod-ai-bot/src/utils/tools.ts @@ -1,4 +1,4 @@ -import { MarkupBlobRef, PersonId, Ref } from '@hcengineering/core' +import { AccountUuid, MarkupBlobRef, Ref } from '@hcengineering/core' import document, { Document, getFirstRank, Teamspace } from '@hcengineering/document' import { makeRank } from '@hcengineering/rank' import { markdownToMarkup } from '@hcengineering/text-markdown' @@ -83,7 +83,7 @@ async function pdfToMarkdown ( async function saveFile ( workspaceClient: WorkspaceClient, - user: PersonId | undefined, + user: AccountUuid | undefined, args: { fileId: string, folder: string | undefined, parent: string | undefined, name: string } ): Promise { console.log('Save file', args) @@ -130,7 +130,7 @@ function getTeamspace ( async function getFoldersForDocuments ( workspaceClient: WorkspaceClient, - user: PersonId | undefined, + user: AccountUuid | undefined, args: Record ): Promise { const client = await workspaceClient.opClient @@ -162,7 +162,7 @@ type PredefinedToolFunction = Omit< T extends string ? RunnableFunctionWithoutParse : RunnableFunctionWithParse, 'function' > -type ToolFunc = (workspaceClient: WorkspaceClient, user: PersonId | undefined, args: any) => Promise | string +type ToolFunc = (workspaceClient: WorkspaceClient, user: AccountUuid | undefined, args: any) => Promise | string const tools: [PredefinedTool, ToolFunc][] = [] @@ -224,7 +224,7 @@ registerTool( export function getTools ( workspaceClient: WorkspaceClient, - user: PersonId | undefined + user: AccountUuid | undefined ): RunnableTools { const result: (RunnableToolFunctionWithoutParse | RunnableToolFunctionWithParse)[] = [] for (const tool of tools) { diff --git a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts index cd4e59764a..0f04076514 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/workspaceClient.ts @@ -47,7 +47,8 @@ import core, { TxCUD, TxOperations, type WorkspaceUuid, - type WorkspaceIds + type WorkspaceIds, + AccountUuid } from '@hcengineering/core' import { Room } from '@hcengineering/love' import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot' @@ -90,7 +91,7 @@ export class WorkspaceClient { readonly transactorUrl: string, readonly token: string, readonly wsIds: WorkspaceIds, - readonly personUuid: PersonUuid, + readonly personUuid: AccountUuid, readonly socialIds: SocialId[], readonly ctx: MeasureContext, readonly openai: OpenAI | undefined, @@ -337,7 +338,13 @@ export class WorkspaceClient { void this.pushHistory(promptText, prompt.role, promptTokens, personUuid, objectId, objectClass) - const chatCompletion = await createChatCompletionWithTools(this, this.openai, prompt, user, history) + const chatCompletion = await createChatCompletionWithTools( + this, + this.openai, + prompt, + personUuid as AccountUuid, + history + ) const response = chatCompletion?.completion if (response == null) { diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index 279cffd580..21d9bc3056 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -1,4 +1,4 @@ -import { PersonId, Doc, Ref, TxOperations } from '@hcengineering/core' +import { Doc, Ref, TxOperations, AccountUuid } from '@hcengineering/core' import notification, { DocNotifyContext } from '@hcengineering/notification' import { IntlString } from '@hcengineering/platform' import { PersonSpace } from '@hcengineering/contact' @@ -7,7 +7,7 @@ import github from '@hcengineering/github' export async function createNotification ( client: TxOperations, forDoc: Doc, - data: { user: PersonId, space: Ref, message: IntlString, props: Record } + data: { user: AccountUuid, space: Ref, message: IntlString, props: Record } ): Promise { let docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { objectId: forDoc._id }) diff --git a/services/telegram-bot/pod-telegram-bot/src/worker.ts b/services/telegram-bot/pod-telegram-bot/src/worker.ts index 5088775f4b..e929c3d4c8 100644 --- a/services/telegram-bot/pod-telegram-bot/src/worker.ts +++ b/services/telegram-bot/pod-telegram-bot/src/worker.ts @@ -14,7 +14,7 @@ // import type { Collection, ObjectId, WithId } from 'mongodb' -import { MeasureContext, PersonId, Ref, SortingOrder, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' +import { MeasureContext, Ref, SortingOrder, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' import { InboxNotification } from '@hcengineering/notification' import { TelegramNotificationRequest } from '@hcengineering/telegram' import { StorageAdapter } from '@hcengineering/server-core' @@ -245,7 +245,7 @@ export class PlatformWorker { async getChannelName (client: WorkspaceClient, channel: ChunterSpace, email: string): Promise { if (client.hierarchy.isDerived(channel._class, chunter.class.DirectMessage)) { - const persons = await client.getPersons(channel.members as PersonId[], email) + const persons = await client.getPersons(channel.members, email) return persons .map(({ name }) => formatName(name)) .sort((a, b) => a.localeCompare(b)) diff --git a/services/telegram-bot/pod-telegram-bot/src/workspace.ts b/services/telegram-bot/pod-telegram-bot/src/workspace.ts index 35717d78ff..1a7222f173 100644 --- a/services/telegram-bot/pod-telegram-bot/src/workspace.ts +++ b/services/telegram-bot/pod-telegram-bot/src/workspace.ts @@ -27,7 +27,8 @@ import core, { Space, systemAccountUuid, TxFactory, - WorkspaceUuid + WorkspaceUuid, + AccountUuid } from '@hcengineering/core' import { generateToken } from '@hcengineering/server-token' import notification, { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification' @@ -104,7 +105,7 @@ export class WorkspaceClient { return attachments } - async isReplyAvailable (account: PersonId, message: ActivityMessage): Promise { + async isReplyAvailable (account: AccountUuid, message: ActivityMessage): Promise { const hierarchy = this.hierarchy let objectId: Ref @@ -376,7 +377,7 @@ export class WorkspaceClient { // }) } - async getPersons (_ids: PersonId[], myEmail: string): Promise { + async getPersons (_ids: AccountUuid[], myEmail: string): Promise { // TODO: FIXME throw new Error('Not implemented') // const me = await this.client.findOne(contact.class.PersonAccount, { email: myEmail }) diff --git a/tests/sanity/tests/model/contacts/contact-page.ts b/tests/sanity/tests/model/contacts/contact-page.ts new file mode 100644 index 0000000000..2f655202c1 --- /dev/null +++ b/tests/sanity/tests/model/contacts/contact-page.ts @@ -0,0 +1,257 @@ +import { expect, type Locator, type Page } from '@playwright/test' + +export enum ButtonAction { + Open = 'Open', + OpenInNewTab = 'Open in new tab', + NewApplication = 'New Application', + NewLead = 'New Lead', + NewRelatedIssue = 'New related issue', + PublicLink = 'Public link', + Delete = 'Delete', + MergeContacts = 'Merge contacts' +} + +export class ContactPage { + page: Page + + constructor (page: Page) { + this.page = page + } + + readonly appContact = (): Locator => this.page.locator('[id="app-contact\\:string\\:Contacts"]') + readonly employeeNavElement = (Employee: string): Locator => + this.page.locator(`.hulyNavItem-container:has-text("${Employee}")`) + + readonly employeeButton = (Employee: string): Locator => + this.page.locator(`button:not(.hulyNavItem-container, .hulyBreadcrumb-container):has-text("${Employee}")`) + + readonly firstNameInput = (): Locator => this.page.locator('[placeholder="First name"]') + readonly lastNameInput = (): Locator => this.page.locator('[placeholder="Last name"]') + readonly emailInput = (): Locator => this.page.locator('[placeholder="Email"]') + readonly createButton = (): Locator => this.page.locator('.antiCard button:has-text("Create")') + readonly formAntiCard = (): Locator => this.page.locator('form.antiCard') + readonly employeeEntry = (first: string, last: string): Locator => + this.page.locator(`td:has-text("${last} ${first}")`) + + readonly kickEmployeeOption = (): Locator => this.page.locator('text="Kick employee"') + readonly submitButton = (): Locator => this.page.locator('form[id="view:string:DeleteObject"] button[type="submit"]') + readonly okButton = (): Locator => this.page.locator('text=Ok') + readonly openButton = (): Locator => this.page.locator('button:has-text("Open")') + readonly openNewTabButton = (): Locator => this.page.locator('button:has-text("Open in new tab")') + readonly openNewApplicationButton = (): Locator => this.page.locator('button:has-text("New Application")') + readonly openNewLeadButton = (): Locator => this.page.locator('button:has-text("New Lead")') + readonly openNewRelatedIssueButton = (): Locator => this.page.locator('button:has-text("New related issue")') + readonly openPublicLinkButton = (): Locator => this.page.locator('button:has-text("Public link")') + readonly openMergeContactsButton = (): Locator => this.page.locator('button:has-text("Merge contacts")') + readonly openDeleteButton = (): Locator => this.page.locator('button:has-text("Delete")') + readonly newApplicationDescription = (): Locator => this.page.getByRole('paragraph') + readonly newApplicationAssignRectruiter = (): Locator => this.page.getByRole('button', { name: 'Assigned recruiter' }) + readonly newApplicationChooseRecruiter = (recruiter: string): Locator => + this.page.getByRole('button', { name: recruiter }) + + readonly newApplicationInterview = (Interview: string): Locator => this.page.getByRole('button', { name: Interview }) + readonly newApplicationStartDate = (): Locator => this.page.getByRole('button', { name: 'Start date' }) + readonly newApplicationStartInADay = (): Locator => this.page.getByText('in a day') + readonly newApplicationCreate = (): Locator => this.page.getByRole('button', { name: 'Create' }) + readonly personName = (person: string): Locator => this.page.locator(`text=${person}`) + readonly personTable = (): Locator => this.page.locator('.antiTable-body__row') + readonly personMarina = (): Locator => this.page.getByRole('link', { name: 'MM M. Marina' }) + readonly comapnyTab = (): Locator => this.page.locator('.hulyNavItem-container:has-text("Company")') + readonly addCompany = (): Locator => this.page.locator('button.antiButton:has-text("Company")') + readonly companyName = (): Locator => this.page.locator('[placeholder="Company name"]') + readonly companyCreateButton = (): Locator => this.page.locator('button:has-text("Create")') + readonly companyByName = (company: string): Locator => this.page.locator(`text=${company}`) + readonly addMember = (): Locator => this.page.locator('[id="contact:string:AddMember"]') + readonly selectMember = (): Locator => this.page.getByRole('cell', { name: 'CR Chen Rosamund' }).getByRole('link') + readonly openNewMember = (member: string): Locator => this.page.locator(`.card a:has-text("${member}")`) + readonly newMemberAdded = (): Locator => this.page.getByText('New Members: Chen Rosamund') + readonly stateApplication = (role: string): Locator => this.page.getByRole('cell', { name: role }) + readonly commentApplication = (): Locator => this.page.getByRole('button', { name: '1', exact: true }) + readonly commentDescription = (): Locator => this.page.getByText('Test Application') + readonly buttonClosePanel = (): Locator => this.page.locator('button#btnPClose') + + // ACTIONS + + async addNewApplication (description: string, recruiter: string): Promise { + await this.newApplicationDescription().click() + await this.newApplicationDescription().fill(description) + await this.newApplicationAssignRectruiter().click() + await this.newApplicationChooseRecruiter(recruiter).click() + await this.newApplicationStartDate().click() + await this.newApplicationStartInADay().click() + await this.newApplicationCreate().click() + } + + async clickAppContact (): Promise { + await this.appContact().click() + } + + async clickEmployeeNavElement (Employee: string): Promise { + await this.employeeNavElement(Employee).click() + } + + async clickEmployeeButton (Employee: string): Promise { + await this.employeeButton(Employee).click() + } + + async clickFirstNameInput (): Promise { + await this.firstNameInput().click() + } + + async fillFirstNameInput (firstName: string): Promise { + await this.firstNameInput().fill(firstName) + } + + async clickLastNameInput (): Promise { + await this.lastNameInput().click() + } + + async fillLastNameInput (lastName: string): Promise { + await this.lastNameInput().fill(lastName) + } + + async clickEmailInput (): Promise { + await this.emailInput().click() + } + + async fillEmailInput (email: string): Promise { + await this.emailInput().fill(email) + } + + async fillCompanyInput (company: string): Promise { + await this.companyName().click() + await this.companyName().fill(company) + } + + async clickCreateButton (): Promise { + await this.createButton().click() + } + + async clickSubmitButton (): Promise { + await this.submitButton().click() + } + + async clickOnEmployee (first: string, last: string): Promise { + await this.employeeEntry(first, last).click() + } + + async waitForFormAntiCardDetached (): Promise { + await this.formAntiCard().waitFor({ state: 'detached' }) + } + + async clickCompanyTab (): Promise { + await this.comapnyTab().click() + } + + async clickAddCompany (): Promise { + await this.addCompany().click() + } + + async clickCreateCompany (): Promise { + await this.companyCreateButton().click() + } + + async clickCompanyByName (company: string): Promise { + await this.companyByName(company).click() + } + + async clickAddMember (): Promise { + await this.addMember().click() + } + + async clickSelectMember (): Promise { + await this.selectMember().click() + } + + async clickOpenNewMember (member: string): Promise { + await this.openNewMember(member).click() + } + + // Regular approach + async kickEmployee (first: string, last: string): Promise { + await this.employeeEntry(first, last).hover() + await this.employeeEntry(first, last).click({ button: 'right' }) + await this.kickEmployeeOption().click() + await this.okButton().click() + } + + // Approach where we use the enum to determine the action to take on the right click + async personRightClickOption (first: string, last: string, action: ButtonAction): Promise { + await this.employeeEntry(first, last).hover() + await this.employeeEntry(first, last).click({ button: 'right' }) + switch (action) { + case ButtonAction.Open: + await this.openButton().click() + break + case ButtonAction.OpenInNewTab: + await this.openNewTabButton().click() + break + case ButtonAction.NewApplication: + await this.openNewApplicationButton().click() + break + case ButtonAction.NewLead: + await this.openNewLeadButton().click() + break + case ButtonAction.NewRelatedIssue: + await this.openNewRelatedIssueButton().click() + break + case ButtonAction.Delete: + await this.openDeleteButton().click() + break + case ButtonAction.PublicLink: + await this.openPublicLinkButton().click() + break + case ButtonAction.MergeContacts: + await this.openMergeContactsButton().click() + break + default: + throw new Error('Option does not exists') + } + } + + // ASSERTIONS + + async expectKickEmployeeShowsInactiveStatus (first: string, last: string): Promise { + await expect(this.employeeEntry(first, last)).toContainText('Inactive') + } + + async checkIfPersonIsDeleted (first: string, last: string, count: number): Promise { + await expect(this.employeeEntry(first, last)).toHaveCount(count) + } + + async checkIfPersonIsCreated (first: string, last: string): Promise { + await expect(this.employeeEntry(first, last)).toBeVisible() + } + + async checkPersonMarinaIsVisible (person: string): Promise { + await expect(this.personName(person)).toBeVisible() + } + + async checkPersonTableCount (count: number, checkMoreOrEqual: boolean = false): Promise { + const actualCount = await this.personTable().count() + + if (checkMoreOrEqual) { + expect(actualCount).toBeGreaterThanOrEqual(count) + } else { + await expect(this.personMarina()).toBeVisible() + } + } + + async checkIfTextIsVisible (text: string): Promise { + await expect(this.page.locator(`text=${text}`)).toBeVisible() + } + + async checkIfNewMemberIsAdded (): Promise { + await expect(this.newMemberAdded()).toBeVisible() + } + + async checkStateApplication (role: string): Promise { + await expect(this.stateApplication(role)).toBeVisible() + await this.commentApplication().hover() + await expect(this.commentDescription()).toBeVisible() + } + + async closePanel (): Promise { + await this.buttonClosePanel().click() + } +}