UBERF-9503: generated social ids (#8208)

This commit is contained in:
Alexey Zinoviev
2025-03-20 17:21:51 +07:00
committed by GitHub
parent 82c53184c0
commit 899b00df66
101 changed files with 1767 additions and 680 deletions
+4 -4
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { buildSocialIdString, concatLink, SocialIdType, TxOperations } from '@hcengineering/core'
import { concatLink, TxOperations } from '@hcengineering/core'
import {
ClickupImporter,
defaultDocumentPreprocessors,
@@ -75,8 +75,8 @@ export function importTool (): void {
setMetadata(serverClientPlugin.metadata.Endpoint, config.ACCOUNTS_URL)
console.log('Trying to login user: ', user)
const unauthAccountClient = getAccountClient()
const { account, token } = await unauthAccountClient.login(user, password)
if (token === undefined || account === undefined) {
const { account, token, socialId } = await unauthAccountClient.login(user, password)
if (token === undefined || account === undefined || socialId === undefined) {
console.log('Login failed for user: ', user)
return
}
@@ -95,7 +95,7 @@ export function importTool (): void {
console.log('Connecting to Transactor URL: ', selectedWs.endpoint)
const connection = await createClient(selectedWs.endpoint, selectedWs.token)
const client = new TxOperations(connection, buildSocialIdString({ type: SocialIdType.EMAIL, value: user }))
const client = new TxOperations(connection, socialId)
const fileUploader = new FrontFileUploader(
getFrontUrl(),
selectedWs.workspace,
+4 -4
View File
@@ -46,7 +46,7 @@ import os from 'os'
import { Worker, isMainThread, parentPort } from 'worker_threads'
import { CSVWriter } from './csv'
import { AvatarType, getPersonBySocialId } from '@hcengineering/contact'
import { AvatarType, getPersonBySocialKey } from '@hcengineering/contact'
import contact from '@hcengineering/model-contact'
import recruit from '@hcengineering/model-recruit'
import { type Vacancy } from '@hcengineering/recruit'
@@ -579,7 +579,7 @@ export async function generateWorkspaceData (
const client = new TxOperations(connection, core.account.System)
try {
const emailSocialString = buildSocialIdString({ type: SocialIdType.EMAIL, value: email })
const person = await getPersonBySocialId(client, emailSocialString)
const person = await getPersonBySocialKey(client, emailSocialString)
const account = person?.personUuid as AccountUuid
if (account == null) {
throw new Error('User not found')
@@ -632,7 +632,7 @@ export async function generateEmployee (client: TxOperations): Promise<AccountUu
type: SocialIdType.HULY,
value: personUuid,
key: socialString,
confirmed: true
verifiedOn: Date.now()
}
)
@@ -678,7 +678,7 @@ async function generateVacancy (client: TxOperations, members: AccountUuid[]): P
type: SocialIdType.HULY,
value: personUuid,
key: socialString,
confirmed: true
verifiedOn: Date.now()
}
)
await client.createMixin(personId, contact.class.Person, contact.space.Contacts, recruit.mixin.Candidate, {})
+72 -10
View File
@@ -43,8 +43,10 @@ import {
import { htmlToMarkup } from '@hcengineering/text'
import {
getAccountUuidByOldAccount,
getAccountUuidBySocialId,
getSocialIdByOldAccount
getAccountUuidBySocialKey,
getSocialIdBySocialKey,
getSocialIdFromOldAccount,
getSocialKeyByOldAccount
} from '@hcengineering/model-core'
import { activityId, DOMAIN_ACTIVITY, DOMAIN_REACTION, DOMAIN_USER_MENTION } from './index'
@@ -204,7 +206,9 @@ async function migrateActivityMarkup (client: MigrationClient): Promise<void> {
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const socialIdBySocialKey = new Map<string, PersonId | null>()
const socialIdByOldAccount = new Map<string, PersonId | null>()
ctx.info('processing activity reactions ', {})
const iterator = await client.traverse(DOMAIN_REACTION, { _class: activity.class.Reaction })
@@ -221,7 +225,14 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
for (const doc of docs) {
const reaction = doc as Reaction
const newCreateBy = socialIdByAccount[reaction.createBy] ?? reaction.createBy
const socialId = await getSocialIdFromOldAccount(
client,
reaction.createBy,
socialKeyByAccount,
socialIdBySocialKey,
socialIdByOldAccount
)
const newCreateBy = socialId ?? reaction.createBy
if (newCreateBy === reaction.createBy) continue
@@ -254,8 +265,8 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
*/
async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('activity migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing activity doc updates ', {})
function getUpdatedClass (attrKey: string): string {
@@ -264,9 +275,9 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<vo
async function getUpdatedVal (oldVal: string, attrKey: string): Promise<any> {
if (['members', 'owners', 'user'].includes(attrKey)) {
return (await getAccountUuidByOldAccount(client, oldVal, socialIdByAccount, accountUuidBySocialId)) ?? oldVal
return (await getAccountUuidByOldAccount(client, oldVal, socialKeyByAccount, accountUuidBySocialKey)) ?? oldVal
} else {
return socialIdByAccount[oldVal] ?? oldVal
return socialKeyByAccount[oldVal] ?? oldVal
}
}
@@ -363,11 +374,11 @@ async function migrateAccountsInDocUpdates (client: MigrationClient): Promise<vo
*/
async function migrateSocialIdsInDocUpdates (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('activity migrateSocialIdsInDocUpdates', {})
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing activity doc updates ', {})
async function getUpdatedVal (oldVal: string): Promise<any> {
return (await getAccountUuidBySocialId(client, oldVal as PersonId, accountUuidBySocialId)) ?? oldVal
return (await getAccountUuidBySocialKey(client, oldVal, accountUuidBySocialKey)) ?? oldVal
}
async function migrateField<P extends keyof DocAttributeUpdates> (
@@ -455,6 +466,51 @@ async function migrateSocialIdsInDocUpdates (client: MigrationClient): Promise<v
ctx.info('finished processing activity doc updates ', {})
}
async function migrateSocialKeysToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('activity migrateSocialKeysToSocialIds', {})
ctx.info('processing activity reactions ', {})
const socialIdBySocialKey = new Map<string, PersonId | null>()
const iterator = await client.traverse(DOMAIN_REACTION, { _class: activity.class.Reaction })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const reaction = doc as Reaction
const newCreateBy =
(await getSocialIdBySocialKey(client, reaction.createBy, socialIdBySocialKey)) ?? reaction.createBy
if (newCreateBy === reaction.createBy) continue
operations.push({
filter: { _id: doc._id },
update: {
createBy: newCreateBy
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_REACTION, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing activity reactions ', {})
}
export const activityOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, activityId, [
@@ -543,6 +599,12 @@ export const activityOperation: MigrateOperation = {
state: 'social-ids-in-doc-updates',
mode: 'upgrade',
func: migrateSocialIdsInDocUpdates
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'social-keys-to-social-ids',
mode: 'upgrade',
func: migrateSocialKeysToSocialIds
}
])
},
+4 -4
View File
@@ -13,12 +13,12 @@
// limitations under the License.
//
import { type Builder, Model, Prop, ReadOnly, TypeString, UX, TypeBoolean } from '@hcengineering/model'
import { type Builder, Model, Prop, ReadOnly, TypeString, UX, TypeBoolean, TypeAccountUuid } from '@hcengineering/model'
import { type OnboardingChannel } from '@hcengineering/analytics-collector'
import chunter from '@hcengineering/chunter'
import { TChannel } from '@hcengineering/model-chunter'
import activity, { type ActivityMessageControl } from '@hcengineering/activity'
import core, { type PersonId, type WorkspaceUuid } from '@hcengineering/core'
import core, { type AccountUuid, type WorkspaceUuid } from '@hcengineering/core'
import analyticsCollector from './plugin'
@@ -40,9 +40,9 @@ export class TOnboardingChannel extends TChannel implements OnboardingChannel {
@ReadOnly()
userName!: string
@Prop(TypeString(), analyticsCollector.string.SocialId)
@Prop(TypeAccountUuid(), analyticsCollector.string.Account)
@ReadOnly()
socialString!: PersonId
account!: AccountUuid
@Prop(TypeString(), analyticsCollector.string.WorkspaceName)
@ReadOnly()
+4 -53
View File
@@ -13,19 +13,16 @@
// limitations under the License.
//
import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector'
import {
tryMigrate,
type MigrateOperation,
type MigrateUpdate,
type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient
type MigrationUpgradeClient,
tryMigrate
} from '@hcengineering/model'
import { DOMAIN_SPACE, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import { analyticsCollectorId } from '@hcengineering/analytics-collector'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import { buildSocialIdString, type Doc, MeasureMetricsContext } from '@hcengineering/core'
async function removeOnboardingChannels (client: MigrationClient): Promise<void> {
const channels = await client.find(DOMAIN_SPACE, { 'analytics:mixin:AnalyticsChannel': { $exists: true } })
@@ -44,48 +41,6 @@ async function removeOnboardingChannels (client: MigrationClient): Promise<void>
await client.deleteMany(DOMAIN_SPACE, { _id: { $in: channelsIds } })
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('analytics collector migrateAccountsToSocialIds', {})
ctx.info('processing analytics collector onboarding channels ', {})
const iterator = await client.traverse(DOMAIN_SPACE, { _class: analyticsCollector.class.OnboardingChannel })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const email = (doc as any).email
if (email === undefined || email === '') continue
const socialString = buildSocialIdString(getSocialKeyByOldEmail(email))
operations.push({
filter: { _id: doc._id },
update: {
socialString
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_SPACE, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing analytics collector onboarding channels ', {})
}
export const analyticsCollectorOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, analyticsCollectorId, [
@@ -93,10 +48,6 @@ export const analyticsCollectorOperation: MigrateOperation = {
state: 'remove-analytics-channels-v3',
mode: 'upgrade',
func: removeOnboardingChannels
},
{
state: 'accounts-to-social-ids',
func: migrateAccountsToSocialIds
}
])
},
+97 -7
View File
@@ -14,7 +14,7 @@
//
import { type Calendar, calendarId, type Event, type ReccuringEvent } from '@hcengineering/calendar'
import { type Doc, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core'
import { type AccountUuid, type Doc, MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core'
import {
createDefaultSpace,
type MigrateUpdate,
@@ -25,18 +25,18 @@ import {
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_SPACE, getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_CALENDAR, DOMAIN_EVENT } from '.'
import calendar from './plugin'
function getCalendarId (socialString: PersonId): Ref<Calendar> {
return `${socialString}_calendar` as Ref<Calendar>
function getCalendarId (val: string): Ref<Calendar> {
return `${val}_calendar` as Ref<Calendar>
}
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('calendar migrateAccountsToSocialIds', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const eventClasses = hierarchy.getDescendants(calendar.class.Event)
const calendars = await client.find<Calendar>(DOMAIN_CALENDAR, {
@@ -53,7 +53,7 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
}
const account = id.substring(0, id.length - 9)
const socialId = socialIdByAccount[account]
const socialId = socialKeyByAccount[account]
if (socialId === undefined) {
ctx.warn('no socialId for account', { account })
continue
@@ -88,7 +88,7 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
}
const account = id.substring(0, id.length - 9)
const socialId = socialIdByAccount[account]
const socialId = socialKeyByAccount[account]
if (socialId === undefined) {
ctx.warn('no socialId for account', { account })
continue
@@ -116,6 +116,90 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
}
}
async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('calendar migrateSocialIdsToAccountUuids', {})
const hierarchy = client.hierarchy
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
const eventClasses = hierarchy.getDescendants(calendar.class.Event)
const calendars = await client.find<Calendar>(DOMAIN_CALENDAR, {
_class: calendar.class.Calendar
})
ctx.info('processing internal calendars')
for (const calendar of calendars) {
const id = calendar._id
if (!id.endsWith('_calendar')) {
ctx.warn('Wrong calendar id format', { calendar: calendar._id })
continue
}
const socialKey = id.substring(0, id.length - 9)
const accountUuid = await getAccountUuidBySocialKey(client, socialKey, accountUuidBySocialKey)
if (accountUuid == null) {
ctx.warn('no account uuid for social key', { socialKey })
continue
}
await client.delete(DOMAIN_CALENDAR, calendar._id)
await client.create(DOMAIN_CALENDAR, {
...calendar,
_id: getCalendarId(accountUuid)
})
}
let processedEvents = 0
const eventsIterator = await client.traverse<Event>(DOMAIN_EVENT, {
_class: { $in: eventClasses }
})
try {
while (true) {
const events = await eventsIterator.next(200)
if (events === null || events.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const event of events) {
const id = event.calendar
if (!id.endsWith('_calendar')) {
// Nothing to do, in external calendar
continue
}
const socialKey = id.substring(0, id.length - 9)
const accountUuid = await getAccountUuidBySocialKey(client, socialKey, accountUuidBySocialKey)
if (accountUuid == null) {
ctx.warn('no account uuid for social key', { socialKey })
continue
}
operations.push({
filter: { _id: event._id },
update: {
calendar: getCalendarId(accountUuid)
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_EVENT, operations)
}
processedEvents += events.length
ctx.info('...processed events', { count: processedEvents })
}
ctx.info('finished processing events')
} finally {
await eventsIterator.close()
}
}
async function migrateCalendars (client: MigrationClient): Promise<void> {
await client.move(
DOMAIN_SPACE,
@@ -242,7 +326,13 @@ export const calendarOperation: MigrateOperation = {
},
{
state: 'accounts-to-social-ids',
mode: 'upgrade',
func: migrateAccountsToSocialIds
},
{
state: 'migrate-social-ids-to-account-uuids',
mode: 'upgrade',
func: migrateSocialIdsToAccountUuids
}
])
},
+6 -5
View File
@@ -61,7 +61,7 @@ import {
TypeBoolean,
TypeCollaborativeDoc,
TypeDate,
TypePersonId,
TypeNumber,
TypeRecord,
TypeRef,
TypeString,
@@ -167,12 +167,13 @@ export class TChannel extends TAttachedDoc implements Channel {
@Model(contact.class.SocialIdentity, core.class.AttachedDoc, DOMAIN_CHANNEL)
@UX(contact.string.SocialId)
export class TSocialIdentity extends TAttachedDoc implements SocialIdentity {
declare _id: Ref<this> & PersonId
declare attachedTo: Ref<Person>
declare attachedToClass: Ref<Class<Person>>
@Prop(TypePersonId(), getEmbeddedLabel('Key'))
@Prop(TypeString(), getEmbeddedLabel('Key'))
@Hidden()
key!: PersonId
key!: string
@Prop(TypeString(), contact.string.Type)
type!: SocialIdType
@@ -181,9 +182,9 @@ export class TSocialIdentity extends TAttachedDoc implements SocialIdentity {
@Index(IndexKind.FullText)
value!: string
@Prop(TypeBoolean(), contact.string.Confirmed)
@Prop(TypeNumber(), contact.string.Confirmed)
@ReadOnly()
confirmed!: boolean
verifiedOn?: number
}
@Model(contact.class.Person, contact.class.Contact)
+129 -10
View File
@@ -1,6 +1,14 @@
//
import { AvatarType, type Person, type Contact, type SocialIdentity } from '@hcengineering/contact'
import {
AvatarType,
type Person,
type Contact,
type SocialIdentity,
type SocialIdentityRef,
getFirstName,
getLastName
} from '@hcengineering/contact'
import {
AccountRole,
buildSocialIdString,
@@ -9,8 +17,8 @@ import {
type Domain,
DOMAIN_MODEL_TX,
DOMAIN_TX,
generateId,
MeasureMetricsContext,
type PersonId,
type Ref,
type Space,
type TxCUD
@@ -22,11 +30,12 @@ import {
type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient,
type ModelLogger,
tryMigrate,
tryUpgrade
} from '@hcengineering/model'
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import core, { getAccountsFromTxes, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import core, { getAccountsFromTxes, getSocialIdBySocialKey, getSocialKeyByOldEmail } from '@hcengineering/model-core'
import { DOMAIN_VIEW } from '@hcengineering/model-view'
import contact, { contactId, DOMAIN_CHANNEL, DOMAIN_CONTACT } from './index'
@@ -122,7 +131,7 @@ async function fillAccountUuids (client: MigrationClient): Promise<void> {
)[0]
if (socialIdentity == null) continue
const accountUuid = await client.accountClient.findPerson(socialIdentity.key)
const accountUuid = await client.accountClient.findPersonBySocialKey(socialIdentity.key)
if (accountUuid == null) {
continue
}
@@ -150,6 +159,66 @@ async function fillAccountUuids (client: MigrationClient): Promise<void> {
}
}
async function fillSocialIdentitiesIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('contact fillSocialIdentitiesIds', {})
ctx.info('filling social identities genenrated ids...')
const socialIdBySocialKey = new Map<string, PersonId | null>()
const iterator = await client.traverse<SocialIdentity>(DOMAIN_CHANNEL, { _class: contact.class.SocialIdentity })
let count = 0
try {
let newSids: SocialIdentity[] = []
let newSidIds = new Set<Ref<SocialIdentity>>()
let deleteSids: Ref<SocialIdentity>[] = []
while (true) {
const socialIdentities = await iterator.next(200)
if (socialIdentities === null || socialIdentities.length === 0) {
break
}
for (const socialIdentity of socialIdentities) {
const socialId = await getSocialIdBySocialKey(client, socialIdentity.key, socialIdBySocialKey)
if (socialId == null || socialId === socialIdentity._id) continue
const socialIdRef = socialId as SocialIdentityRef
// Some old data might contain duplicate accounts for github users
// so need to filter just in case
if (!newSidIds.has(socialIdRef)) {
newSidIds.add(socialIdRef)
newSids.push({
...socialIdentity,
_id: socialIdRef
})
}
deleteSids.push(socialIdentity._id)
count++
if (newSids.length > 50) {
await client.create(DOMAIN_CHANNEL, newSids)
await client.deleteMany(DOMAIN_CHANNEL, { _id: { $in: deleteSids } })
newSids = []
newSidIds = new Set()
deleteSids = []
}
}
}
if (newSids.length > 0) {
await client.create(DOMAIN_CHANNEL, newSids)
await client.deleteMany(DOMAIN_CHANNEL, { _id: { $in: deleteSids } })
newSids = []
newSidIds = new Set()
deleteSids = []
}
ctx.info('finished filling social identities genenrated ids. Updated count: ', { count })
} finally {
await iterator.close()
}
}
async function assignWorkspaceRoles (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('contact assignWorkspaceRoles', {})
ctx.info('assigning workspace roles...')
@@ -166,7 +235,7 @@ async function assignWorkspaceRoles (client: MigrationClient): Promise<void> {
}
const socialKey = getSocialKeyByOldEmail(email)
try {
await client.accountClient.updateWorkspaceRoleBySocialId(buildSocialIdString(socialKey), role)
await client.accountClient.updateWorkspaceRoleBySocialKey(buildSocialIdString(socialKey), role)
} catch (err: any) {
ctx.error('Failed to update workspace role', { email, ...socialKey, role, err })
}
@@ -230,6 +299,7 @@ async function createSocialIdentities (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('createSocialIdentities', {})
ctx.info('processing person accounts ', {})
const socialIdBySocialKey = new Map<string, PersonId | null>()
const personAccountsTxes: any[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: 'contact:class:PersonAccount' as Ref<Class<Doc>>
})
@@ -240,13 +310,17 @@ async function createSocialIdentities (client: MigrationClient): Promise<void> {
if (email === '') continue
const socialIdKey = getSocialKeyByOldEmail(email)
const socialId: SocialIdentity = {
_id: generateId(),
const socialKey = buildSocialIdString(socialIdKey)
const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)
if (socialId == null) continue
const socialIdObj: SocialIdentity = {
_id: socialId as SocialIdentityRef,
_class: contact.class.SocialIdentity,
space: contact.space.Contacts,
...socialIdKey,
key: buildSocialIdString(socialIdKey),
confirmed: false,
key: socialKey,
attachedTo: pAcc.person,
attachedToClass: contact.class.Person,
@@ -258,11 +332,46 @@ async function createSocialIdentities (client: MigrationClient): Promise<void> {
modifiedBy: core.account.ConfigUser
}
await client.create(DOMAIN_CHANNEL, socialId)
await client.create(DOMAIN_CHANNEL, socialIdObj)
}
}
async function ensureGlobalPersonsForLocalAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('contact ensureGlobalPersonsForLocalAccounts', {})
ctx.info('ensuring global persons for local accounts ', {})
const personAccountsTxes: any[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: 'contact:class:PersonAccount' as Ref<Class<Doc>>
})
const personAccounts = getAccountsFromTxes(personAccountsTxes)
let count = 0
for (const pAcc of personAccounts) {
const email: string = pAcc.email ?? ''
if (email === '') continue
const socialIdKey = getSocialKeyByOldEmail(email)
const person = (await client.find<Person>(DOMAIN_CONTACT, { _id: pAcc.person }))[0]
const name = person?.name
const firstName = getFirstName(name)
const lastName = getLastName(name)
const effectiveFirstName = firstName === '' ? socialIdKey.value : firstName
await client.accountClient.ensurePerson(socialIdKey.type, socialIdKey.value, effectiveFirstName, lastName)
count++
}
ctx.info('finished ensuring global persons for local accounts. Total persons ensured: ', { count })
}
export const contactOperation: MigrateOperation = {
async preMigrate (client: MigrationClient, logger: ModelLogger, mode): Promise<void> {
await tryMigrate(mode, client, contactId, [
{
state: 'ensure-accounts-global-persons',
func: (client) => ensureGlobalPersonsForLocalAccounts(client)
}
])
},
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, contactId, [
{
@@ -407,19 +516,29 @@ export const contactOperation: MigrateOperation = {
},
{
state: 'create-social-identities',
mode: 'upgrade',
func: createSocialIdentities
},
{
state: 'assign-workspace-roles',
mode: 'upgrade',
func: assignWorkspaceRoles
},
{
state: 'fill-account-uuids',
mode: 'upgrade',
func: fillAccountUuids
},
{
state: 'assign-employee-roles-v1',
mode: 'upgrade',
func: assignEmployeeRoles
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'fill-social-identities-ids',
mode: 'upgrade',
func: fillSocialIdentitiesIds
}
])
},
+5 -3
View File
@@ -84,13 +84,15 @@ export { coreId, DOMAIN_SPACE } from '@hcengineering/core'
export * from './core'
export {
coreOperation,
getSocialIdByOldAccount,
getSocialKeyByOldAccount,
getAccountsFromTxes,
getSocialKeyByOldEmail,
getAccountUuidBySocialId,
getAccountUuidBySocialKey,
getUniqueAccounts,
getAccountUuidByOldAccount,
getUniqueAccountsFromOldAccounts
getUniqueAccountsFromOldAccounts,
getSocialIdBySocialKey,
getSocialIdFromOldAccount
} from './migration'
export * from './security'
export * from './status'
+181 -50
View File
@@ -309,27 +309,27 @@ export function getAccountsFromTxes (accTxes: TxCUD<Doc>[]): any {
.filter((it) => it !== undefined)
}
export async function getSocialIdByOldAccount (client: MigrationClient): Promise<Record<string, PersonId>> {
export async function getSocialKeyByOldAccount (client: MigrationClient): Promise<Record<string, string>> {
const systemAccounts = [core.account.System, core.account.ConfigUser]
const accountsTxes: TxCUD<Doc>[] = await client.find<TxCUD<Doc>>(DOMAIN_MODEL_TX, {
objectClass: { $in: ['core:class:Account', 'contact:class:PersonAccount'] as Ref<Class<Doc>>[] }
})
const accounts = getAccountsFromTxes(accountsTxes)
const socialIdByAccount: Record<string, PersonId> = {}
const socialKeyByAccount: Record<string, PersonId> = {}
for (const account of accounts) {
if (account.email === undefined) {
continue
}
if (systemAccounts.includes(account._id)) {
socialIdByAccount[account._id] = account._id
socialKeyByAccount[account._id] = account._id
} else {
socialIdByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email))
socialKeyByAccount[account._id] = buildSocialIdString(getSocialKeyByOldEmail(account.email)) as any
}
}
return socialIdByAccount
return socialKeyByAccount
}
export function getSocialKeyByOldEmail (rawEmail: string): SocialKey {
@@ -362,7 +362,9 @@ export function getSocialKeyByOldEmail (rawEmail: string): SocialKey {
async function migrateAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('core migrateAccounts', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const socialIdBySocialKey = new Map<string, PersonId | null>()
const socialIdByOldAccount = new Map<string, PersonId | null>()
ctx.info('migrating createdBy and modifiedBy')
function chunkArray<T> (array: T[], chunkSize: number): T[][] {
@@ -379,9 +381,16 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const groupByCreated = await client.groupBy<any, Doc>(domain, 'createdBy', {})
const groupByModified = await client.groupBy<any, Doc>(domain, 'modifiedBy', {})
groupByCreated.forEach((_, accId) => {
const socialId = socialIdByAccount[accId]
if (socialId == null || accId === socialId) return
for (const accId of groupByCreated.keys()) {
if (accId == null) continue
const socialId = await getSocialIdFromOldAccount(
client,
accId,
socialKeyByAccount,
socialIdBySocialKey,
socialIdByOldAccount
)
if (socialId == null || accId === socialId) continue
operations.push({
filter: { createdBy: accId },
@@ -389,11 +398,18 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
createdBy: socialId
}
})
})
}
groupByModified.forEach((_, accId) => {
const socialId = socialIdByAccount[accId]
if (socialId == null || accId === socialId) return
for (const accId of groupByModified.keys()) {
if (accId == null) continue
const socialId = await getSocialIdFromOldAccount(
client,
accId,
socialKeyByAccount,
socialIdBySocialKey,
socialIdByOldAccount
)
if (socialId == null || accId === socialId) continue
operations.push({
filter: { modifiedBy: accId },
@@ -401,7 +417,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
modifiedBy: socialId
}
})
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
@@ -437,7 +453,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
}
}
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing spaces members, owners and roles assignment', {})
let processedSpaces = 0
@@ -459,14 +475,14 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const newMembers = await getUniqueAccountsFromOldAccounts(
client,
space.members,
socialIdByAccount,
accountUuidBySocialId
socialKeyByAccount,
accountUuidBySocialKey
)
const newOwners = await getUniqueAccountsFromOldAccounts(
client,
space.owners ?? [],
socialIdByAccount,
accountUuidBySocialId
socialKeyByAccount,
accountUuidBySocialKey
)
const update: MigrateUpdate<Space> = {
members: newMembers as any,
@@ -486,8 +502,8 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const newAssignees = await getUniqueAccountsFromOldAccounts(
client,
oldAssignees,
socialIdByAccount,
accountUuidBySocialId
socialKeyByAccount,
accountUuidBySocialKey
)
update[`${type.targetClass}`] = {
@@ -525,8 +541,8 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const newMembers = await getUniqueAccountsFromOldAccounts(
client,
spaceType.members,
socialIdByAccount,
accountUuidBySocialId
socialKeyByAccount,
accountUuidBySocialKey
)
const tx: TxUpdateDoc<SpaceType> = {
_id: generateId(),
@@ -550,41 +566,41 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes })
}
export async function getAccountUuidBySocialId (
export async function getAccountUuidBySocialKey (
client: MigrationClient,
socialId: PersonId,
accountUuidBySocialId: Map<PersonId, AccountUuid | null>
socialKey: string,
accountUuidBySocialKey: Map<string, AccountUuid | null>
): Promise<AccountUuid | null> {
if (socialId === core.account.System) {
if (socialKey === core.account.System) {
return systemAccountUuid
}
if (socialId === core.account.ConfigUser) {
if (socialKey === core.account.ConfigUser) {
return configUserAccountUuid
}
const cached = accountUuidBySocialId.has(socialId)
const cached = accountUuidBySocialKey.has(socialKey)
if (!cached) {
const personUuid = await client.accountClient.findPerson(socialId)
const personUuid = await client.accountClient.findPersonBySocialKey(socialKey)
if (personUuid === undefined) {
console.log('Could not find person for', socialId)
console.log('Could not find person for', socialKey)
}
accountUuidBySocialId.set(socialId, (personUuid as AccountUuid | undefined) ?? null)
accountUuidBySocialKey.set(socialKey, (personUuid as AccountUuid | undefined) ?? null)
}
return accountUuidBySocialId.get(socialId) ?? null
return accountUuidBySocialKey.get(socialKey) ?? null
}
export async function getUniqueAccounts (
client: MigrationClient,
persons: PersonId[],
accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
socialKeys: string[],
accountUuidBySocialKey = new Map<string, AccountUuid | null>()
): Promise<AccountUuid[]> {
const accounts = new Set<AccountUuid>()
for (const person of persons) {
let newAccount = await getAccountUuidBySocialId(client, person as unknown as PersonId, accountUuidBySocialId)
for (const person of socialKeys) {
let newAccount = await getAccountUuidBySocialKey(client, person, accountUuidBySocialKey)
if (newAccount == null && isUuid(person)) {
newAccount = person as unknown as AccountUuid
@@ -600,7 +616,7 @@ export async function getUniqueAccounts (
export async function getAccountUuidByOldAccount (
client: MigrationClient,
oldAccount: string,
socialIdByOldAccount: Record<string, PersonId>,
socialKeyByOldAccount: Record<string, string>,
accountUuidByOldAccount: Map<string, AccountUuid | null>
): Promise<AccountUuid | null> {
if (oldAccount === core.account.System) {
@@ -614,13 +630,13 @@ export async function getAccountUuidByOldAccount (
const cached = accountUuidByOldAccount.has(oldAccount)
if (!cached) {
const socialId = socialIdByOldAccount[oldAccount]
const socialId = socialKeyByOldAccount[oldAccount]
if (socialId == null) {
accountUuidByOldAccount.set(oldAccount, null)
return null
}
const personUuid = await client.accountClient.findPerson(socialId)
const personUuid = await client.accountClient.findPersonBySocialKey(socialId)
accountUuidByOldAccount.set(oldAccount, (personUuid as AccountUuid | undefined) ?? null)
}
@@ -628,6 +644,42 @@ export async function getAccountUuidByOldAccount (
return accountUuidByOldAccount.get(oldAccount) ?? null
}
export async function getSocialIdBySocialKey (
client: MigrationClient,
socialKey: string,
socialIdBySocialKey?: Map<string, PersonId | null>
): Promise<PersonId | null> {
if ([core.account.System, core.account.ConfigUser].includes(socialKey as PersonId)) {
return socialKey as PersonId
}
if (socialIdBySocialKey == null || !socialIdBySocialKey.has(socialKey)) {
const val = (await client.accountClient.findSocialIdBySocialKey(socialKey)) ?? null
if (socialIdBySocialKey == null) return val
socialIdBySocialKey.set(socialKey, val)
}
return socialIdBySocialKey.get(socialKey) ?? null
}
export async function getSocialIdFromOldAccount (
client: MigrationClient,
oldAccount: string,
socialKeyByOldAccount: Record<string, string>,
socialIdBySocialKey: Map<string, PersonId | null>,
socialIdByOldAccount: Map<string, PersonId | null>
): Promise<PersonId | null> {
if (!socialIdByOldAccount.has(oldAccount)) {
const socialKey = socialKeyByOldAccount[oldAccount]
if (socialKey == null) return null
socialIdByOldAccount.set(oldAccount, await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey))
}
return socialIdByOldAccount.get(oldAccount) ?? null
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
function isUuid (val: string): boolean {
return uuidRegex.test(val)
@@ -636,12 +688,12 @@ function isUuid (val: string): boolean {
export async function getUniqueAccountsFromOldAccounts (
client: MigrationClient,
oldAccounts: string[],
socialIdByOldAccount: Record<string, PersonId>,
socialKeyByOldAccount: Record<string, string>,
accountUuidByOldAccount: Map<string, AccountUuid | null> = new Map<string, AccountUuid | null>()
): Promise<AccountUuid[]> {
const accounts = new Set<AccountUuid>()
for (const oldAcc of oldAccounts) {
let newAccount = await getAccountUuidByOldAccount(client, oldAcc, socialIdByOldAccount, accountUuidByOldAccount)
let newAccount = await getAccountUuidByOldAccount(client, oldAcc, socialKeyByOldAccount, accountUuidByOldAccount)
if (newAccount == null && isUuid(oldAcc)) {
newAccount = oldAcc as unknown as AccountUuid
@@ -656,8 +708,8 @@ export async function getUniqueAccountsFromOldAccounts (
}
/**
* Migrates social ids to new accounts where needed.
* Should only be applied to staging where old accounts have already been migrated to social ids.
* Migrates social keys to new accounts where needed.
* Should only be applied to staging where old accounts have already been migrated to social keys.
* REMOVE IT BEFORE MERGING TO PRODUCTION
* @param client
* @returns
@@ -665,7 +717,7 @@ export async function getUniqueAccountsFromOldAccounts (
async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('core migrateSpaceMembersToAccountUuids', {})
const hierarchy = client.hierarchy
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
const spaceTypes = client.model.findAllSync(core.class.SpaceType, {})
const spaceTypesById = toIdMap(spaceTypes)
@@ -698,8 +750,8 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom
if (!hierarchy.isDerived(s._class, core.class.Space)) continue
const space = s as Space
const update: MigrateUpdate<Space> = {
members: await getUniqueAccounts(client, space.members as unknown as PersonId[], accountUuidBySocialId),
owners: await getUniqueAccounts(client, (space.owners ?? []) as unknown as PersonId[], accountUuidBySocialId)
members: await getUniqueAccounts(client, space.members, accountUuidBySocialKey),
owners: await getUniqueAccounts(client, space.owners ?? [], accountUuidBySocialKey)
}
const type = spaceTypesById.get((space as TypedSpace).type)
@@ -712,7 +764,7 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom
for (const role of roles ?? []) {
const oldAssignees: PersonId[] | undefined = (mixin as any)[role._id]
if (oldAssignees != null && oldAssignees.length > 0) {
const newAssignees = await getUniqueAccounts(client, oldAssignees, accountUuidBySocialId)
const newAssignees = await getUniqueAccounts(client, oldAssignees, accountUuidBySocialKey)
update[`${type.targetClass}`] = {
[role._id]: newAssignees
@@ -749,7 +801,7 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom
const newMembers = await getUniqueAccounts(
client,
spaceType.members as unknown as PersonId[],
accountUuidBySocialId
accountUuidBySocialKey
)
const tx: TxUpdateDoc<SpaceType> = {
_id: generateId(),
@@ -773,6 +825,79 @@ async function migrateSpaceMembersToAccountUuids (client: MigrationClient): Prom
ctx.info('finished processing space types members', { totalSpaceTypes: spaceTypes.length, updatedSpaceTypes })
}
/**
* Migrates social keys to social ids where needed.
* Should only be applied to staging where old accounts have already been migrated to social keys.
* REMOVE IT BEFORE MERGING TO PRODUCTION
* @param client
* @returns
*/
async function migrateCreatedByToGenSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('core migrateCreatedByToGenSocialIds', {})
const socialIdBySocialKey = new Map<string, PersonId | null>()
ctx.info('migrating createdBy and modifiedBy')
function chunkArray<T> (array: T[], chunkSize: number): T[][] {
const chunks: T[][] = []
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize))
}
return chunks
}
for (const domain of client.hierarchy.domains()) {
ctx.info('processing domain ', { domain })
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
const groupByCreated = await client.groupBy<any, Doc>(domain, 'createdBy', {})
const groupByModified = await client.groupBy<any, Doc>(domain, 'modifiedBy', {})
for (const socialKey of groupByCreated.keys()) {
if (socialKey == null) continue
const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)
if (socialId == null || socialKey === socialId) continue
operations.push({
filter: { createdBy: socialKey },
update: {
createdBy: socialId
}
})
}
for (const socialKey of groupByModified.keys()) {
if (socialKey == null) continue
const socialId = await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)
if (socialId == null || socialKey === socialId) continue
operations.push({
filter: { modifiedBy: socialKey },
update: {
modifiedBy: socialId
}
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
ctx.info('chunks to process ', { total: operationsChunks.length })
let processed = 0
for (const operationsChunk of operationsChunks) {
if (operationsChunk.length === 0) continue
await client.bulk(domain, operationsChunk)
processed++
if (operationsChunks.length > 1) {
ctx.info('processed chunk', { processed, of: operationsChunks.length })
}
}
} else {
ctx.info('no social keys to migrate')
}
}
ctx.info('finished migrating createdBy and modifiedBy')
}
async function processMigrateJsonForDomain (
ctx: MeasureContext,
domain: Domain,
@@ -990,9 +1115,15 @@ export const coreOperation: MigrateOperation = {
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'space-members-to-account-uuids',
state: 'created-by-to-account-uuids',
mode: 'upgrade',
func: migrateSpaceMembersToAccountUuids
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'created-by-to-gen-social-ids',
mode: 'upgrade',
func: migrateCreatedByToGenSocialIds
}
])
},
+5 -14
View File
@@ -14,16 +14,7 @@
//
import activity from '@hcengineering/activity'
import type {
CollectionSize,
MarkupBlobRef,
Domain,
Rank,
Ref,
Role,
RolesAssignment,
PersonId
} from '@hcengineering/core'
import type { CollectionSize, MarkupBlobRef, Domain, Rank, Ref, Role, RolesAssignment } from '@hcengineering/core'
import { AccountUuid, AccountRole, IndexKind } from '@hcengineering/core'
import {
type Document,
@@ -45,8 +36,8 @@ import {
TypeNumber,
TypeRef,
TypeString,
TypePersonId,
UX
UX,
TypeAccountUuid
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
@@ -89,9 +80,9 @@ export class TDocument extends TDoc implements Document, Todoable {
@Hidden()
declare space: Ref<Teamspace>
@Prop(TypePersonId(), document.string.LockedBy)
@Prop(TypeAccountUuid(), document.string.LockedBy)
@Hidden()
lockedBy?: PersonId
lockedBy?: AccountUuid
@Prop(Collection(attachment.class.Embedding), attachment.string.Embeddings)
embeddings?: number
+57 -4
View File
@@ -22,7 +22,8 @@ import {
SortingOrder,
type Class,
type CollaborativeDoc,
type Doc
type Doc,
type AccountUuid
} from '@hcengineering/core'
import { type Document, type DocumentSnapshot, type Teamspace } from '@hcengineering/document'
import {
@@ -35,7 +36,7 @@ import {
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import core, { DOMAIN_SPACE, getSocialIdByOldAccount } from '@hcengineering/model-core'
import core, { DOMAIN_SPACE, getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core'
import { DOMAIN_NOTIFICATION } from '@hcengineering/notification'
import { type Asset } from '@hcengineering/platform'
import { makeRank } from '@hcengineering/rank'
@@ -292,7 +293,52 @@ async function migrateRanks (client: MigrationClient): Promise<void> {
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('document migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
ctx.info('processing document lockedBy ', {})
const iterator = await client.traverse(DOMAIN_DOCUMENT, { _class: document.class.Document })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<Document>, update: MigrateUpdate<Document> }[] = []
for (const doc of docs) {
const document = doc as Document
const newLockedBy: any =
document.lockedBy != null ? socialKeyByAccount[document.lockedBy] ?? document.lockedBy : document.lockedBy
if (newLockedBy === document.lockedBy) continue
operations.push({
filter: { _id: document._id },
update: {
lockedBy: newLockedBy
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_DOCUMENT, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing document lockedBy ', {})
}
async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('document migrateSocialIdsToGlobalAccounts', {})
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing document lockedBy ', {})
const iterator = await client.traverse(DOMAIN_DOCUMENT, { _class: document.class.Document })
@@ -310,7 +356,9 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
for (const doc of docs) {
const document = doc as Document
const newLockedBy =
document.lockedBy != null ? socialIdByAccount[document.lockedBy] ?? document.lockedBy : document.lockedBy
document.lockedBy != null
? (await getAccountUuidBySocialKey(client, document.lockedBy, accountUuidBySocialKey)) ?? document.lockedBy
: document.lockedBy
if (newLockedBy === document.lockedBy) continue
@@ -413,6 +461,11 @@ export const documentOperation: MigrateOperation = {
state: 'migrateEmbeddingsRefs',
mode: 'upgrade',
func: migrateEmbeddingsRefs
},
{
state: 'social-ids-to-global-accounts',
mode: 'upgrade',
func: migrateSocialIdsToGlobalAccounts
}
])
},
+88 -17
View File
@@ -45,11 +45,13 @@ import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import {
DOMAIN_SPACE,
getSocialIdByOldAccount,
getSocialKeyByOldAccount,
getUniqueAccounts,
getAccountUuidBySocialId,
getAccountUuidBySocialKey,
getAccountUuidByOldAccount,
getUniqueAccountsFromOldAccounts
getUniqueAccountsFromOldAccounts,
getSocialIdBySocialKey,
getSocialIdFromOldAccount
} from '@hcengineering/model-core'
import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY } from './index'
@@ -249,7 +251,9 @@ export async function migrateDuplicateContexts (client: MigrationClient): Promis
async function migrateAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('notification migrateAccounts', {})
const hierarchy = client.hierarchy
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const socialIdBySocialKey = new Map<string, PersonId | null>()
const socialIdByOldAccount = new Map<string, PersonId | null>()
const accountUuidByOldAccount = new Map<string, AccountUuid | null>()
ctx.info('processing collaborators ', {})
@@ -276,7 +280,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const newCollaborators = await getUniqueAccountsFromOldAccounts(
client,
oldCollaborators,
socialIdByAccount,
socialKeyByAccount,
accountUuidByOldAccount
)
@@ -329,8 +333,9 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
})
for (const oldAccId of groupByUser.keys()) {
const newAccId = await getAccountUuidByOldAccount(client, oldAccId, socialIdByAccount, accountUuidByOldAccount)
if (newAccId == null || oldAccId === newAccId) return
if (oldAccId == null) continue
const newAccId = await getAccountUuidByOldAccount(client, oldAccId, socialKeyByAccount, accountUuidByOldAccount)
if (newAccId == null || oldAccId === newAccId) continue
operations.push({
filter: {
@@ -356,20 +361,27 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
_class: notification.class.BrowserNotification
})
groupBySenderId.forEach((_, accId) => {
const socialId = socialIdByAccount[accId]
if (socialId == null || accId === socialId) return
for (const oldAccId of groupBySenderId.keys()) {
if (oldAccId == null) continue
const socialId = await getSocialIdFromOldAccount(
client,
oldAccId,
socialKeyByAccount,
socialIdBySocialKey,
socialIdByOldAccount
)
if (socialId == null || oldAccId === socialId) continue
operations.push({
filter: {
senderId: accId,
senderId: oldAccId,
_class: notification.class.BrowserNotification
},
update: {
senderId: socialId
}
})
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
@@ -408,7 +420,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
for (const doc of docs) {
const oldUser: any = doc.user
const newUser = await getAccountUuidByOldAccount(client, oldUser, socialIdByAccount, accountUuidByOldAccount)
const newUser = await getAccountUuidByOldAccount(client, oldUser, socialKeyByAccount, accountUuidByOldAccount)
if (newUser != null && newUser !== oldUser) {
operations.push({
@@ -443,7 +455,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('notification migrateSocialIdsToAccountUuids', {})
const hierarchy = client.hierarchy
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing collaborators ', {})
for (const domain of client.hierarchy.domains()) {
@@ -466,7 +478,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise
if (oldCollaborators === undefined || oldCollaborators.length === 0) continue
const newCollaborators = await getUniqueAccounts(client, oldCollaborators, accountUuidBySocialId)
const newCollaborators = await getUniqueAccounts(client, oldCollaborators, accountUuidBySocialKey)
operations.push({
filter: { _id: doc._id },
@@ -517,7 +529,8 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise
})
for (const socialId of groupByUser.keys()) {
const account = await getAccountUuidBySocialId(client, socialId, accountUuidBySocialId)
if (socialId == null) continue
const account = await getAccountUuidBySocialKey(client, socialId, accountUuidBySocialKey)
if (account == null || (account as unknown as PersonId) === socialId) continue
@@ -581,7 +594,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise
for (const doc of docs) {
const oldUser: any = doc.user
const newUser = await getAccountUuidBySocialId(client, oldUser, accountUuidBySocialId)
const newUser = await getAccountUuidBySocialKey(client, oldUser, accountUuidBySocialKey)
if (newUser != null && newUser !== oldUser) {
operations.push({
@@ -606,6 +619,58 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise
ctx.info('finished processing doc notify contexts ', {})
}
async function migrateSocialKeysToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('notification migrateSocialKeysToSocialIds', {})
ctx.info('processing browser notifications sender ids ', {})
const socialIdBySocialKey = new Map<string, PersonId | null>()
function chunkArray<T> (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<Doc>, update: MigrateUpdate<Doc> }[] = []
const groupBySenderId = await client.groupBy<any, Doc>(DOMAIN_NOTIFICATION, 'senderId', {
_class: notification.class.BrowserNotification
})
for (const socialKey of groupBySenderId.keys()) {
if (socialKey == null) continue
const socialId = (await getSocialIdBySocialKey(client, socialKey, socialIdBySocialKey)) ?? socialKey
if (socialId == null || socialKey === socialId) continue
operations.push({
filter: {
senderId: socialKey,
_class: notification.class.BrowserNotification
},
update: {
senderId: socialId
}
})
}
if (operations.length > 0) {
const operationsChunks = chunkArray(operations, 40)
let processed = 0
for (const operationsChunk of operationsChunks) {
if (operationsChunk.length === 0) continue
await client.bulk(DOMAIN_NOTIFICATION, operationsChunk)
processed++
if (operationsChunks.length > 1) {
ctx.info('processed chunk', { processed, of: operationsChunks.length })
}
}
} else {
ctx.info('no social keys to migrate')
}
ctx.info('finished processing browser notifications sender ids ', {})
}
export async function migrateSettings (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_PREFERENCE,
@@ -886,6 +951,12 @@ export const notificationOperation: MigrateOperation = {
state: 'migrate-social-ids-to-account-uuids',
mode: 'upgrade',
func: migrateSocialIdsToAccountUuids
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'migrate-social-keys-to-social-ids',
mode: 'upgrade',
func: migrateSocialKeysToSocialIds
}
])
},
+13 -10
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { type AccountUuid, MeasureMetricsContext, type PersonId, type Ref, type Space } from '@hcengineering/core'
import core, { type AccountUuid, MeasureMetricsContext, type Ref, type Space } from '@hcengineering/core'
import {
migrateSpace,
type MigrateUpdate,
@@ -24,7 +24,11 @@ import {
type MigrationUpgradeClient
} from '@hcengineering/model'
import setting, { type Integration, settingId } from '@hcengineering/setting'
import { getSocialIdByOldAccount, getUniqueAccounts, getUniqueAccountsFromOldAccounts } from '@hcengineering/model-core'
import {
getSocialKeyByOldAccount,
getUniqueAccounts,
getUniqueAccountsFromOldAccounts
} from '@hcengineering/model-core'
import { DOMAIN_SETTING } from '.'
@@ -36,7 +40,7 @@ import { DOMAIN_SETTING } from '.'
*/
async function migrateAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('setting migrateAccounts', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const accountUuidByOldAccount = new Map<string, AccountUuid | null>()
ctx.info('processing setting integration shared ', {})
@@ -60,7 +64,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
const newShared = await getUniqueAccountsFromOldAccounts(
client,
integration.shared,
socialIdByAccount,
socialKeyByAccount,
accountUuidByOldAccount
)
@@ -94,7 +98,7 @@ async function migrateAccounts (client: MigrationClient): Promise<void> {
*/
async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('setting migrateAccounts', {})
const accountUuidBySocialId = new Map<PersonId, AccountUuid | null>()
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing setting integration shared ', {})
const iterator = await client.traverse(DOMAIN_SETTING, { _class: setting.class.Integration })
@@ -114,11 +118,7 @@ async function migrateSocialIdsToAccountUuids (client: MigrationClient): Promise
if (integration.shared === undefined || integration.shared.length === 0) continue
const newShared = await getUniqueAccounts(
client,
integration.shared as unknown as PersonId[],
accountUuidBySocialId
)
const newShared = await getUniqueAccounts(client, integration.shared, accountUuidBySocialKey)
operations.push({
filter: { _id: integration._id },
@@ -146,17 +146,20 @@ export const settingOperation: MigrateOperation = {
await tryMigrate(mode, client, settingId, [
{
state: 'removeDeprecatedSpace',
mode: 'upgrade',
func: async (client: MigrationClient) => {
await migrateSpace(client, 'setting:space:Setting' as Ref<Space>, core.space.Workspace, [DOMAIN_SETTING])
}
},
{
state: 'accounts-to-social-ids',
mode: 'upgrade',
func: migrateAccounts
},
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{
state: 'migrate-social-ids-to-account-uuids',
mode: 'upgrade',
func: migrateSocialIdsToAccountUuids
}
])
+3 -3
View File
@@ -16,7 +16,6 @@
//
import {
type PersonId,
type Class,
type Client,
DOMAIN_MODEL,
@@ -27,7 +26,8 @@ import {
type Domain,
type Ref,
type Space,
type AnyAttribute
type AnyAttribute,
type AccountUuid
} from '@hcengineering/core'
import { type Builder, Mixin, Model, UX } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
@@ -119,7 +119,7 @@ export class TFilteredView extends TDoc implements FilteredView {
viewOptions?: ViewOptions
filterClass?: Ref<Class<Doc>>
viewletId?: Ref<Viewlet> | null
users!: PersonId[]
users!: AccountUuid[]
attachedTo!: string
sharable?: boolean
}
+55 -4
View File
@@ -23,10 +23,10 @@ import {
} from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import view, { type Filter, type FilteredView, type ViewletPreference, viewId } from '@hcengineering/view'
import { getSocialIdByOldAccount } from '@hcengineering/model-core'
import { getSocialKeyByOldAccount, getUniqueAccounts } from '@hcengineering/model-core'
import { type AccountUuid, MeasureMetricsContext } from '@hcengineering/core'
import { DOMAIN_VIEW } from '.'
import { MeasureMetricsContext } from '@hcengineering/core'
async function removeDoneStatePref (client: MigrationClient): Promise<void> {
const prefs = await client.find<ViewletPreference>(DOMAIN_PREFERENCE, {
@@ -83,7 +83,7 @@ async function removeDoneStateFilter (client: MigrationClient): Promise<void> {
async function migrateAccountsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('view migrateAccountsToSocialIds', {})
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
ctx.info('processing view filtered view users ', {})
const iterator = await client.traverse(DOMAIN_VIEW, { _class: view.class.FilteredView })
@@ -103,7 +103,52 @@ async function migrateAccountsToSocialIds (client: MigrationClient): Promise<voi
if (filteredView.users === undefined || filteredView.users.length === 0) continue
const newUsers = filteredView.users.map((u) => socialIdByAccount[u] ?? u)
const newUsers = filteredView.users.map((u) => socialKeyByAccount[u] ?? u)
operations.push({
filter: { _id: filteredView._id },
update: {
users: newUsers as any
}
})
}
if (operations.length > 0) {
await client.bulk(DOMAIN_VIEW, operations)
}
processed += docs.length
ctx.info('...processed', { count: processed })
}
} finally {
await iterator.close()
}
ctx.info('finished processing view filtered view users ', {})
}
async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('view migrateSocialIdsToGlobalAccounts', {})
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
ctx.info('processing view filtered view users ', {})
const iterator = await client.traverse(DOMAIN_VIEW, { _class: view.class.FilteredView })
try {
let processed = 0
while (true) {
const docs = await iterator.next(200)
if (docs === null || docs.length === 0) {
break
}
const operations: { filter: MigrationDocumentQuery<FilteredView>, update: MigrateUpdate<FilteredView> }[] = []
for (const doc of docs) {
const filteredView = doc as FilteredView
if (filteredView.users === undefined || filteredView.users.length === 0) continue
const newUsers = await getUniqueAccounts(client, filteredView.users, accountUuidBySocialKey)
operations.push({
filter: { _id: filteredView._id },
@@ -141,7 +186,13 @@ export const viewOperation: MigrateOperation = {
},
{
state: 'accounts-to-social-ids',
mode: 'upgrade',
func: migrateAccountsToSocialIds
},
{
state: 'social-ids-to-global-accounts',
mode: 'upgrade',
func: migrateSocialIdsToGlobalAccounts
}
])
},
+2 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole, type PersonId } from '@hcengineering/core'
import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole, type AccountUuid } from '@hcengineering/core'
import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model'
import preference, { TPreference } from '@hcengineering/model-preference'
import { createAction } from '@hcengineering/model-view'
@@ -106,7 +106,7 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent {
@Model(workbench.class.WorkbenchTab, preference.class.Preference)
@UX(workbench.string.Tab)
export class TWorkbenchTab extends TPreference implements WorkbenchTab {
declare attachedTo: PersonId
declare attachedTo: AccountUuid
location!: string
name?: string
isPinned!: boolean
+25 -4
View File
@@ -20,8 +20,8 @@ import {
} from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import workbench, { type WorkbenchTab } from '@hcengineering/workbench'
import core, { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
import { getSocialIdByOldAccount } from '@hcengineering/model-core'
import core, { type AccountUuid, DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
import { getAccountUuidBySocialKey, getSocialKeyByOldAccount } from '@hcengineering/model-core'
import { workbenchId } from '.'
@@ -32,10 +32,10 @@ async function removeTabs (client: MigrationClient): Promise<void> {
async function migrateTabsToSocialIds (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('workbench migrateTabsToSocialIds', {})
ctx.info('migrating workbench tabs to social ids...')
const socialIdByAccount = await getSocialIdByOldAccount(client)
const socialKeyByAccount = await getSocialKeyByOldAccount(client)
const tabs = await client.find<WorkbenchTab>(DOMAIN_PREFERENCE, { _class: workbench.class.WorkbenchTab })
for (const tab of tabs) {
const newAttachedTo = socialIdByAccount[tab.attachedTo]
const newAttachedTo: any = socialKeyByAccount[tab.attachedTo]
if (newAttachedTo != null && newAttachedTo !== tab.attachedTo) {
await client.update(DOMAIN_PREFERENCE, { _id: tab._id }, { attachedTo: newAttachedTo })
}
@@ -43,6 +43,21 @@ async function migrateTabsToSocialIds (client: MigrationClient): Promise<void> {
ctx.info('migrating workbench tabs to social ids completed...')
}
async function migrateSocialIdsToGlobalAccounts (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('workbench migrateSocialIdsToGlobalAccounts', {})
ctx.info('migrating workbench tabs to global accounts...')
const accountUuidBySocialKey = new Map<string, AccountUuid | null>()
const tabs = await client.find<WorkbenchTab>(DOMAIN_PREFERENCE, { _class: workbench.class.WorkbenchTab })
for (const tab of tabs) {
const newAttachedTo = await getAccountUuidBySocialKey(client, tab.attachedTo, accountUuidBySocialKey)
if (newAttachedTo != null && newAttachedTo !== tab.attachedTo) {
await client.update(DOMAIN_PREFERENCE, { _id: tab._id }, { attachedTo: newAttachedTo })
}
}
ctx.info('migrating workbench tabs to global accounts completed...')
}
export const workbenchOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, workbenchId, [
@@ -63,7 +78,13 @@ export const workbenchOperation: MigrateOperation = {
},
{
state: 'tabs-accounts-to-social-ids',
mode: 'upgrade',
func: migrateTabsToSocialIds
},
{
state: 'tabs-social-ids-to-global-accounts',
mode: 'upgrade',
func: migrateSocialIdsToGlobalAccounts
}
])
},
+26 -6
View File
@@ -80,7 +80,9 @@ export interface AccountClient {
updateWorkspaceRole: (account: string, role: AccountRole) => Promise<void>
updateWorkspaceName: (name: string) => Promise<void>
deleteWorkspace: () => Promise<void>
findPerson: (socialString: PersonId) => Promise<PersonUuid | undefined>
findPersonBySocialKey: (socialKey: string) => Promise<PersonUuid | undefined>
findPersonBySocialId: (socialId: PersonId) => Promise<PersonUuid | undefined>
findSocialIdBySocialKey: (socialKey: string) => Promise<PersonId | undefined>
// Service methods
workerHandshake: (region: string, version: Data<Version>, operation: WorkspaceOperation) => Promise<void>
@@ -104,7 +106,7 @@ export interface AccountClient {
) => Promise<boolean>
assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise<void>
updateBackupInfo: (info: BackupStatus) => Promise<void>
updateWorkspaceRoleBySocialId: (socialKey: string, targetRole: AccountRole) => Promise<void>
updateWorkspaceRoleBySocialKey: (socialKey: string, targetRole: AccountRole) => Promise<void>
ensurePerson: (
socialType: SocialIdType,
socialValue: string,
@@ -530,15 +532,33 @@ class AccountClientImpl implements AccountClient {
await this.rpc(request)
}
async findPerson (socialString: string): Promise<PersonUuid | undefined> {
async findPersonBySocialKey (socialString: string): Promise<PersonUuid | undefined> {
const request = {
method: 'findPerson' as const,
method: 'findPersonBySocialKey' as const,
params: { socialString }
}
return await this.rpc(request)
}
async findPersonBySocialId (socialId: PersonId): Promise<PersonUuid | undefined> {
const request = {
method: 'findPersonBySocialId' as const,
params: { socialId }
}
return await this.rpc(request)
}
async findSocialIdBySocialKey (socialKey: string): Promise<PersonId | undefined> {
const request = {
method: 'findSocialIdBySocialKey' as const,
params: { socialKey }
}
return await this.rpc(request)
}
async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'listWorkspaces' as const,
@@ -579,9 +599,9 @@ class AccountClientImpl implements AccountClient {
await this.rpc(request)
}
async updateWorkspaceRoleBySocialId (socialKey: string, targetRole: AccountRole): Promise<void> {
async updateWorkspaceRoleBySocialKey (socialKey: string, targetRole: AccountRole): Promise<void> {
const request = {
method: 'updateWorkspaceRoleBySocialId' as const,
method: 'updateWorkspaceRoleBySocialKey' as const,
params: { socialKey, targetRole }
}
+4 -9
View File
@@ -37,8 +37,8 @@ import {
PersonId,
TxOperations,
WorkspaceUuid,
buildSocialIdString,
generateId
generateId,
pickPrimarySocialId
} from '@hcengineering/core'
import { addLocation, getResource } from '@hcengineering/platform'
@@ -60,19 +60,14 @@ export async function connect (url: string, options: ConnectOptions): Promise<Pl
const { endpoint, token } = await getWorkspaceToken(url, options, config)
const accountClient = getAccountClient(config.ACCOUNTS_URL, token)
const socialStrings = (await accountClient.getSocialIds()).map((si) => buildSocialIdString(si))
if (socialStrings.length === 0) {
throw new Error('No social ids found for the logged in user')
}
const socialId = pickPrimarySocialId(await accountClient.getSocialIds())
const wsLoginInfo = await accountClient.selectWorkspace(options.workspace)
if (wsLoginInfo === undefined) {
throw new Error(`Workspace ${options.workspace} not found`)
}
return await createClient(url, endpoint, token, wsLoginInfo.workspace, socialStrings[0], config, options)
return await createClient(url, endpoint, token, wsLoginInfo.workspace, socialId._id, config, options)
}
async function createClient (
+11 -4
View File
@@ -91,8 +91,7 @@ export type AccountUuid = PersonUuid & { __accountUuid: true }
/**
* @public
* String representation of a social id linked to a global person.
* E.g. email:pied.piper@hcengineering.com or huly:ea3bf257-94b5-4a31-a7da-466d578d850f
* Generated identifier of a social id linked to a global person.
*/
export type PersonId = string & { __personId: true }
@@ -863,11 +862,19 @@ export enum SocialIdType {
HULY = 'huly',
TELEGRAM = 'telegram'
}
export interface SocialId {
id: string
// generated ID so the actual social ID can be detached from a person w/o losing the ID in the linked database records
_id: PersonId
// Should never be changed after creation
type: SocialIdType
value: string
key: PersonId
key: string // Calculated from type and value. Just for convenience.
// To be used later when person detaches social id from his account by any means
// There should always be only one ACTIVE social id with the same key every time
// active: boolean
verifiedOn?: number
}
+11 -4
View File
@@ -34,7 +34,6 @@ import {
IndexKind,
Obj,
Permission,
PersonId,
Ref,
Role,
roleOrder,
@@ -907,13 +906,21 @@ export function combineAttributes (
).filter((v) => v != null)
}
export function buildSocialIdString (key: SocialKey): PersonId {
return `${key.type}:${key.value}` as PersonId
export function buildSocialIdString (key: SocialKey): string {
return `${key.type}:${key.value}`
}
export function parseSocialIdString (id: PersonId): SocialKey {
export function parseSocialIdString (id: string): SocialKey {
const [type, value] = id.split(':')
if (type === undefined || value === undefined) {
throw new Error(`Social id is not valid: ${id}`)
}
if (!Object.values(SocialIdType).includes(type as SocialIdType)) {
throw new Error(`Social id type is not valid: ${id}`)
}
return { type: type as SocialIdType, value }
}
+38 -3
View File
@@ -343,6 +343,7 @@ export class HulyFormatImporter {
private personsByName = new Map<string, Ref<Person>>()
private employeesByName = new Map<string, Ref<Employee>>()
private accountsByEmail = new Map<string, AccountUuid>()
private readonly personIdByEmail = new Map<string, PersonId>()
constructor (
private readonly client: TxOperations,
@@ -606,12 +607,28 @@ export class HulyFormatImporter {
return person
}
private getSocialIdByEmail (email: string): PersonId {
private async getPersonIdByEmail (email: string): Promise<PersonId> {
if (email === this.importerEmailPlaceholder && this.importerSocialId != null) {
return this.importerSocialId
}
return buildSocialIdString({ type: SocialIdType.EMAIL, value: email })
const personId = this.personIdByEmail.get(email)
if (personId !== undefined) {
return personId
}
const socialId = await this.client.findOne(contact.class.SocialIdentity, {
type: SocialIdType.EMAIL,
value: email
})
if (socialId === undefined) {
throw new Error(`Social ID not found for email: ${email}`)
}
this.personIdByEmail.set(email, socialId._id)
return socialId._id
}
private findAccountByEmail (email: string): AccountUuid {
@@ -767,7 +784,7 @@ export class HulyFormatImporter {
}
return {
text: comment.text,
author: this.getSocialIdByEmail(comment.author),
author: await this.getPersonIdByEmail(comment.author),
attachments
}
})
@@ -969,6 +986,24 @@ export class HulyFormatImporter {
}, new Map())
}
private async cachePersonIdsByEmails (): Promise<void> {
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<void> {
this.personsByName = (await this.client.findAll(contact.class.Person, {}))
.map((person) => {
@@ -29,16 +29,16 @@
const dispatch = createEventDispatcher()
const me = getCurrentAccount()
let reactionsAccounts = new Map<string, PersonId[]>()
let reactionsPersons = new Map<string, PersonId[]>()
let opened: boolean = false
$: {
reactionsAccounts.clear()
reactionsPersons.clear()
reactions.forEach((r) => {
const accounts = reactionsAccounts.get(r.emoji) ?? []
reactionsAccounts.set(r.emoji, [...accounts, r.createBy])
const persons = reactionsPersons.get(r.emoji) ?? []
reactionsPersons.set(r.emoji, [...persons, r.createBy])
})
reactionsAccounts = reactionsAccounts
reactionsPersons = reactionsPersons
}
function getClickHandler (emoji: string): ((e: CustomEvent) => void) | undefined {
@@ -63,21 +63,21 @@
</script>
<div class="hulyReactions-container">
{#each [...reactionsAccounts] as [emoji, accounts]}
{#each [...reactionsPersons] as [emoji, persons]}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="hulyReactions-button"
class:highlight={includesAny(accounts, me.socialIds)}
class:highlight={includesAny(persons, me.socialIds)}
class:cursor-pointer={!readonly}
use:tooltip={{ component: ReactionsTooltip, props: { reactionAccounts: accounts } }}
use:tooltip={{ component: ReactionsTooltip, props: { reactionAccounts: persons } }}
on:click={getClickHandler(emoji)}
>
<span class="emoji">{emoji}</span>
<span class="counter">{accounts.length}</span>
<span class="counter">{persons.length}</span>
</div>
{/each}
{#if object && reactionsAccounts.size > 0 && !readonly}
{#if object && reactionsPersons.size > 0 && !readonly}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="hulyReactions-button withoutBackground" class:opened on:click={openEmojiPalette}>
+2
View File
@@ -45,6 +45,8 @@
"@hcengineering/love": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"svelte": "^4.2.19"
}
+1
View File
@@ -16,5 +16,6 @@
import { type Resources } from '@hcengineering/platform'
export * from './requests'
export * from './utils'
export default async (): Promise<Resources> => ({})
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { derived, writable } from 'svelte/store'
import contact, { type SocialIdentity } from '@hcengineering/contact'
import { personRefByPersonIdStore } from '@hcengineering/contact-resources'
import { createQuery, onClient } from '@hcengineering/presentation'
import { aiBotEmailSocialKey } from '@hcengineering/ai-bot'
export const aiBotSocialIdentityStore = writable<SocialIdentity>()
const identityQuery = createQuery(true)
export const aiBotPersonRefStore = derived(
[personRefByPersonIdStore, aiBotSocialIdentityStore],
([personRefByPersonId, aiBotSocialIdentity]) => {
return personRefByPersonId.get(aiBotSocialIdentity?._id)
}
)
onClient(() => {
identityQuery.query(contact.class.SocialIdentity, { key: aiBotEmailSocialKey }, (res) => {
aiBotSocialIdentityStore.set(res[0])
})
})
+2 -3
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { AccountUuid, buildSocialIdString, SocialIdType } from '@hcengineering/core'
import { buildSocialIdString, SocialIdType } from '@hcengineering/core'
import type { Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
@@ -21,9 +21,8 @@ export * from './rest'
export const aiBotId = 'ai-bot' as Plugin
export const aiBotAccountUuid = '' as AccountUuid
export const aiBotAccountEmail = 'huly.ai.bot@hc.engineering'
export const aiBotEmailSocialId = buildSocialIdString({
export const aiBotEmailSocialKey = buildSocialIdString({
type: SocialIdType.EMAIL,
value: aiBotAccountEmail
})
@@ -15,6 +15,7 @@
"WorkspaceName": "Název pracovního prostoru",
"WorkspaceUrl": "URL pracovního prostoru",
"Email": "E-mail",
"Account": "Účet",
"UserName": "Uživatelské jméno",
"DisableAIReplies": "Zakázat odpovědi AI",
"ShowAIReplies": "Zobrazit odpovědi AI"
@@ -14,7 +14,7 @@
"WorkspaceId": "Workspace id",
"WorkspaceName": "Workspace name",
"WorkspaceUrl": "Workspace url",
"SocialId": "Social Id",
"Account": "Account",
"UserName": "User name",
"DisableAIReplies": "Disable AI replies",
"ShowAIReplies": "Show AI replies"
@@ -14,7 +14,7 @@
"WorkspaceId": "Workspace id",
"WorkspaceName": "Workspace name",
"WorkspaceUrl": "Workspace url",
"SocialId": "Social Id",
"Account": "Cuenta",
"UserName": "User name",
"DisableAIReplies": "Disable AI replies",
"ShowAIReplies": "Show AI replies"
@@ -14,7 +14,7 @@
"WorkspaceId": "Workspace id",
"WorkspaceName": "Workspace name",
"WorkspaceUrl": "Workspace url",
"SocialId": "Social Id",
"Account": "Conta",
"UserName": "User name",
"DisableAIReplies": "Disable AI replies",
"ShowAIReplies": "Show AI replies"
@@ -14,7 +14,7 @@
"WorkspaceId": "Id пространства",
"WorkspaceName": "Имя пространства",
"WorkspaceUrl": "URL пространства",
"SocialId": "Социальный ID",
"Account": "Учетная запись",
"UserName": "Имя пользователя",
"DisableAIReplies": "Отключить ответы ИИ",
"ShowAIReplies": "Показать ответы ИИ"
+1 -1
View File
@@ -49,7 +49,7 @@ const analyticsCollector = plugin(analyticsCollectorId, {
WorkspaceId: '' as IntlString,
WorkspaceName: '' as IntlString,
WorkspaceUrl: '' as IntlString,
SocialId: '' as IntlString,
Account: '' as IntlString,
UserName: '' as IntlString,
DisableAIReplies: '' as IntlString,
ShowAIReplies: '' as IntlString
+2 -2
View File
@@ -14,7 +14,7 @@
//
import { Channel } from '@hcengineering/chunter'
import type { PersonId, WorkspaceUuid } from '@hcengineering/core'
import type { AccountUuid, WorkspaceUuid } from '@hcengineering/core'
export enum AnalyticEventType {
SetUser = 'setUser',
@@ -34,7 +34,7 @@ export interface OnboardingChannel extends Channel {
workspaceId: WorkspaceUuid
workspaceName: string
workspaceUrl: string
socialString: PersonId
account: AccountUuid
userName: string
disableAIReplies: boolean
showAIReplies: boolean
+2 -4
View File
@@ -13,8 +13,6 @@
// limitations under the License.
//
import type { PersonId } from '@hcengineering/core'
export function getOnboardingChannelName (worksapceUrl: string, personId: PersonId): string {
return `${personId}; ${worksapceUrl}`
export function getOnboardingChannelName (worksapceUrl: string, name: string): string {
return `${name}; ${worksapceUrl}`
}
@@ -70,7 +70,7 @@
attachedTo: { $in: selectedParticipants_ },
attachedToClass: contact.class.Person
})
const senderQuery = allSocialIds.length !== 0 ? { modifiedBy: { $in: allSocialIds.map((si) => si.key) } } : {}
const senderQuery = allSocialIds.length !== 0 ? { modifiedBy: { $in: allSocialIds.map((si) => si._id) } } : {}
let spaceQuery: { space: any }
if (selectedSpaces_.length > 0) {
@@ -260,7 +260,7 @@
attachedToClass: dragItem._class,
_class: dragEventClass,
collection: 'events',
calendar: `${myPrimaryId}_calendar` as Ref<Calendar>,
calendar: `${acc.uuid}_calendar` as Ref<Calendar>,
modifiedBy: myPrimaryId,
participants: [me],
modifiedOn: Date.now(),
@@ -77,7 +77,7 @@
let description: Markup = EmptyMarkup
let visibility: Visibility = 'private'
let _calendar: Ref<Calendar> = `${myPrimaryId}_calendar` as Ref<Calendar>
let _calendar: Ref<Calendar> = `${acc.uuid}_calendar` as Ref<Calendar>
const q = createQuery()
q.query(
@@ -44,7 +44,7 @@
let date: number | undefined
if (value != null) date = value
if (date === undefined) return
const _calendar = `${getCurrentAccount().primarySocialId}_calendar` as Ref<Calendar>
const _calendar = `${getCurrentAccount().uuid}_calendar` as Ref<Calendar>
await client.addCollection(calendar.class.Event, calendar.space.Calendar, attachedTo, attachedToClass, 'events', {
calendar: _calendar,
eventId: generateEventId(),
@@ -13,7 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { AvatarType, Channel, combineName, ContactEvents, Employee, Person } from '@hcengineering/contact'
import {
AvatarType,
Channel,
combineName,
ContactEvents,
Employee,
Person,
SocialIdentityRef
} from '@hcengineering/contact'
import {
AccountRole,
AttachedData,
@@ -30,7 +38,7 @@
import { createEventDispatcher } from 'svelte'
import { ChannelsDropdown } from '..'
import contact from '../plugin'
import { personByPersonIdStore } from '../utils'
import { employeeBySocialKeyStore, getAccountClient } from '../utils'
import EditableAvatar from './EditableAvatar.svelte'
import { Analytics } from '@hcengineering/analytics'
@@ -59,6 +67,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
const accountClient = getAccountClient()
async function createEmployee (): Promise<void> {
try {
@@ -70,13 +79,20 @@
value: mail
})
const existingPerson = $personByPersonIdStore.get(socialString)
const existingId = await client.findOne(contact.class.SocialIdentity, { key: socialString })
const existingPerson =
existingId !== undefined
? await client.findOne(contact.class.Person, { _id: existingId.attachedTo })
: undefined
if (existingPerson !== undefined && client.getHierarchy().hasMixin(existingPerson, contact.mixin.Employee)) {
return
}
const { uuid, socialId } = await accountClient.ensurePerson(SocialIdType.EMAIL, mail, firstName, lastName)
const name = combineName(firstName, lastName)
person.name = name
person.personUuid = uuid
const info = await avatarEditor.createAvatar()
person.avatar = info.avatar
person.avatarType = info.avatarType
@@ -89,8 +105,9 @@
}
const employeeRef = (existingPerson?._id as Ref<Employee>) ?? id
await client.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
active: true
await client.createMixin(employeeRef, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
active: true,
role: AccountRole.User
})
await client.addCollection(
@@ -102,9 +119,9 @@
{
type: SocialIdType.EMAIL,
value: mail,
confirmed: false,
key: socialString
}
},
socialId as SocialIdentityRef
)
const sendInvite = await getResource(login.function.SendInvite)
@@ -140,8 +157,7 @@
let channels: AttachedData<Channel>[] = []
$: emailPerson = $personByPersonIdStore.get(emailSocialString)
$: exists = emailPerson !== undefined && client.getHierarchy().hasMixin(emailPerson, contact.mixin.Employee)
$: exists = $employeeBySocialKeyStore.get(emailSocialString) !== undefined
const manager = createFocusManager()
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { AvatarType, Channel, combineName, ContactEvents, Person } from '@hcengineering/contact'
import { AvatarType, Channel, combineName, ContactEvents, Person, SocialIdentityRef } from '@hcengineering/contact'
import {
AccountRole,
AttachedData,
@@ -25,11 +25,12 @@
} from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { Card, getClient } from '@hcengineering/presentation'
import { createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { ChannelsDropdown, personByPersonIdStore } from '..'
import { ChannelsDropdown, employeeBySocialKeyStore } from '..'
import contact from '../plugin'
import { getAccountClient } from '../utils'
import { Analytics } from '@hcengineering/analytics'
export let canSave: boolean = true
@@ -49,6 +50,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
const accountClient = getAccountClient()
async function createGuest (): Promise<void> {
try {
@@ -60,31 +62,51 @@
value: mail
})
const existingPerson = $personByPersonIdStore.get(socialString)
if (existingPerson === undefined) {
const name = combineName(firstName, lastName)
const person: Data<Person> = {
name,
city: '',
avatarType: AvatarType.COLOR
}
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
await client.addCollection(
contact.class.SocialIdentity,
contact.space.Contacts,
id,
contact.class.Person,
'socialIds',
{
type: SocialIdType.EMAIL,
value: mail,
confirmed: false,
key: socialString
}
)
const existingId = await client.findOne(contact.class.SocialIdentity, { key: socialString })
const existingPerson =
existingId !== undefined
? await client.findOne(contact.class.Person, { _id: existingId.attachedTo })
: undefined
if (existingPerson !== undefined && client.getHierarchy().hasMixin(existingPerson, contact.mixin.Employee)) {
return
}
const { uuid, socialId } = await accountClient.ensurePerson(SocialIdType.EMAIL, mail, firstName, lastName)
const name = combineName(firstName, lastName)
const person: Data<Person> = {
name,
city: '',
avatarType: AvatarType.COLOR,
personUuid: uuid
}
if (existingPerson === undefined) {
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
} else {
await client.update(existingPerson, person)
}
const personRef = existingPerson?._id ?? id
await client.createMixin(personRef, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
active: true,
role: AccountRole.Guest
})
await client.addCollection(
contact.class.SocialIdentity,
contact.space.Contacts,
personRef,
contact.class.Person,
'socialIds',
{
type: SocialIdType.EMAIL,
value: mail,
key: socialString
},
socialId as SocialIdentityRef
)
const sendInvite = await getResource(login.function.SendInvite)
await sendInvite(mail, AccountRole.Guest)
@@ -102,7 +124,7 @@
)
}
if (onCreate != null) {
await onCreate(existingPerson?._id ?? id)
await onCreate(personRef)
}
Analytics.handleEvent(ContactEvents.PersonCreated, { id, email: mail })
dispatch('close', id)
@@ -117,12 +139,7 @@
type: SocialIdType.EMAIL,
value: email.trim()
})
$: emailPerson = $personByPersonIdStore.get(emailSocialString)
$: exists = emailPerson !== undefined && client.getHierarchy().hasMixin(emailPerson, contact.mixin.Employee)
$: console.log(email)
$: console.log(emailPerson)
$: console.log(exists)
$: exists = $employeeBySocialKeyStore.get(emailSocialString) !== undefined
const manager = createFocusManager()
@@ -25,7 +25,6 @@
import EditableAvatar from './EditableAvatar.svelte'
import Avatar from './Avatar.svelte'
import ChannelsDropdown from './ChannelsDropdown.svelte'
import { socialIdsByPersonRefStore } from '../utils'
export let object: Person
export let readonly: boolean = false
@@ -37,7 +36,6 @@
const account = getCurrentAccount()
const me = getCurrentEmployee()
$: owner = me === object._id
$: mySocialStrings = ($socialIdsByPersonRefStore.get(me) ?? []).map((si) => si.key)
function isEditable (owner: boolean, object: Person): boolean {
if (owner) return true
@@ -74,9 +72,13 @@
let integrations: Set<Ref<IntegrationType>> = new Set<Ref<IntegrationType>>()
const settingsQuery = createQuery()
$: settingsQuery.query(setting.class.Integration, { createdBy: { $in: mySocialStrings }, disabled: false }, (res) => {
integrations = new Set(res.map((p) => p.type))
})
$: settingsQuery.query(
setting.class.Integration,
{ createdBy: { $in: account.socialIds }, disabled: false },
(res) => {
integrations = new Set(res.map((p) => p.type))
}
)
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels', 'city'] })
onMount(sendOpen)
+74 -35
View File
@@ -26,7 +26,6 @@ import {
getLastName,
getName,
getCurrentEmployee,
pickPrimarySocialId,
currentEmployeePromise,
type Person,
type SocialIdentity,
@@ -55,12 +54,15 @@ import core, {
type WithLookup,
type AccountUuid,
notEmpty,
getCurrentAccount
getCurrentAccount,
pickPrimarySocialId,
type PersonId
} from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { type IntlString, getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
import { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { type IntlString, getEmbeddedLabel, getMetadata, getResource, translate } from '@hcengineering/platform'
import presentation, { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { type TemplateDataProvider } from '@hcengineering/templates'
import login from '@hcengineering/login'
import {
getCurrentResolvedLocation,
getPanelURI,
@@ -71,8 +73,9 @@ import {
} from '@hcengineering/ui'
import view, { type Filter, type GrouppingManager } from '@hcengineering/view'
import { accessDeniedStore, FilterQuery } from '@hcengineering/view-resources'
import { derived, get, writable } from 'svelte/store'
import { derived, get, type Readable, writable } from 'svelte/store'
import { type LocationData } from '@hcengineering/workbench'
import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client'
import contact from './plugin'
@@ -338,37 +341,45 @@ export const socialIdsStore = writable<Array<WithLookup<SocialIdentity>>>([])
export const mySocialIdsStore = derived([currentEmployeeRefStore, socialIdsStore], ([myEmployeeRef, socialIds]) => {
return socialIds.filter((si) => si.attachedTo === myEmployeeRef)
})
export const mySocialStringsStore = derived(mySocialIdsStore, (mySocialIds) => {
return mySocialIds.map((si) => si.key)
})
/**
* [Ref<Person> => SocialIdentity[]] mapping
*/
export const socialIdsByPersonRefStore = derived([socialIdsStore], ([socialIds]) => {
const sidsByPersonRef: Record<Ref<Person>, SocialIdentity[]> = socialIds.reduce<
Record<Ref<Person>, SocialIdentity[]>
>((acc, si) => {
acc[si.attachedTo] = acc[si.attachedTo] ?? []
acc[si.attachedTo].push(si)
return acc
}, {})
export const socialIdsByPersonRefStore: Readable<Map<Ref<Person>, SocialIdentity[]>> = derived(
[socialIdsStore],
([socialIds]) => {
const sidsByPersonRef = socialIds.reduce<Record<Ref<Person>, SocialIdentity[]>>((acc, si) => {
acc[si.attachedTo] = acc[si.attachedTo] ?? []
acc[si.attachedTo].push(si)
return acc
}, {})
return new Map(Object.entries(sidsByPersonRef))
})
return new Map(Object.entries(sidsByPersonRef) as Array<[Ref<Person>, SocialIdentity[]]>)
}
)
/**
* [Ref<Person> => PersonId (primary)] mapping
*/
export const primarySocialIdByPersonRefStore = derived(socialIdsByPersonRefStore, (socialIdsByPersonRef) => {
const mapped = Array.from(socialIdsByPersonRef.entries())
.filter(([_, socialIds]) => socialIds.length > 0)
.map(([_id, socialIds]) => [_id, pickPrimarySocialId(socialIds.map((si) => si.key))] as const)
export const primarySocialIdByPersonRefStore: Readable<Map<Ref<Person>, PersonId>> = derived(
socialIdsByPersonRefStore,
(socialIdsByPersonRef) => {
const mapped = Array.from(socialIdsByPersonRef.entries())
.filter(([_, socialIds]) => socialIds.length > 0)
.map(([_id, socialIds]) => [_id, pickPrimarySocialId(socialIds)._id] as const)
return new Map(mapped)
}
)
/**
* [PersonId (social ID) => Ref<Person>] mapping
*/
export const personRefByPersonIdStore: Readable<Map<PersonId, Ref<Person>>> = derived(socialIdsStore, (socialIds) => {
const mapped = socialIds.map((si) => [si._id, si.attachedTo] as const)
return new Map(mapped)
})
/**
* [PersonId (social string) => Ref<Person>] mapping
* [string (social key) => Ref<Person>] mapping
*/
export const personRefByPersonIdStore = derived(socialIdsStore, (socialIds) => {
export const personRefBySocialKeyStore: Readable<Map<string, Ref<Person>>> = derived(socialIdsStore, (socialIds) => {
const mapped = socialIds.map((si) => [si.key, si.attachedTo] as const)
return new Map(mapped)
})
@@ -377,9 +388,9 @@ export const personRefByPersonIdStore = derived(socialIdsStore, (socialIds) => {
*/
export const personRefByAccountUuidStore = writable<Map<AccountUuid, Ref<Employee>>>(new Map())
/**
* [PersonId (social string) => Person] mapping
* [PersonId (social ID) => Person] mapping
*/
export const personByPersonIdStore = derived(
export const personByPersonIdStore: Readable<Map<PersonId, Person>> = derived(
[personRefByPersonIdStore, personByIdStore],
([personRefByPersonId, personById]) => {
const mapped = Array.from(personRefByPersonId.entries())
@@ -394,6 +405,24 @@ export const personByPersonIdStore = derived(
return new Map(mapped)
}
)
/**
* [string (social key) => Employee] mapping
*/
export const employeeBySocialKeyStore: Readable<Map<string, Employee>> = derived(
[personRefBySocialKeyStore, employeeByIdStore],
([personRefBySocialKey, employeeById]) => {
const mapped = Array.from(personRefBySocialKey.entries())
.map(([socialKey, personRef]) => {
const employee = employeeById.get(personRef as Ref<Employee>)
if (employee === undefined) {
return undefined
}
return [socialKey, employee] as const
})
.filter(notEmpty)
return new Map(mapped)
}
)
/**
* [AccountUuid => Person] mapping
*/
@@ -413,9 +442,9 @@ export const employeeByAccountStore = derived(
}
)
/**
* [PersonId (social string) => SocialIdentity[]] mapping
* [PersonId (social ID) => SocialIdentity[]] mapping
*/
export const socialIdsByPersonIdStore = derived(
export const socialIdsByPersonIdStore: Readable<Map<PersonId, SocialIdentity[]>> = derived(
[personRefByPersonIdStore, socialIdsByPersonRefStore],
([personRefByPersonId, socialIdsByPersonRef]) => {
const mapped = Array.from(personRefByPersonId.entries()).map(([personId, personRef]) => {
@@ -426,14 +455,17 @@ export const socialIdsByPersonIdStore = derived(
}
)
/**
* [PersonId (social string) => PersonId (primary)] mapping
* [PersonId (social ID) => PersonId (primary)] mapping
*/
export const primarySocialIdByPersonIdStore = derived(socialIdsByPersonIdStore, (socialIdsByPersonId) => {
const mapped = Array.from(socialIdsByPersonId.entries())
.filter(([_, socialIds]) => socialIds.length > 0)
.map(([personId, socialIds]) => [personId, pickPrimarySocialId(socialIds.map((si) => si.key))] as const)
return new Map(mapped)
})
export const primarySocialIdByPersonIdStore: Readable<Map<PersonId, PersonId>> = derived(
socialIdsByPersonIdStore,
(socialIdsByPersonId) => {
const mapped = Array.from(socialIdsByPersonId.entries())
.filter(([_, socialIds]) => socialIds.length > 0)
.map(([personId, socialIds]) => [personId, pickPrimarySocialId(socialIds)._id] as const)
return new Map(mapped)
}
)
export const channelProviders = writable<ChannelProvider[]>([])
export const statusByUserStore = writable<Map<AccountUuid, UserStatus>>(new Map())
@@ -771,3 +803,10 @@ spaceTypesQuery.query(core.class.SpaceType, {}, (types) => {
}
)
})
export function getAccountClient (): AccountClient {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
const token = getMetadata(presentation.metadata.Token)
return getAccountClientRaw(accountsUrl, token)
}
+5 -5
View File
@@ -21,7 +21,7 @@ import {
Doc,
PersonId,
Ref,
SocialKey,
SocialId,
Space,
Timestamp,
UXObject,
@@ -57,14 +57,14 @@ export interface ChannelProvider extends Doc, UXObject {
integrationType?: Ref<Doc>
}
export interface SocialIdentity extends SocialKey, AttachedDoc {
export interface SocialIdentity extends SocialId, AttachedDoc {
_id: Ref<this> & PersonId
attachedTo: Ref<Person>
attachedToClass: Ref<Class<Person>>
key: PersonId
confirmed: boolean
}
export type SocialIdentityRef = SocialIdentity['_id']
/**
* @public
*/
+68 -44
View File
@@ -31,11 +31,12 @@ import {
TxFactory,
Person as GlobalPerson,
AccountUuid,
notEmpty
notEmpty,
toIdMap
} from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { ColorDefinition } from '@hcengineering/ui'
import contact, { AvatarProvider, AvatarType, Channel, Contact, Employee, Person } from '.'
import contact, { AvatarProvider, AvatarType, Channel, Contact, Employee, Person, SocialIdentityRef } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
@@ -293,8 +294,14 @@ export function includesAny (members: PersonId[], ids: PersonId[]): boolean {
return members.some((m) => ids.includes(m))
}
export async function getPersonBySocialKey (client: Client, socialKey: string): Promise<Person | undefined> {
const socialId = await client.findOne(contact.class.SocialIdentity, { key: socialKey })
return await client.findOne(contact.class.Person, { _id: socialId?.attachedTo, _class: socialId?.attachedToClass })
}
export async function getPersonBySocialId (client: Client, socialIdString: PersonId): Promise<Person | undefined> {
const socialId = await client.findOne(contact.class.SocialIdentity, { key: socialIdString })
const socialId = await client.findOne(contact.class.SocialIdentity, { _id: socialIdString as SocialIdentityRef })
return await client.findOne(contact.class.Person, { _id: socialId?.attachedTo, _class: socialId?.attachedToClass })
}
@@ -303,7 +310,7 @@ export async function getPersonRefBySocialId (
client: Client,
socialIdString: PersonId
): Promise<Ref<Person> | undefined> {
const socialId = await client.findOne(contact.class.SocialIdentity, { key: socialIdString })
const socialId = await client.findOne(contact.class.SocialIdentity, { _id: socialIdString as SocialIdentityRef })
return socialId?.attachedTo
}
@@ -312,11 +319,14 @@ export async function getPersonRefsBySocialIds (
client: Client,
ids: PersonId[] = []
): Promise<Record<PersonId, Ref<Person>>> {
const socialIds = await client.findAll(contact.class.SocialIdentity, ids.length === 0 ? {} : { key: { $in: ids } })
const socialIds = await client.findAll(
contact.class.SocialIdentity,
ids.length === 0 ? {} : { _id: { $in: ids as SocialIdentityRef[] } }
)
const result: Record<PersonId, Ref<Person>> = {}
for (const socialId of socialIds) {
result[socialId.key] = socialId.attachedTo
result[socialId._id] = socialId.attachedTo
}
return result
@@ -329,12 +339,12 @@ export async function getPrimarySocialId (client: Client, person: Ref<Person>):
return
}
return pickPrimarySocialId(socialIds.map((it) => it.key))
return pickPrimarySocialId(socialIds.map((it) => it._id))
}
export async function getAllSocialStringsByPersonId (client: Client, personId: PersonId): Promise<PersonId[]> {
const socialId = await client.findOne(contact.class.SocialIdentity, {
key: personId,
_id: personId as SocialIdentityRef,
attachedToClass: contact.class.Person
})
@@ -344,13 +354,13 @@ export async function getAllSocialStringsByPersonId (client: Client, personId: P
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: socialId.attachedTo })
return socialIds.map((it) => it.key)
return socialIds.map((it) => it._id)
}
export async function getAllSocialStringsByPersonRef (client: Client, person: Ref<Person>): Promise<PersonId[]> {
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person })
return socialIds.map((it) => it.key)
return socialIds.map((it) => it._id)
}
export async function getSocialStringsByEmployee (client: Client): Promise<Record<Ref<Person>, PersonId[]>> {
@@ -359,16 +369,15 @@ export async function getSocialStringsByEmployee (client: Client): Promise<Recor
attachedTo: { $in: employees.map((it) => it._id) },
attachedToClass: contact.class.Person
})
const socialStringsByPerson: Record<Ref<Person>, PersonId[]> = {}
const socialStringsByPerson: Record<Ref<Employee>, PersonId[]> = {}
for (const socialId of socialIds) {
const socialStrings = socialStringsByPerson[socialId.attachedTo]
const socialString = buildSocialIdString(socialId)
const socialStrings = socialStringsByPerson[socialId.attachedTo as Ref<Employee>]
if (socialStrings === undefined) {
socialStringsByPerson[socialId.attachedTo] = [socialString]
} else {
socialStrings.push(socialString)
socialStringsByPerson[socialId.attachedTo as Ref<Employee>] = []
}
socialStrings.push(socialId._id)
}
return socialStringsByPerson
@@ -403,21 +412,11 @@ export async function ensureEmployee (
const personByUuid = await client.findOne(contact.class.Person, { personUuid: me.uuid })
let personRef: Ref<Person> | undefined = personByUuid?._id
if (personRef === undefined) {
const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: { $in: me.socialIds } })
if (socialIdentity !== undefined && !socialIdentity.confirmed) {
const updateSocialIdentityTx = txFactory.createTxUpdateDoc(
contact.class.SocialIdentity,
contact.space.Contacts,
socialIdentity._id,
{
confirmed: true
}
)
await client.tx(updateSocialIdentityTx)
}
const socialIdentity = await client.findOne(contact.class.SocialIdentity, {
_id: { $in: me.socialIds as SocialIdentityRef[] }
})
// This social id is confirmed globally as we only have ids of confirmed social identities in socialIds array
personRef = socialIdentity?.attachedTo
}
@@ -450,13 +449,13 @@ export async function ensureEmployee (
await client.tx(updatePersonTx)
}
const existingIdentifiers = await client.findAll(contact.class.SocialIdentity, {
attachedTo: personRef,
attachedToClass: contact.class.Person
})
const existingIdentifiers = toIdMap(
await client.findAll(contact.class.SocialIdentity, { _id: { $in: me.socialIds as SocialIdentityRef[] } })
)
for (const socialId of socialIds) {
const existing = existingIdentifiers.find((it) => it.type === socialId.type && it.value === socialId.value)
const existing = existingIdentifiers.get(socialId._id as SocialIdentityRef)
if (existing === undefined) {
await ctx.with('create-social-identity', {}, async () => {
if (personRef === undefined) {
@@ -470,19 +469,44 @@ export async function ensureEmployee (
personRef,
contact.space.Contacts,
'socialIds',
txFactory.createTxCreateDoc(contact.class.SocialIdentity, contact.space.Contacts, {
attachedTo: personRef,
attachedToClass: contact.class.Person,
collection: 'socialIds',
type: socialId.type,
value: socialId.value,
key: buildSocialIdString(socialId), // TODO: fill it in trigger or on DB level as stored calculated column or smth?
confirmed: socialId.verifiedOn !== undefined && socialId.verifiedOn > 0
})
txFactory.createTxCreateDoc(
contact.class.SocialIdentity,
contact.space.Contacts,
{
attachedTo: personRef,
attachedToClass: contact.class.Person,
collection: 'socialIds',
type: socialId.type,
value: socialId.value,
key: buildSocialIdString(socialId), // TODO: fill it in trigger or on DB level as stored calculated column or smth?
verifiedOn: socialId.verifiedOn
},
socialId._id as SocialIdentityRef
)
)
await client.tx(createSocialIdTx)
})
} else {
// This social identity must be attached to the correct person. If it's not the case, something is wrong.
// personRef must be readonly after creation and must NEVER be changed.
if (existing.attachedTo !== personRef) {
throw new Error('Social identity is attached to the wrong person')
}
// Check and update if needed
if (existing.verifiedOn == null) {
const updateSocialIdentityTx = txFactory.createTxUpdateDoc(
contact.class.SocialIdentity,
contact.space.Contacts,
existing._id,
{
verifiedOn: socialId.verifiedOn
}
)
await client.tx(updateSocialIdentityTx)
}
}
}
@@ -29,7 +29,9 @@
translate
} from '@hcengineering/platform'
import { EditBox, StylishEdit, ModernDialog } from '@hcengineering/ui'
import { getCurrentAccount, parseSocialIdString, SocialIdType } from '@hcengineering/core'
import { getCurrentAccount, SocialIdType } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import contact, { SocialIdentityRef } from '@hcengineering/contact'
import documents from '../plugin'
import StatusControl from './requests/StatusControl.svelte'
@@ -41,20 +43,27 @@
const dispatch = createEventDispatcher()
const account = getCurrentAccount()
const client = getClient()
let rejectionNote = ''
const accountsUrl = getMetadata(login.metadata.AccountsUrl) ?? ''
const emailSocialIdString = account.socialIds.find((si) => parseSocialIdString(si).type === SocialIdType.EMAIL)
const emailSocialId = emailSocialIdString !== undefined ? parseSocialIdString(emailSocialIdString) : undefined
const email: string = emailSocialId?.value ?? ''
const disableEmailField = email !== ''
const object: LoginInfo = {
email,
email: '',
password: ''
}
void client
.findOne(contact.class.SocialIdentity, {
_id: { $in: account.socialIds as SocialIdentityRef[] },
type: SocialIdType.EMAIL
})
.then((si) => {
if (si != null) {
object.email = si.value
}
})
const accountsUrl = getMetadata(login.metadata.AccountsUrl) ?? ''
$: disableEmailField = object.email !== ''
$: canSubmit = object.email !== '' && object.password !== '' && (!isRejection || rejectionNote.trim().length > 0)
let status = OK
+1 -1
View File
@@ -137,7 +137,7 @@ export async function lockContent (doc: Document | Document[]): Promise<void> {
const arr = Array.isArray(doc) ? doc : [doc]
for (const doc of arr) {
await client.diffUpdate(doc, { lockedBy: me.primarySocialId })
await client.diffUpdate(doc, { lockedBy: me.uuid })
}
}
+2 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { PersonId, Doc, MarkupBlobRef, Rank, Ref, TypedSpace } from '@hcengineering/core'
import { AccountUuid, Doc, MarkupBlobRef, Rank, Ref, TypedSpace } from '@hcengineering/core'
import { Preference } from '@hcengineering/preference'
import { IconProps } from '@hcengineering/view'
@@ -27,7 +27,7 @@ export interface Document extends Doc, IconProps {
parent: Ref<Document>
space: Ref<Teamspace>
lockedBy?: PersonId | null
lockedBy?: AccountUuid | null
snapshots?: number
attachments?: number
@@ -32,7 +32,7 @@
status = loginStatus
if (result !== undefined) {
if (result != null) {
await logIn(result)
await afterConfirm()
} else {
@@ -78,7 +78,7 @@
{fields}
{object}
{action}
subtitle={getAccountDisplayName(loginInfo)}
subtitle={getAccountDisplayName(loginInfo ?? null)}
bottomActions={[
{
caption: login.string.HaveWorkspace,
@@ -31,7 +31,7 @@
} from 'livekit-client'
import { onDestroy, onMount, tick } from 'svelte'
import presentation from '@hcengineering/presentation'
import { aiBotEmailSocialId } from '@hcengineering/ai-bot'
import { aiBotSocialIdentityStore } from '@hcengineering/ai-bot-resources'
import love from '../plugin'
import { storePromise, currentRoom, infos, invites, myInfo, myRequests } from '../stores'
@@ -65,7 +65,8 @@
let screen: HTMLVideoElement
let roomEl: HTMLDivElement
$: aiPersonId = $personRefByPersonIdStore.get(aiBotEmailSocialId)
$: aiPersonId =
$aiBotSocialIdentityStore != null ? $personRefByPersonIdStore.get($aiBotSocialIdentityStore._id) : undefined
function handleTrackSubscribed (
track: RemoteTrack,
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { aiBotEmailSocialId } from '@hcengineering/ai-bot'
import { aiBotSocialIdentityStore } from '@hcengineering/ai-bot-resources'
import { personRefByPersonIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { RoomType, Room as TypeRoom } from '@hcengineering/love'
@@ -66,7 +66,8 @@
isAgent: boolean
}
$: aiPersonId = $personRefByPersonIdStore.get(aiBotEmailSocialId)
$: aiPersonId =
$aiBotSocialIdentityStore != null ? $personRefByPersonIdStore.get($aiBotSocialIdentityStore._id) : undefined
const dispatch = createEventDispatcher()
+2 -3
View File
@@ -13,10 +13,9 @@ import {
} from '@hcengineering/love'
import { createQuery, onClient } from '@hcengineering/presentation'
import { derived, get, writable } from 'svelte/store'
import { aiBotEmailSocialId } from '@hcengineering/ai-bot'
import { aiBotPersonRefStore } from '@hcengineering/ai-bot-resources'
import love from './plugin'
import { personRefByPersonIdStore } from '@hcengineering/contact-resources'
export const rooms = writable<Room[]>([])
export const myOffice = derived(rooms, (val) => {
@@ -62,7 +61,7 @@ export const selectedRoomPlace = writable<{ _id: Ref<Room>, x: number, y: number
function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] {
const map = new Map<string, ParticipantInfo>()
const aiPerson = get(personRefByPersonIdStore).get(aiBotEmailSocialId)
const aiPerson = get(aiBotPersonRefStore)
for (const val of value) {
if (aiPerson !== undefined && val.person === aiPerson) {
map.set(val._id, val)
@@ -13,20 +13,19 @@
// limitations under the License.
-->
<script lang="ts">
import { mySocialStringsStore } from '@hcengineering/contact-resources'
import contact, { includesAny } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import contact from '@hcengineering/contact'
import { DisplayDocUpdateMessage } from '@hcengineering/activity'
import notification from '@hcengineering/notification'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import { Action, Icon, Label } from '@hcengineering/ui'
import { type PersonId } from '@hcengineering/core'
export let message: DisplayDocUpdateMessage
export let actions: Action[] = []
$: attributeUpdates = message.attributeUpdates ?? { added: [], removed: [], set: [] }
$: addedAttributes = (attributeUpdates.added.length > 0 ? attributeUpdates.added : attributeUpdates.set) ?? []
$: isMeAdded = includesAny(addedAttributes as PersonId[], $mySocialStringsStore)
$: isMeAdded = addedAttributes.includes(getCurrentAccount().uuid)
</script>
<BaseMessagePreview {actions} {message} on:click>
@@ -18,7 +18,6 @@
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import core, { Class, Doc, Ref, WithLookup, getCurrentAccount } from '@hcengineering/core'
import { includesAny } from '@hcengineering/contact'
import { checkMyPermission, permissionsStore } from '@hcengineering/contact-resources'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
@@ -84,7 +83,7 @@
$: canEdit =
!readonly &&
object?.$lookup?.space !== undefined &&
(includesAny(object?.$lookup?.space.owners ?? [], getCurrentAccount().socialIds) ||
((object?.$lookup?.space.owners ?? []).includes(getCurrentAccount().uuid) ||
checkMyPermission(core.permission.UpdateSpace, object.space, $permissionsStore) ||
checkMyPermission(core.permission.UpdateObject, core.space.Space, $permissionsStore))
@@ -18,7 +18,6 @@
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import core, { type Class, type Doc, type Ref, getCurrentAccount } from '@hcengineering/core'
import { includesAny } from '@hcengineering/contact'
import { checkMyPermission, permissionsStore } from '@hcengineering/contact-resources'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
@@ -105,7 +104,7 @@
!readonly &&
object !== undefined &&
!object.archived &&
(includesAny(object.owners ?? [], getCurrentAccount().socialIds) ||
((object.owners ?? []).includes(getCurrentAccount().uuid) ||
checkMyPermission(core.permission.UpdateSpace, _id, $permissionsStore) ||
checkMyPermission(core.permission.UpdateObject, core.space.Space, $permissionsStore))
@@ -16,7 +16,7 @@
import calendar, { Calendar } from '@hcengineering/calendar'
import type { Organization, Person } from '@hcengineering/contact'
import contact, { getCurrentEmployee } from '@hcengineering/contact'
import core, { Class, Client, DateRangeMode, Doc, generateId, Markup, Ref } from '@hcengineering/core'
import core, { Class, Client, DateRangeMode, Doc, generateId, Markup, PersonId, Ref } from '@hcengineering/core'
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
import { Card, getClient } from '@hcengineering/presentation'
import { UserBox, UserBoxList } from '@hcengineering/contact-resources'
@@ -63,7 +63,7 @@
_id: generateId(),
collection: 'reviews',
modifiedOn: Date.now(),
modifiedBy: '',
modifiedBy: '' as PersonId,
date: 0,
access: 'reader',
allDay: false,
@@ -16,8 +16,7 @@
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { getCurrentEmployee } from '@hcengineering/contact'
import { mySocialStringsStore } from '@hcengineering/contact-resources'
import { AttachedData, Markup } from '@hcengineering/core'
import { AttachedData, getCurrentAccount, Markup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Request, RequestStatus } from '@hcengineering/request'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
@@ -32,6 +31,7 @@
export let value: Request
const client = getClient()
const account = getCurrentAccount()
const myPerson = getCurrentEmployee()
const approvable =
@@ -130,7 +130,7 @@
</script>
{#if value.status === RequestStatus.Active}
{#if value.createdBy !== undefined && $mySocialStringsStore.includes(value.createdBy)}
{#if value.createdBy !== undefined && account.socialIds.includes(value.createdBy)}
<div class="mt-2">
<Button label={request.string.Cancel} on:click={cancel} />
</div>
@@ -34,6 +34,7 @@
import setting from '../plugin'
const client = getClient()
const account = getCurrentAccount()
const me = getCurrentEmployee()
$: employee = $personByIdStore.get(me)
@@ -60,7 +61,7 @@
message: setting.string.LeaveDescr,
action: async () => {
const leaveWorkspace = await getResource(login.function.LeaveWorkspace)
const loginInfo = await leaveWorkspace(getCurrentAccount().uuid)
const loginInfo = await leaveWorkspace(account.uuid)
if (loginInfo?.token != null) {
await logIn(loginInfo)
@@ -18,7 +18,6 @@
import { createQuery } from '@hcengineering/presentation'
import { Task } from '@hcengineering/task'
import { getCurrentEmployee } from '@hcengineering/contact'
import { mySocialStringsStore } from '@hcengineering/contact-resources'
import {
Component,
IModeSelector,
@@ -49,8 +48,9 @@
let search = ''
const dispatch = createEventDispatcher()
const me = getCurrentEmployee()
const account = getCurrentAccount()
const assigned = { assignee: me }
$: created = { createdBy: { $in: $mySocialStringsStore } }
$: created = { createdBy: { $in: account.socialIds } }
let subscribed = { _id: { $in: [] as Ref<Task>[] } }
let mode: string | undefined = undefined
let baseQuery: DocumentQuery<Task> | undefined = undefined
@@ -30,7 +30,6 @@
import DueDateEditor from './DueDateEditor.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import Workslots from './Workslots.svelte'
import { mySocialStringsStore } from '@hcengineering/contact-resources'
export let object: Doc | undefined
@@ -112,12 +111,12 @@
dispatch('close', true)
}
let _calendar: Ref<Calendar> = `${myAccount.primarySocialId}_calendar` as Ref<Calendar>
let _calendar: Ref<Calendar> = `${myAccount.uuid}_calendar` as Ref<Calendar>
const q = createQuery()
q.query(
calendar.class.ExternalCalendar,
{ default: true, hidden: false, createdBy: { $in: $mySocialStringsStore } },
{ default: true, hidden: false, createdBy: { $in: myAccount.socialIds } },
(res) => {
if (res.length > 0) {
_calendar = res[0]._id
@@ -54,7 +54,7 @@
hidden: false,
default: true
})
const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.primarySocialId}_calendar` as Ref<Calendar>)
const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.uuid}_calendar` as Ref<Calendar>)
const dueDate = date + defaultDuration
await client.addCollection(time.class.WorkSlot, calendar.space.Calendar, doc._id, doc._class, 'workslots', {
calendar: _calendar,
@@ -114,7 +114,7 @@
current.date = e.detail.date.getTime()
current.dueDate = new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
} else {
const _calendar = `${myAcc.primarySocialId}_calendar` as Ref<Calendar>
const _calendar = `${myAcc.uuid}_calendar` as Ref<Calendar>
const ev: WorkSlot = {
_id: dragItemId,
allDay: false,
@@ -129,7 +129,7 @@
visibility: 'public',
calendar: _calendar,
space: calendar.space.Calendar,
modifiedBy: getCurrentAccount().primarySocialId,
modifiedBy: myAcc.primarySocialId,
participants: [getCurrentEmployee()],
modifiedOn: Date.now(),
date: e.detail.date.getTime(),
@@ -61,7 +61,7 @@
hidden: false,
default: true
})
const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.primarySocialId}_calendar` as Ref<Calendar>)
const _calendar = extCalendar ? extCalendar._id : (`${currentAccount.uuid}_calendar` as Ref<Calendar>)
const dueDate = date + defaultDuration
await client.addCollection(time.class.WorkSlot, calendar.space.Calendar, todo._id, todo._class, 'workslots', {
eventId: generateEventId(),
@@ -49,7 +49,7 @@
let txes = new Map<Ref<Person>, Tx[]>()
$: personsSocialStrings = persons.map((p) => ($socialIdsByPersonRefStore.get(p) ?? []).map((si) => si.key)).flat()
$: personsSocialStrings = persons.map((p) => ($socialIdsByPersonRefStore.get(p) ?? []).map((si) => si._id)).flat()
$: txCreateQuery.query(
core.class.Tx,
{ modifiedBy: { $in: personsSocialStrings }, modifiedOn: { $gt: fromDate, $lt: toDate } },
@@ -29,7 +29,7 @@
viewOptions,
viewletId: getActiveViewletId(),
sharable,
users: [getCurrentAccount().primarySocialId]
users: [getCurrentAccount().uuid]
})
}
+3 -2
View File
@@ -37,7 +37,8 @@ import {
Tx,
TxOperations,
Type,
UXObject
UXObject,
AccountUuid
} from '@hcengineering/core'
import { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference'
@@ -112,7 +113,7 @@ export interface FilteredView extends Doc {
filterClass?: Ref<Class<Doc>>
viewletId?: Ref<Viewlet> | null
sharable?: boolean
users: PersonId[]
users: AccountUuid[]
createdBy: PersonId
attachedTo: string
}
@@ -1,5 +1,5 @@
<script lang="ts">
import contact, { includesAny } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import { Ref, getCurrentAccount, toIdMap } from '@hcengineering/core'
import { copyTextToClipboard, createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
@@ -48,8 +48,8 @@
let myFilteredViews: FilteredView[] = []
$: if (alias !== undefined) {
filteredViewsQuery.query<FilteredView>(view.class.FilteredView, { attachedTo: alias }, (result) => {
myFilteredViews = result.filter((p) => includesAny(p.users, myAcc.socialIds))
availableFilteredViews = result.filter((p) => p.sharable && !includesAny(p.users, myAcc.socialIds))
myFilteredViews = result.filter((p) => p.users.includes(myAcc.uuid))
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(myAcc.uuid))
const location = getLocation()
if (location.query?.filterViewId) {
@@ -152,7 +152,7 @@
icon: view.icon.Archive,
label: view.string.Hide,
action: async (ctx: any, evt: Event) => {
await client.update(object, { $pull: { users: { $in: myAcc.socialIds } } })
await client.update(object, { $pull: { users: myAcc.uuid } })
}
}
]
@@ -239,7 +239,7 @@
const pushMeToFV = async (id: Ref<FilteredView>): Promise<void> => {
if (id === undefined) return
const filteredView = filteredViewsIdMap.get(id)
if (filteredView) await client.update(filteredView, { $push: { users: myAcc.primarySocialId } })
if (filteredView) await client.update(filteredView, { $push: { users: myAcc.uuid } })
}
const value = availableFilteredViews.map((p) => ({
id: p._id,
@@ -191,7 +191,7 @@
const query = createQuery()
$: query.query(
workbench.class.WorkbenchTab,
{ attachedTo: { $in: account.socialIds } },
{ attachedTo: account.uuid },
(res) => {
tabs = res
tabsStore.set(tabs)
@@ -241,7 +241,7 @@
} else {
console.log('Creating new tab on init')
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: account.primarySocialId,
attachedTo: account.uuid,
location: url,
isPinned: false
})
+2 -2
View File
@@ -347,8 +347,8 @@ export async function connect (title: string): Promise<Client | undefined> {
const me: Account = {
uuid: account,
role: workspaceLoginInfo.role,
primarySocialId: pickPrimarySocialId(socialIds).key,
socialIds: socialIds.map((si) => si.key)
primarySocialId: pickPrimarySocialId(socialIds)._id,
socialIds: socialIds.map((si) => si._id)
}
// Ensure employee and social identifiers
+2 -2
View File
@@ -103,7 +103,7 @@ const syncTabLoc = reduceCalls(async (): Promise<void> => {
space: core.space.Workspace,
location: url,
name,
attachedTo: me.primarySocialId,
attachedTo: me.uuid,
isPinned: false,
modifiedOn: Date.now(),
modifiedBy: me.primarySocialId
@@ -210,7 +210,7 @@ export async function createTab (): Promise<void> {
const name = await translate(notification.string.Inbox, {}, get(languageStore))
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: getCurrentAccount().primarySocialId,
attachedTo: getCurrentAccount().uuid,
location: defaultUrl,
isPinned: false,
name
+2 -2
View File
@@ -14,7 +14,7 @@
// limitations under the License.
//
import type { AccountRole, Class, Doc, DocumentQuery, Obj, PersonId, Ref, Space, Tx } from '@hcengineering/core'
import type { AccountRole, AccountUuid, Class, Doc, DocumentQuery, Obj, Ref, Space, Tx } from '@hcengineering/core'
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
@@ -115,7 +115,7 @@ export interface TxSidebarEvent<T extends Record<string, any> = Record<string, a
/** @public */
export interface WorkbenchTab extends Preference {
attachedTo: PersonId
attachedTo: AccountUuid
location: string
isPinned: boolean
name?: string
@@ -37,8 +37,7 @@ import core, {
TxUpdateDoc,
Type,
type MeasureContext,
AccountUuid,
buildSocialIdString
AccountUuid
} from '@hcengineering/core'
import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
@@ -82,7 +81,7 @@ export async function getPersonNotificationTxes (
): Promise<Tx[]> {
const receiver = reference.attachedTo as Ref<Person>
const receiverSocialIds = await control.findAll(ctx, contact.class.SocialIdentity, { attachedTo: receiver })
const receiverSocialStrings = receiverSocialIds.map(buildSocialIdString)
const receiverSocialStrings = receiverSocialIds.map((si) => si._id) as PersonId[]
if (receiverSocialStrings.includes(senderId)) {
return []
@@ -178,7 +177,7 @@ export async function getPersonNotificationTxes (
const senderInfo = {
_id: senderId,
person: senderPerson,
socialStrings: senderSocialIds.map((si) => si.key)
socialStrings: senderSocialIds.map((si) => si._id)
}
const notifyResult = await shouldNotifyCommon(
+11 -9
View File
@@ -24,8 +24,8 @@ import core, {
UserStatus
} from '@hcengineering/core'
import { TriggerControl } from '@hcengineering/server-core'
import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact'
import { aiBotEmailSocialId, AIEventRequest } from '@hcengineering/ai-bot'
import { getAccountBySocialKey } from '@hcengineering/server-contact'
import { aiBotEmailSocialKey, AIEventRequest } from '@hcengineering/ai-bot'
import { createAccountRequest, hasAiEndpoint, sendAIEvents } from './utils'
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
@@ -35,7 +35,7 @@ async function OnUserStatus (txes: TxCUD<UserStatus>[], control: TriggerControl)
return []
}
const account = await getAccountBySocialId(control, aiBotEmailSocialId)
let account = await getAccountBySocialKey(control, aiBotEmailSocialKey)
if (control.ctx.contextData.account.uuid === account) {
return []
@@ -67,11 +67,13 @@ async function OnUserStatus (txes: TxCUD<UserStatus>[], control: TriggerControl)
}
}
const aiBotPerson = await getPerson(control, aiBotEmailSocialId)
if (account === undefined) {
account = await getAccountBySocialKey(control, aiBotEmailSocialKey)
if (aiBotPerson === undefined) {
await createAccountRequest(control.workspace.uuid, control.ctx)
return []
if (account === undefined) {
await createAccountRequest(control.workspace.uuid, control.ctx)
return []
}
}
}
@@ -84,7 +86,7 @@ async function OnMessageSend (originTxs: TxCreateDoc<ChatMessage>[], control: Tr
}
const { hierarchy } = control
const txes = originTxs.filter((it) => it.modifiedBy !== aiBotEmailSocialId)
const txes = originTxs.filter((it) => it.modifiedBy !== aiBotEmailSocialKey)
if (txes.length === 0) {
return []
@@ -153,7 +155,7 @@ async function getMessageDoc (message: ChatMessage, control: TriggerControl): Pr
async function isDirectAvailable (direct: DirectMessage, control: TriggerControl): Promise<boolean> {
const { members } = direct
const account = await getAccountBySocialId(control, aiBotEmailSocialId)
const account = await getAccountBySocialKey(control, aiBotEmailSocialKey)
if (account == null) {
return false
@@ -13,8 +13,8 @@
// limitations under the License.
//
import chunter, { Channel } from '@hcengineering/chunter'
import core, { MeasureContext, PersonId, Ref, TxOperations, type WorkspaceUuid } from '@hcengineering/core'
import { getAllUserAccounts, getAllSocialStringsByPersonId, Person } from '@hcengineering/contact'
import core, { AccountUuid, MeasureContext, Ref, TxOperations, type WorkspaceUuid } from '@hcengineering/core'
import { getAllUserAccounts, Person } from '@hcengineering/contact'
import analyticsCollector, { getOnboardingChannelName, OnboardingChannel } from '@hcengineering/analytics-collector'
import { translate } from '@hcengineering/platform'
@@ -27,27 +27,26 @@ interface WorkspaceInfo {
export async function getOrCreateOnboardingChannel (
ctx: MeasureContext,
client: TxOperations,
socialString: PersonId,
account: AccountUuid,
workspace: WorkspaceInfo,
person?: Person
): Promise<[Ref<OnboardingChannel> | undefined, boolean]> {
// TODO: FIXME
const personIds = await getAllSocialStringsByPersonId(client, socialString)
const channel = await client.findOne(analyticsCollector.class.OnboardingChannel, {
workspaceId: workspace.workspaceId,
personId: { $in: personIds }
account
})
if (channel !== undefined) {
return [channel._id, false]
}
ctx.info('Creating user onboarding channel', { personId: socialString, workspace })
const user = person?.name ?? account
ctx.info('Creating user onboarding channel', { account, workspace, user })
const _id = await client.createDoc(analyticsCollector.class.OnboardingChannel, core.space.Space, {
name: getOnboardingChannelName(workspace.workspaceUrl, socialString),
name: getOnboardingChannelName(workspace.workspaceUrl, user),
topic: await translate(analyticsCollector.string.OnboardingChannelDescription, {
user: person?.name ?? socialString,
user,
workspace: workspace.workspaceName
}),
description: '',
@@ -55,11 +54,11 @@ export async function getOrCreateOnboardingChannel (
members: [],
autoJoin: false,
archived: false,
socialString,
account,
workspaceId: workspace.workspaceId,
workspaceUrl: workspace.workspaceUrl,
workspaceName: workspace.workspaceName,
userName: person?.name ?? socialString,
userName: user,
disableAIReplies: false,
showAIReplies: true
})
+27 -15
View File
@@ -24,8 +24,6 @@ import core, {
FindResult,
Hierarchy,
PersonId,
buildSocialIdString,
parseSocialIdString,
Ref,
systemAccountUuid,
Tx,
@@ -34,7 +32,8 @@ import core, {
TxMixin,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
TxUpdateDoc,
AccountUuid
} from '@hcengineering/core'
import serverCalendar from '@hcengineering/server-calendar'
import { getMetadata, getResource } from '@hcengineering/platform'
@@ -96,14 +95,24 @@ export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise<
if (ctx.attributes?.active !== true) continue
if (await checkCalendarsExist(control, ctx.objectId)) continue
const socialStrings = await getSocialStrings(control, ctx.objectId)
if (socialStrings.length === 0) continue
const socialIds = await getSocialStrings(control, ctx.objectId)
if (socialIds.length === 0) continue
const socialString = pickPrimarySocialId(socialStrings)
const { value } = parseSocialIdString(socialString)
const socialId = pickPrimarySocialId(socialIds)
result.push(...(await createCalendar(control, socialString, value)))
const employee = (
await control.findAll(
control.ctx,
contactPlugin.mixin.Employee,
{ _id: ctx.objectId as Ref<Employee> },
{ limit: 1 }
)
)[0]
if (employee?.personUuid === undefined) continue
result.push(...(await createCalendar(control, employee.personUuid, socialId, socialId)))
}
return result
}
@@ -117,13 +126,11 @@ export async function OnSocialIdentityCreate (txes: Tx[], control: TriggerContro
const employee = (
await control.findAll(control.ctx, contactPlugin.mixin.Employee, { _id: socialId.attachedTo as Ref<Employee> })
)[0]
if (employee === undefined || !employee.active) continue
if (employee === undefined || !employee.active || employee.personUuid === undefined) continue
if (await checkCalendarsExist(control, employee._id)) continue
const socialString = buildSocialIdString(socialId)
result.push(...(await createCalendar(control, socialString, socialId.value)))
result.push(...(await createCalendar(control, employee.personUuid, socialId._id, socialId.value)))
}
return result
}
@@ -140,7 +147,12 @@ async function checkCalendarsExist (control: TriggerControl, person: Ref<Person>
return calendars.length > 0
}
async function createCalendar (control: TriggerControl, socialString: PersonId, name: string): Promise<Tx[]> {
async function createCalendar (
control: TriggerControl,
account: AccountUuid,
socialId: PersonId,
name: string
): Promise<Tx[]> {
const res: TxCreateDoc<Calendar> = control.txFactory.createTxCreateDoc(
calendar.class.Calendar,
calendar.space.Calendar,
@@ -149,9 +161,9 @@ async function createCalendar (control: TriggerControl, socialString: PersonId,
hidden: false,
visibility: 'public'
},
`${socialString}_calendar` as Ref<Calendar>,
`${account}_calendar` as Ref<Calendar>,
undefined,
socialString
socialId
)
return [res]
}
@@ -45,7 +45,7 @@ import core, {
} from '@hcengineering/core'
import notification, { Collaborators } from '@hcengineering/notification'
import { getMetadata } from '@hcengineering/platform'
import { getAccountBySocialId, getTriggerCurrentPerson } from '@hcengineering/server-contact'
import { getAccountBySocialId, getCurrentPerson } from '@hcengineering/server-contact'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { workbenchId } from '@hcengineering/workbench'
@@ -263,13 +263,13 @@ export function contactNameProvider (
}
export async function getCurrentEmployeeName (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
const person = await getTriggerCurrentPerson(control)
const person = await getCurrentPerson(control)
return person !== undefined ? formatName(person.name, control.branding?.lastNameFirst) : ''
}
export async function getCurrentEmployeeEmail (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
const person = await getTriggerCurrentPerson(control)
const person = await getCurrentPerson(control)
if (person === undefined) return ''
const emailSocialId = (
@@ -288,7 +288,7 @@ export async function getCurrentEmployeePosition (
control: TriggerControl,
context: Record<string, Doc>
): Promise<string | undefined> {
const person = await getTriggerCurrentPerson(control)
const person = await getCurrentPerson(control)
if (person === undefined) return ''
return control.hierarchy.as(person, contact.mixin.Employee)?.position ?? ''
+46 -13
View File
@@ -14,10 +14,10 @@
//
import { TriggerControl } from '@hcengineering/server-core'
import contact, { Employee, pickPrimarySocialId, type Person } from '@hcengineering/contact'
import contact, { Employee, pickPrimarySocialId, SocialIdentityRef, type Person } from '@hcengineering/contact'
import { AccountUuid, parseSocialIdString, PersonId, type Ref, toIdMap } from '@hcengineering/core'
export async function getTriggerCurrentPerson (control: TriggerControl): Promise<Person | undefined> {
export async function getCurrentPerson (control: TriggerControl): Promise<Person | undefined> {
const { type, value } = parseSocialIdString(control.txFactory.account)
const socialIdentity = (await control.findAll(control.ctx, contact.class.SocialIdentity, { type, value }))[0]
@@ -41,7 +41,7 @@ export async function getSocialStrings (control: TriggerControl, person: Ref<Per
attachedToClass: contact.class.Person
})
return socialIdentities.map((s) => s.key)
return socialIdentities.map((s) => s._id)
}
export async function getSocialStringsByPersons (
@@ -58,7 +58,7 @@ export async function getSocialStringsByPersons (
acc[s.attachedTo] = []
}
acc[s.attachedTo].push(s.key)
acc[s.attachedTo].push(s._id)
return acc
}, {})
@@ -68,13 +68,15 @@ export async function getAllSocialStringsByPersonId (
control: TriggerControl,
personIds: PersonId[]
): Promise<PersonId[]> {
const socialIdentities = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } })
const socialIdentities = await control.findAll(control.ctx, contact.class.SocialIdentity, {
_id: { $in: personIds as SocialIdentityRef[] }
})
const allSocialIdentities = await control.findAll(control.ctx, contact.class.SocialIdentity, {
attachedTo: { $in: socialIdentities.map((sid) => sid.attachedTo) },
attachedToClass: contact.class.Person
})
return allSocialIdentities.map((sid) => sid.key)
return allSocialIdentities.map((sid) => sid._id)
}
export async function getPerson (control: TriggerControl, personId: PersonId): Promise<Person | undefined> {
@@ -92,7 +94,9 @@ export async function getPersonsBySocialIds (
control: TriggerControl,
personIds: PersonId[]
): Promise<Record<PersonId, Person>> {
const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } })
const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, {
_id: { $in: personIds as SocialIdentityRef[] }
})
const persons = toIdMap(
await control.findAll(control.ctx, contact.class.Person, { _id: { $in: socialIds.map((s) => s.attachedTo) } })
)
@@ -100,7 +104,7 @@ export async function getPersonsBySocialIds (
return socialIds.reduce<Record<PersonId, Person>>((acc, s) => {
const person = persons.get(s.attachedTo)
if (person !== undefined) {
acc[s.key] = person
acc[s._id] = person
} else {
console.error('No person found for social id', s.key)
}
@@ -139,15 +143,17 @@ export async function getEmployeesBySocialIds (
control: TriggerControl,
personIds: PersonId[]
): Promise<Record<PersonId, Employee | undefined>> {
const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, { key: { $in: personIds } })
const socialIds = await control.findAll(control.ctx, contact.class.SocialIdentity, {
_id: { $in: personIds as SocialIdentityRef[] }
})
const employees = toIdMap(
await control.findAll(control.ctx, contact.mixin.Employee, {
_id: { $in: socialIds.map((s) => s.attachedTo as Ref<Employee>) }
})
)
return socialIds.reduce<Record<string, Employee | undefined>>((acc, s) => {
acc[s.key] = employees.get(s.attachedTo as Ref<Employee>)
return socialIds.reduce<Record<PersonId, Employee | undefined>>((acc, s) => {
acc[s._id] = employees.get(s.attachedTo as Ref<Employee>)
return acc
}, {})
@@ -173,7 +179,7 @@ export async function getSocialIdsByAccounts (
acc[employee.personUuid] = []
}
acc[employee.personUuid].push(sid.key)
acc[employee.personUuid].push(sid._id)
return acc
}, {})
}
@@ -195,7 +201,34 @@ export async function getAccountBySocialId (control: TriggerControl, socialId: P
const socialIdentity = await control.findAll(
control.ctx,
contact.class.SocialIdentity,
{ key: socialId },
{ _id: socialId as SocialIdentityRef },
{ limit: 1 }
)
if (socialIdentity.length === 0) {
return null
}
const employee = await control.findAll(
control.ctx,
contact.mixin.Employee,
{ _id: socialIdentity[0].attachedTo as Ref<Employee> },
{ limit: 1 }
)
return employee[0]?.personUuid ?? null
}
/**
* It should only be used for well-known social identities. Should never be used for regular users as social key might change.
* @param control
* @param socialId
*/
export async function getAccountBySocialKey (control: TriggerControl, socialKey: string): Promise<AccountUuid | null> {
const socialIdentity = await control.findAll(
control.ctx,
contact.class.SocialIdentity,
{ key: socialKey },
{ limit: 1 }
)
@@ -461,8 +461,9 @@ export async function getUsersInfo (
control: TriggerControl
): Promise<Map<PersonId, ReceiverInfo | SenderInfo>> {
if (ids.length === 0) return new Map()
const uniqueIds = Array.from(new Set(ids))
const employeesBySocialId = await getEmployeesBySocialIds(control, ids)
const employeesBySocialId = await getEmployeesBySocialIds(control, uniqueIds)
const presentEmployeeIds = Object.values(employeesBySocialId)
.map((it) => it?._id)
.filter((it) => it !== undefined)
@@ -482,7 +483,7 @@ export async function getUsersInfo (
const socialStringsByPersons = await getSocialStringsByPersons(control, persons as Ref<Person>[])
return new Map(
ids.map((_id) => {
uniqueIds.map((_id) => {
const employee = employeesBySocialId[_id]
const space = employee !== undefined ? spacesByEmployee.get(employee._id)?.[0] : undefined
const person = employee ?? personsBySocialId[_id]
@@ -25,15 +25,22 @@ import { getMongoAccountDB } from './utils'
import { type Account as OldAccount, type Workspace as OldWorkspace } from './types'
import { type MongoAccountDB } from './collections/mongo'
async function shouldMigrate (oldAccountDb: MongoAccountDB, migrationKey: string): Promise<boolean> {
async function shouldMigrate (
oldAccountDb: MongoAccountDB,
migrationKey: string
): Promise<{ completed: boolean, exists: boolean }> {
while (true) {
const migration = await oldAccountDb.migration.findOne({ key: migrationKey })
if (migration?.completed === true) {
return false
return { completed: true, exists: true }
}
if (migration?.lastProcessedTime === undefined || Date.now() - migration.lastProcessedTime > 1000 * 15) {
return true
if (migration == null) {
return { completed: false, exists: false }
}
if (migration.lastProcessedTime === undefined || Date.now() - migration.lastProcessedTime > 1000 * 15) {
return { completed: false, exists: true }
}
console.log('Migration of accounts database from old accounts is still in progress, waiting...')
@@ -48,11 +55,16 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc
let processingHandle
try {
if (!(await shouldMigrate(oldAccountDb, migrationKey))) {
const { completed, exists } = await shouldMigrate(oldAccountDb, migrationKey)
if (completed) {
return
}
await oldAccountDb.migration.insertOne({ key: migrationKey, completed: false, lastProcessedTime: Date.now() })
if (!exists) {
await oldAccountDb.migration.insertOne({ key: migrationKey, completed: false, lastProcessedTime: Date.now() })
} else {
await oldAccountDb.migration.updateOne({ key: migrationKey }, { completed: false, lastProcessedTime: Date.now() })
}
processingHandle = setInterval(() => {
void oldAccountDb.migration.updateOne({ key: migrationKey }, { lastProcessedTime: Date.now() })
+20 -1
View File
@@ -673,10 +673,12 @@ describe('WorkspaceStatusMongoDbCollection', () => {
describe('MongoAccountDB', () => {
let mockDb: any
let accountDb: MongoAccountDB
let mockSocialId: any
let mockAccount: any
let mockWorkspace: any
let mockWorkspaceMembers: any
let mockWorkspaceStatus: any
let mockMigration: any
beforeEach(() => {
mockDb = {}
@@ -687,6 +689,15 @@ describe('MongoAccountDB', () => {
ensureIndices: jest.fn()
}
mockSocialId = {
find: jest.fn().mockResolvedValue([]),
findCursor: jest.fn(() => ({
hasNext: jest.fn().mockReturnValue(false),
close: jest.fn()
})),
updateOne: jest.fn()
}
mockWorkspace = {
updateOne: jest.fn(),
insertOne: jest.fn(),
@@ -710,14 +721,22 @@ describe('MongoAccountDB', () => {
insertOne: jest.fn()
}
mockMigration = {
insertOne: jest.fn(),
updateOne: jest.fn(),
findOne: jest.fn()
}
accountDb = new MongoAccountDB(mockDb)
// Override the getters to return our mocks
Object.defineProperties(accountDb, {
account: { get: () => mockAccount },
socialId: { get: () => mockSocialId },
workspace: { get: () => mockWorkspace },
workspaceMembers: { get: () => mockWorkspaceMembers },
workspaceStatus: { get: () => mockWorkspaceStatus }
workspaceStatus: { get: () => mockWorkspaceStatus },
migration: { get: () => mockMigration }
})
})
+15 -9
View File
@@ -693,12 +693,15 @@ describe('account utils', () => {
})
})
const socialIdId = '333444555' as PersonId
describe('sendOtp', () => {
const mockSocialId = {
_id: socialIdId,
personUuid: '123456-uuid' as PersonUuid,
type: SocialIdType.EMAIL,
value: 'test@example.com',
key: 'email:test@example.com' as PersonId
key: 'email:test@example.com'
}
test('should return existing OTP if not expired', async () => {
@@ -730,7 +733,7 @@ describe('account utils', () => {
retryOn: expect.any(Number)
})
expect(mockDb.otp.insertOne).toHaveBeenCalledWith({
socialId: mockSocialId.key,
socialId: mockSocialId._id,
code: expect.any(String),
expiresOn: expect.any(Number),
createdOn: expect.any(Number)
@@ -739,10 +742,11 @@ describe('account utils', () => {
test('should throw error for unsupported social id type', async () => {
const invalidSocialId = {
_id: '999888777' as PersonId,
personUuid: '123456-uuid' as PersonUuid,
type: 'INVALID' as SocialIdType,
value: 'test',
key: 'invalid:test' as PersonId
key: 'invalid:test'
}
await expect(sendOtp(mockCtx, mockDb, mockBranding, invalidSocialId)).rejects.toThrow(
@@ -788,7 +792,7 @@ describe('account utils', () => {
}
;(mockDb.otp.findOne as jest.Mock).mockResolvedValue(mockOtpData)
const result = await isOtpValid(mockDb, 'email:test@example.com', '123456')
const result = await isOtpValid(mockDb, socialIdId, '123456')
expect(result).toBe(true)
})
@@ -798,14 +802,14 @@ describe('account utils', () => {
}
;(mockDb.otp.findOne as jest.Mock).mockResolvedValue(mockOtpData)
const result = await isOtpValid(mockDb, 'email:test@example.com', '123456')
const result = await isOtpValid(mockDb, socialIdId, '123456')
expect(result).toBe(false)
})
test('should return false for non-existent OTP', async () => {
;(mockDb.otp.findOne as jest.Mock).mockResolvedValue(null)
const result = await isOtpValid(mockDb, 'email:test@example.com', '123456')
const result = await isOtpValid(mockDb, socialIdId, '123456')
expect(result).toBe(false)
})
})
@@ -1290,7 +1294,7 @@ describe('account utils', () => {
const result = await signUpByEmail(mockCtx, mockDb, mockBranding, email, password, firstName, lastName)
expect(result).toBe(personUuid)
expect(result.account).toBe(personUuid)
expect(mockDb.person.insertOne).toHaveBeenCalledWith({ firstName, lastName })
expect(mockDb.socialId.insertOne).toHaveBeenCalledWith({
type: SocialIdType.EMAIL,
@@ -1320,7 +1324,7 @@ describe('account utils', () => {
const result = await signUpByEmail(mockCtx, mockDb, mockBranding, email, password, firstName, lastName)
expect(result).toBe(personUuid)
expect(result.account).toBe(personUuid)
expect(mockDb.person.updateOne).toHaveBeenCalledWith({ uuid: personUuid }, { firstName, lastName })
expect(mockDb.account.insertOne).toHaveBeenCalledWith({ uuid: personUuid })
expect(mockDb.setPassword).toHaveBeenCalledWith(personUuid, expect.any(Buffer), expect.any(Buffer))
@@ -1727,8 +1731,10 @@ describe('account utils', () => {
const firstName = 'John'
const lastName = 'Doe'
const personUuid = 'new-person' as PersonUuid
const mockSocialIdUuid = 'mock-social-id-uuid'
;(mockDb.socialId.findOne as jest.Mock).mockResolvedValue(null)
;(mockDb.socialId.insertOne as jest.Mock).mockResolvedValue(mockSocialIdUuid)
;(mockDb.person.insertOne as jest.Mock).mockResolvedValue(personUuid)
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({ firstName, lastName })
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null)
@@ -1745,7 +1751,7 @@ describe('account utils', () => {
expect(result).toEqual({
account: personUuid,
socialId: expect.any(String),
socialId: mockSocialIdUuid,
name: 'John Doe',
token: 'new-token'
})
+103 -4
View File
@@ -134,7 +134,9 @@ implements DbCollection<T> {
}
return cursor.map((doc) => {
delete doc._id
if (this.idKey !== '_id') {
delete doc._id
}
return doc
})
@@ -146,7 +148,9 @@ implements DbCollection<T> {
return null
}
delete doc._id
if (this.idKey !== '_id') {
delete doc._id
}
return doc
}
@@ -216,9 +220,9 @@ export class AccountMongoDbCollection extends MongoDbCollection<Account, 'uuid'>
}
}
export class SocialIdMongoDbCollection extends MongoDbCollection<SocialId, 'key'> implements DbCollection<SocialId> {
export class SocialIdMongoDbCollection extends MongoDbCollection<SocialId, '_id'> implements DbCollection<SocialId> {
constructor (db: Db) {
super('socialId', db, 'key')
super('socialId', db, '_id')
}
async insertOne (data: Partial<SocialId>): Promise<any> {
@@ -344,7 +348,19 @@ interface WorkspaceMember {
role: AccountRole
}
interface Migration {
key: string
op: () => Promise<void>
}
interface MigrationInfo {
key: string
completed: boolean
lastProcessedTime: number
}
export class MongoAccountDB implements AccountDB {
migration: MongoDbCollection<MigrationInfo, 'key'>
person: MongoDbCollection<Person, 'uuid'>
socialId: SocialIdMongoDbCollection
workspace: MongoDbCollection<WorkspaceInfoWithStatus, 'uuid'>
@@ -357,6 +373,7 @@ export class MongoAccountDB implements AccountDB {
workspaceMembers: MongoDbCollection<WorkspaceMember>
constructor (readonly db: Db) {
this.migration = new MongoDbCollection<MigrationInfo, 'key'>('migration', db, 'key')
this.person = new MongoDbCollection<Person, 'uuid'>('person', db, 'uuid')
this.socialId = new SocialIdMongoDbCollection(db)
this.workspace = new MongoDbCollection<WorkspaceInfoWithStatus, 'uuid'>('workspace', db, 'uuid')
@@ -370,6 +387,11 @@ export class MongoAccountDB implements AccountDB {
}
async init (): Promise<void> {
// Apply all the migrations
for (const migration of this.getMigrations()) {
await this.migrate(migration)
}
await this.account.ensureIndices([
{
key: { uuid: 1 },
@@ -410,6 +432,83 @@ export class MongoAccountDB implements AccountDB {
])
}
async migrate ({ key, op }: Migration): Promise<void> {
const { completed, exists } = await this.shouldMigrate(key)
if (completed) {
return
}
console.log(`Applying migration: ${key}`)
if (!exists) {
await this.migration.insertOne({ key, completed: false, lastProcessedTime: Date.now() })
} else {
await this.migration.updateOne({ key }, { lastProcessedTime: Date.now() })
}
const processingHandle = setInterval(() => {
void this.migration.updateOne({ key }, { lastProcessedTime: Date.now() })
}, 1000 * 5)
await op()
await this.migration.updateOne({ key }, { completed: true, lastProcessedTime: Date.now() })
clearInterval(processingHandle)
console.log(`Migration ${key} completed`)
}
async shouldMigrate (key: string): Promise<{ completed: boolean, exists: boolean }> {
while (true) {
const migrationInfo = await this.migration.findOne({ key })
if (migrationInfo?.completed === true) {
return { completed: true, exists: true }
}
if (migrationInfo == null) {
return { completed: false, exists: false }
}
if (migrationInfo.lastProcessedTime === undefined || Date.now() - migrationInfo.lastProcessedTime > 1000 * 15) {
return { completed: false, exists: true }
}
console.log(`Migration ${key} is in progress by other process, waiting...`)
await new Promise((resolve) => setTimeout(resolve, 5000))
}
}
protected getMigrations (): Migration[] {
return [this.getV1Migration()]
}
// NOTE: NEVER MODIFY EXISTING MIGRATIONS. IF YOU NEED TO DO SOMETHING, ADD A NEW MIGRATION.
private getV1Migration (): Migration {
return {
key: 'account_db_v1_fill_social_id_ids',
op: async () => {
const sidCursor = this.socialId.findCursor({})
try {
let sidsCount = 0
while (await sidCursor.hasNext()) {
const socialIdObj = await sidCursor.next()
if (socialIdObj == null) break
if (socialIdObj._id != null && socialIdObj._id !== socialIdObj.key) continue
await this.socialId.deleteMany({ key: socialIdObj.key })
const newSocialId: any = { ...socialIdObj }
delete newSocialId._id
await this.socialId.insertOne(newSocialId)
sidsCount++
}
console.log(`Migrated ${sidsCount} social ids`)
} finally {
await sidCursor.close()
}
}
}
}
async assignWorkspace (accountId: PersonUuid, workspaceId: WorkspaceUuid, role: AccountRole): Promise<void> {
await this.workspaceMembers.insertOne({
workspaceUuid: workspaceId,
+68 -6
View File
@@ -43,11 +43,19 @@ import type {
} from '../types'
function toSnakeCase (str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
// Preserve leading underscore
const hasLeadingUnderscore = str.startsWith('_')
const baseStr = hasLeadingUnderscore ? str.slice(1) : str
const converted = baseStr.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
return hasLeadingUnderscore ? '_' + converted : converted
}
function toCamelCase (str: string): string {
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
// Preserve leading underscore
const hasLeadingUnderscore = str.startsWith('_')
const baseStr = hasLeadingUnderscore ? str.slice(1) : str
const converted = baseStr.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
return hasLeadingUnderscore ? '_' + converted : converted
}
function convertKeysToCamelCase (obj: any): any {
@@ -108,7 +116,7 @@ implements DbCollection<T> {
for (const key of Object.keys(query)) {
const qKey = query[key]
const operator = typeof qKey === 'object' ? Object.keys(qKey)[0] : ''
const operator = qKey != null && typeof qKey === 'object' ? Object.keys(qKey)[0] : ''
const snakeKey = toSnakeCase(key)
switch (operator) {
case '$in': {
@@ -362,7 +370,7 @@ export class PostgresAccountDB implements AccountDB {
person: PostgresDbCollection<Person, 'uuid'>
account: AccountPostgresDbCollection
socialId: PostgresDbCollection<SocialId, 'key'>
socialId: PostgresDbCollection<SocialId, '_id'>
workspace: PostgresDbCollection<Workspace, 'uuid'>
workspaceStatus: PostgresDbCollection<WorkspaceStatus>
accountEvent: PostgresDbCollection<AccountEvent>
@@ -375,7 +383,7 @@ export class PostgresAccountDB implements AccountDB {
) {
this.person = new PostgresDbCollection<Person, 'uuid'>('person', client, 'uuid')
this.account = new AccountPostgresDbCollection(client)
this.socialId = new PostgresDbCollection<SocialId, 'key'>('social_id', client, 'key')
this.socialId = new PostgresDbCollection<SocialId, '_id'>('social_id', client, '_id')
this.workspaceStatus = new PostgresDbCollection<WorkspaceStatus>('workspace_status', client)
this.workspace = new PostgresDbCollection<Workspace, 'uuid'>('workspace', client, 'uuid')
this.accountEvent = new PostgresDbCollection<AccountEvent>('account_events', client)
@@ -623,7 +631,7 @@ export class PostgresAccountDB implements AccountDB {
}
protected getMigrations (): [string, string][] {
return [this.getV1Migration()]
return [this.getV1Migration(), this.getV2Migration1(), this.getV2Migration2(), this.getV2Migration3()]
}
// NOTE: NEVER MODIFY EXISTING MIGRATIONS. IF YOU NEED TO ADJUST THE SCHEMA, ADD A NEW MIGRATION.
@@ -768,4 +776,58 @@ export class PostgresAccountDB implements AccountDB {
`
]
}
private getV2Migration1 (): [string, string] {
return [
'account_db_v2_social_id_id_add',
`
-- Add _id column to social_id table
ALTER TABLE ${this.ns}.social_id
ADD COLUMN IF NOT EXISTS _id INT8 NOT NULL DEFAULT unique_rowid();
`
]
}
private getV2Migration2 (): [string, string] {
return [
'account_db_v2_social_id_pk_change',
`
-- Drop existing otp foreign key constraint
ALTER TABLE ${this.ns}.otp
DROP CONSTRAINT IF EXISTS otp_social_id_fk;
-- Drop existing primary key on social_id
ALTER TABLE ${this.ns}.social_id
DROP CONSTRAINT IF EXISTS social_id_pk;
-- Add new primary key on _id
ALTER TABLE ${this.ns}.social_id
ADD CONSTRAINT social_id_pk PRIMARY KEY (_id);
`
]
}
private getV2Migration3 (): [string, string] {
return [
'account_db_v2_social_id_constraints',
`
-- Add unique constraint on type, value
ALTER TABLE ${this.ns}.social_id
ADD CONSTRAINT social_id_tv_key_unique UNIQUE (type, value);
-- Drop old table
DROP TABLE ${this.ns}.otp;
-- Create new OTP table with correct column type
CREATE TABLE ${this.ns}.otp (
social_id INT8 NOT NULL,
code STRING NOT NULL,
expires_on BIGINT NOT NULL,
created_on BIGINT NOT NULL DEFAULT current_epoch_ms(),
CONSTRAINT otp_new_pk PRIMARY KEY (social_id, code),
CONSTRAINT otp_new_social_id_fk FOREIGN KEY (social_id) REFERENCES ${this.ns}.social_id(_id)
);
`
]
}
}
+86 -36
View File
@@ -93,9 +93,11 @@ import {
setPassword,
signUpByEmail,
verifyAllowedServices,
verifyAllowedRole,
verifyPassword,
wrap,
getWorkspaceRole
getWorkspaceRole,
normalizeValue
} from './utils'
// Move to config?
@@ -155,7 +157,7 @@ export async function login (
account: existingAccount.uuid,
token: isConfirmed ? generateToken(existingAccount.uuid, undefined, extraToken) : undefined,
name: getPersonName(person),
socialId: emailSocialId.key
socialId: emailSocialId._id
}
} catch (err: any) {
Analytics.handleError(err)
@@ -210,7 +212,7 @@ export async function signUp (
}
): Promise<LoginInfo> {
const { email, password, firstName, lastName } = params
const account = await signUpByEmail(ctx, db, branding, email, password, firstName, lastName)
const { account, socialId } = await signUpByEmail(ctx, db, branding, email, password, firstName, lastName)
const person = await db.person.findOne({ uuid: account })
if (person == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {}))
@@ -230,7 +232,7 @@ export async function signUp (
return {
account,
name: getPersonName(person),
socialId: buildSocialIdString({ type: SocialIdType.EMAIL, value: email }),
socialId,
token: !forceConfirmation ? generateToken(account) : undefined
}
}
@@ -267,8 +269,8 @@ export async function signUpOtp (
// There's no person linked to this email, so we need to create a new one
personUuid = await db.person.insertOne({ firstName, lastName })
const newSocialId = { type: SocialIdType.EMAIL, value: normalizedEmail, personUuid }
const emailSocialIdKey = await db.socialId.insertOne(newSocialId)
emailSocialId = { ...newSocialId, key: emailSocialIdKey }
const emailSocialIdId = await db.socialId.insertOne(newSocialId)
emailSocialId = { ...newSocialId, _id: emailSocialIdId, key: buildSocialIdString(newSocialId) }
}
return await sendOtp(ctx, db, branding, emailSocialId)
@@ -294,16 +296,16 @@ export async function validateOtp (
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: email }))
}
const isValid = await isOtpValid(db, emailSocialId.key, code)
const isValid = await isOtpValid(db, emailSocialId._id, code)
if (!isValid) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidOtp, {}))
}
await db.otp.deleteMany({ socialId: emailSocialId.key })
await db.otp.deleteMany({ socialId: emailSocialId._id })
if (emailSocialId.verifiedOn == null) {
await db.socialId.updateOne({ key: emailSocialId.key }, { verifiedOn: Date.now() })
await db.socialId.updateOne({ _id: emailSocialId._id }, { verifiedOn: Date.now() })
}
// This method handles both login and signup
@@ -328,7 +330,7 @@ export async function validateOtp (
return {
account: emailSocialId.personUuid,
name: getPersonName(person),
socialId: emailSocialId.key,
socialId: emailSocialId._id,
token: generateToken(emailSocialId.personUuid)
}
}
@@ -380,7 +382,7 @@ export async function createWorkspace (
return {
account,
socialId: socialId.key,
socialId: socialId._id,
name: getPersonName(person),
token: generateToken(account, workspaceUuid),
endpoint: getEndpoint(ctx, workspaceUuid, region, EndpointKind.External),
@@ -446,7 +448,7 @@ export async function sendInvite (
}
): Promise<void> {
const { email, role } = params
const { account, workspace: workspaceUuid } = decodeTokenVerbose(ctx, token)
const { account, workspace: workspaceUuid, extra } = decodeTokenVerbose(ctx, token)
const currentAccount = await db.account.findOne({ uuid: account })
if (currentAccount == null) {
@@ -458,6 +460,9 @@ export async function sendInvite (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
const callerRole = await db.getWorkspaceRole(account, workspace.uuid)
verifyAllowedRole(callerRole, role, extra)
checkRateLimit(account, workspaceUuid)
const expHours = 48
@@ -662,7 +667,7 @@ export async function signUpJoin (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
const account = await signUpByEmail(ctx, db, branding, email, password, first, last, true)
const { account } = await signUpByEmail(ctx, db, branding, email, password, first, last, true)
return await doJoinByInvite(ctx, db, branding, generateToken(account, workspaceUuid), account, workspace, invite)
}
@@ -681,7 +686,7 @@ export async function confirm (
throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {}))
}
await confirmEmail(ctx, db, account, email)
const socialId = await confirmEmail(ctx, db, account, email)
const person = await db.person.findOne({ uuid: account })
if (person == null) {
@@ -691,7 +696,7 @@ export async function confirm (
const result = {
account,
name: getPersonName(person),
socialId: buildSocialIdString({ type: SocialIdType.EMAIL, value: email }),
socialId,
token: generateToken(account)
}
@@ -1229,7 +1234,7 @@ export async function getLoginInfoByToken (
const loginInfo = {
account: accountUuid,
name: getPersonName(person),
socialId: socialId?.key,
socialId: socialId?._id,
token
}
@@ -1326,11 +1331,11 @@ export async function getPersonInfo (
return {
personUuid: account,
name: getPersonName(person),
socialIds: verifiedSocialIds.map((it) => it.key)
socialIds: verifiedSocialIds.map((it) => it._id)
}
}
export async function findPerson (
export async function findPersonBySocialKey (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
@@ -1340,7 +1345,7 @@ export async function findPerson (
const { socialString } = params
decodeTokenVerbose(ctx, token)
const socialId = await db.socialId.findOne({ key: socialString as PersonId })
const socialId = await db.socialId.findOne({ key: socialString })
if (socialId == null) {
return
@@ -1349,6 +1354,44 @@ export async function findPerson (
return socialId.personUuid
}
export async function findPersonBySocialId (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
token: string,
params: { socialId: PersonId }
): Promise<PersonUuid | undefined> {
const { socialId } = params
decodeTokenVerbose(ctx, token)
const socialIdObj = await db.socialId.findOne({ _id: socialId })
if (socialIdObj == null) {
return
}
return socialIdObj.personUuid
}
export async function findSocialIdBySocialKey (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
token: string,
params: { socialKey: string }
): Promise<PersonId | undefined> {
const { socialKey } = params
decodeTokenVerbose(ctx, token)
const socialIdObj = await db.socialId.findOne({ key: socialKey })
if (socialIdObj == null) {
return
}
return socialIdObj._id
}
export async function getWorkspaceMembers (
ctx: MeasureContext,
db: AccountDB,
@@ -1370,7 +1413,7 @@ export async function getWorkspaceMembers (
return await db.getWorkspaceMembers(workspace)
}
export async function updateWorkspaceRoleBySocialId (
export async function updateWorkspaceRoleBySocialKey (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
@@ -1733,27 +1776,30 @@ export async function ensurePerson (
lastName: string
}
): Promise<{ uuid: PersonUuid, socialId: PersonId }> {
const { extra } = decodeTokenVerbose(ctx, token)
verifyAllowedServices(['schedule', 'mail'], extra)
const { account, workspace, extra } = decodeTokenVerbose(ctx, token)
const allowedService = verifyAllowedServices(['tool', 'workspace', 'schedule', 'mail'], extra, false)
if (!allowedService) {
const callerRole = await getWorkspaceRole(db, account, workspace)
verifyAllowedRole(callerRole, AccountRole.User, extra)
}
const { socialType, socialValue, firstName, lastName } = params
const trimmedFirst = firstName.trim()
const trimmedLast = lastName.trim()
const normalizedValue = normalizeValue(socialValue)
if (
!Object.values(SocialIdType).includes(socialType) ||
firstName.length === 0 ||
lastName.length === 0 ||
socialValue.length === 0
) {
if (!Object.values(SocialIdType).includes(socialType) || trimmedFirst.length === 0 || normalizedValue.length === 0) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
}
const socialId = await db.socialId.findOne({ type: socialType, value: socialValue })
const socialId = await db.socialId.findOne({ type: socialType, value: normalizedValue })
if (socialId != null) {
return { uuid: socialId.personUuid, socialId: socialId.key }
return { uuid: socialId.personUuid, socialId: socialId._id }
}
const personUuid = await db.person.insertOne({ firstName, lastName })
const newSocialId = await db.socialId.insertOne({ type: socialType, value: socialValue, personUuid })
const personUuid = await db.person.insertOne({ firstName: trimmedFirst, lastName: trimmedLast })
const newSocialId = await db.socialId.insertOne({ type: socialType, value: normalizedValue, personUuid })
return { uuid: personUuid, socialId: newSocialId }
}
@@ -1796,9 +1842,11 @@ export type AccountMethods =
| 'getPersonInfo'
| 'getWorkspaceMembers'
| 'updateWorkspaceRole'
| 'findPerson'
| 'findPersonBySocialKey'
| 'findPersonBySocialId'
| 'findSocialIdBySocialKey'
| 'performWorkspaceOperation'
| 'updateWorkspaceRoleBySocialId'
| 'updateWorkspaceRoleBySocialKey'
| 'ensurePerson'
/**
@@ -1839,7 +1887,9 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
getSocialIds: wrap(getSocialIds),
getPerson: wrap(getPerson),
getPersonInfo: wrap(getPersonInfo),
findPerson: wrap(findPerson),
findPersonBySocialKey: wrap(findPersonBySocialKey),
findPersonBySocialId: wrap(findPersonBySocialId),
findSocialIdBySocialKey: wrap(findSocialIdBySocialKey),
getWorkspaceMembers: wrap(getWorkspaceMembers),
/* SERVICE METHODS */
@@ -1850,7 +1900,7 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
assignWorkspace: wrap(assignWorkspace),
listWorkspaces: wrap(listWorkspaces),
performWorkspaceOperation: wrap(performWorkspaceOperation),
updateWorkspaceRoleBySocialId: wrap(updateWorkspaceRoleBySocialId),
updateWorkspaceRoleBySocialKey: wrap(updateWorkspaceRoleBySocialKey),
ensurePerson: wrap(ensurePerson)
}
}
+5 -7
View File
@@ -20,10 +20,10 @@ import {
MeasureContext,
Timestamp,
Version,
SocialIdType,
WorkspaceMode,
WorkspaceMemberInfo,
BackupStatus,
type SocialId as SocialIdBase,
type PersonUuid,
type WorkspaceUuid,
type WorkspaceDataId,
@@ -41,10 +41,8 @@ export enum Location {
// AccountRole in core
// Person in core
export interface SocialId {
type: SocialIdType
value: string
key: PersonId // Calculated from type and value
export interface SocialId extends SocialIdBase {
personUuid: PersonUuid
createdOn?: Timestamp
verifiedOn?: Timestamp
@@ -106,7 +104,7 @@ export interface Workspace {
}
export interface OTP {
socialId: string
socialId: PersonId
code: string
expiresOn: Timestamp
createdOn: Timestamp
@@ -180,7 +178,7 @@ export type Sort<T> = {
}
export type Query<T> = {
[P in keyof T]?: T[P] | QueryOperator<T[P]>
[P in keyof T]?: T[P] | QueryOperator<T[P]> | null
}
export interface QueryOperator<T> {
+54 -23
View File
@@ -29,8 +29,7 @@ import {
isActiveMode,
type PersonUuid,
type PersonId,
type Person,
buildSocialIdString
type Person
} from '@hcengineering/core'
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
@@ -81,8 +80,7 @@ export async function getAccountDB (uri: string, dbNs?: string): Promise<[Accoun
} else {
const client = getDBClient(sharedPipelineContextVars, uri)
const pgClient = await client.getClient()
// TODO: if dbNs is provided put tables in that schema
const pgAccount = new PostgresAccountDB(pgClient)
const pgAccount = new PostgresAccountDB(pgClient, dbNs ?? 'global_account')
let error = false
@@ -303,6 +301,10 @@ export function cleanEmail (email: string): string {
return email.toLowerCase().trim()
}
export function normalizeValue (value: string): string {
return value.toLowerCase().trim()
}
export function isEmail (email: string): boolean {
// RFC 5322 compliant email regex
const EMAIL_REGEX =
@@ -356,7 +358,7 @@ export async function sendOtp (
socialId: SocialId
): Promise<OtpInfo> {
const ts = Date.now()
const otpData = (await db.otp.find({ socialId: socialId.key }, { createdOn: 'descending' }, 1))[0]
const otpData = (await db.otp.find({ socialId: socialId._id }, { createdOn: 'descending' }, 1))[0]
const retryDelay = getMetadata(accountPlugin.metadata.OtpRetryDelaySec) ?? 30
if (otpData !== undefined && otpData.expiresOn > ts && otpData.createdOn + retryDelay * 1000 > ts) {
@@ -379,7 +381,7 @@ export async function sendOtp (
const code = await generateUniqueOtp(db)
await sendMethod(ctx, branding, code, socialId.value)
await db.otp.insertOne({ socialId: socialId.key, code, expiresOn: ts + ttlMs, createdOn: ts })
await db.otp.insertOne({ socialId: socialId._id, code, expiresOn: ts + ttlMs, createdOn: ts })
return { sent: true, retryOn: ts + retryDelayMs }
}
@@ -423,7 +425,7 @@ export async function sendOtpEmail (
}
}
export async function isOtpValid (db: AccountDB, socialId: string, code: string): Promise<boolean> {
export async function isOtpValid (db: AccountDB, socialId: PersonId, code: string): Promise<boolean> {
const otpData = await db.otp.findOne({ socialId, code })
return (otpData?.expiresOn ?? 0) > Date.now()
@@ -461,11 +463,12 @@ export async function signUpByEmail (
firstName: string,
lastName: string,
confirmed = false
): Promise<PersonUuid> {
): Promise<{ account: PersonUuid, socialId: PersonId }> {
const normalizedEmail = cleanEmail(email)
const emailSocialId = await getEmailSocialId(db, normalizedEmail)
let personUuid: PersonUuid
let account: PersonUuid
let socialId: PersonId
if (emailSocialId !== null) {
const existingAccount = await db.account.findOne({ uuid: emailSocialId.personUuid })
@@ -475,24 +478,25 @@ export async function signUpByEmail (
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyExists, {}))
}
personUuid = emailSocialId.personUuid
account = emailSocialId.personUuid
socialId = emailSocialId._id
// Person exists, but may have different name, need to update with what's been provided
await db.person.updateOne({ uuid: personUuid }, { firstName, lastName })
await db.person.updateOne({ uuid: account }, { firstName, lastName })
} else {
// There's no person we can link to this email, so we need to create a new one
personUuid = await db.person.insertOne({ firstName, lastName })
await db.socialId.insertOne({
account = await db.person.insertOne({ firstName, lastName })
socialId = await db.socialId.insertOne({
type: SocialIdType.EMAIL,
value: normalizedEmail,
personUuid,
personUuid: account,
...(confirmed ? { verifiedOn: Date.now() } : {})
})
}
await createAccount(db, personUuid, confirmed)
await setPassword(ctx, db, branding, personUuid, password)
await createAccount(db, account, confirmed)
await setPassword(ctx, db, branding, account, password)
return personUuid
return { account, socialId }
}
export async function selectWorkspace (
@@ -803,7 +807,12 @@ export async function sendEmailConfirmation (
}
}
export async function confirmEmail (ctx: MeasureContext, db: AccountDB, account: string, email: string): Promise<void> {
export async function confirmEmail (
ctx: MeasureContext,
db: AccountDB,
account: string,
email: string
): Promise<PersonId> {
const normalizedEmail = cleanEmail(email)
ctx.info('Confirming email', { account, email, normalizedEmail })
@@ -829,6 +838,7 @@ export async function confirmEmail (ctx: MeasureContext, db: AccountDB, account:
}
await db.socialId.updateOne({ key: emailSocialId.key }, { verifiedOn: Date.now() })
return emailSocialId._id
}
export async function useInvite (db: AccountDB, inviteId: string): Promise<void> {
@@ -891,7 +901,7 @@ export async function getWorkspaceInvite (db: AccountDB, id: string): Promise<Wo
return await db.invite.findOne({ migratedFrom: id })
}
export async function getSocialIdByKey (db: AccountDB, socialKey: PersonId): Promise<SocialId | null> {
export async function getSocialIdByKey (db: AccountDB, socialKey: string): Promise<SocialId | null> {
return await db.socialId.findOne({ key: socialKey })
}
@@ -1026,11 +1036,13 @@ export async function loginOrSignUpWithProvider (
await db.resetPassword(personUuid)
}
let socialIdId: PersonId | undefined
// Create and/or confirm missing social ids
if (targetSocialId == null) {
await db.socialId.insertOne({ ...socialId, personUuid, verifiedOn: Date.now() })
socialIdId = await db.socialId.insertOne({ ...socialId, personUuid, verifiedOn: Date.now() })
} else if (targetSocialId.verifiedOn == null) {
await db.socialId.updateOne({ key: targetSocialId.key }, { verifiedOn: Date.now() })
socialIdId = targetSocialId._id
}
if (emailSocialId == null) {
@@ -1048,7 +1060,7 @@ export async function loginOrSignUpWithProvider (
return {
account: personUuid,
socialId: buildSocialIdString(socialId),
socialId: socialIdId,
name: getPersonName(person),
token: generateToken(personUuid)
}
@@ -1153,10 +1165,29 @@ export async function getWorkspaces (
}))
}
export function verifyAllowedServices (services: string[], extra: any): void {
if (!services.includes(extra?.service) && extra?.admin !== 'true') {
export function verifyAllowedServices (services: string[], extra: any, shouldThrow = true): boolean {
const ok = services.includes(extra?.service) || extra?.admin === 'true'
if (!ok && shouldThrow) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
return ok
}
export function verifyAllowedRole (
targetRole: AccountRole | null,
minRole: AccountRole,
extra: any,
shouldThrow = true
): boolean {
const ok = extra?.admin === 'true' || (targetRole != null && getRolePower(targetRole) >= getRolePower(minRole))
if (!ok && shouldThrow) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
return ok
}
export function getPersonName (person: Person): string {
+1 -1
View File
@@ -41,7 +41,7 @@ async function getTxOperations (client: Client, token: Token, isDerived: boolean
const rawToken = generateToken(token.account, token.workspace, { service: 'collaborator' })
const accountClient = getAccountClient(config.AccountsUrl, rawToken)
const socialIds = await accountClient.getSocialIds()
primarySocialString = pickPrimarySocialId(socialIds).key
primarySocialString = pickPrimarySocialId(socialIds)._id
}
return new TxOperations(client, primarySocialString, isDerived)
+2 -3
View File
@@ -36,7 +36,6 @@ import core, {
type WorkspaceInfoWithStatus,
Account,
pickPrimarySocialId,
buildSocialIdString,
type PersonId,
type WorkspaceDataId,
Data,
@@ -347,8 +346,8 @@ export class TSessionManager implements SessionManager {
return {
uuid: loginInfo.account,
role: loginInfo.role,
primarySocialId: buildSocialIdString(pickPrimarySocialId(socialIds)),
socialIds: socialIds.map((si) => si.key)
primarySocialId: pickPrimarySocialId(socialIds)._id,
socialIds: socialIds.map((si) => si._id)
}
} catch (err: any) {
if (err?.cause?.code === 'ECONNRESET' || err?.cause?.code === 'ECONNREFUSED') {
+5 -3
View File
@@ -54,7 +54,7 @@ import { DbStorage } from './storage'
import { tryAssignToWorkspace } from './utils/account'
import { summarizeMessages, translateHtml } from './utils/openai'
import { WorkspaceClient } from './workspace/workspaceClient'
import contact, { Contact, getName } from '@hcengineering/contact'
import contact, { Contact, getName, SocialIdentityRef } from '@hcengineering/contact'
const CLOSE_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
@@ -255,13 +255,15 @@ export class AIControl {
for (const m of messages) {
if (m.createdBy !== undefined) personIds.add(m.createdBy)
}
const identities = await client.findAll(contact.class.SocialIdentity, { key: { $in: Array.from(personIds) } })
const identities = await client.findAll(contact.class.SocialIdentity, {
_id: { $in: Array.from(personIds) as SocialIdentityRef[] }
})
const contacts = await client.findAll(contact.class.Contact, { _id: { $in: identities.map((i) => i.attachedTo) } })
const contactById = toIdMap(contacts)
const contactByPersonId = new Map<PersonId, Contact>()
for (const identity of identities) {
const contact = contactById.get(identity.attachedTo)
if (contact !== undefined) contactByPersonId.set(identity.key, contact)
if (contact !== undefined) contactByPersonId.set(identity._id, contact)
}
const messagesToSummarize: PersonMessage[] = []
@@ -23,7 +23,7 @@ import {
} from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token'
import { getAccountClient, withRetry } from '@hcengineering/server-client'
import { aiBotAccountEmail, aiBotEmailSocialId } from '@hcengineering/ai-bot'
import { aiBotAccountEmail, aiBotEmailSocialKey } from '@hcengineering/ai-bot'
import { MeasureContext, PersonUuid, systemAccountUuid } from '@hcengineering/core'
import config from '../config'
@@ -130,21 +130,27 @@ async function confirmAccount (uuid: PersonUuid): Promise<void> {
}
}
let account: AccountUuid | undefined
export async function getAccountUuid (ctx?: MeasureContext): Promise<AccountUuid | undefined> {
if (account !== undefined) return account
const token = generateToken(systemAccountUuid, undefined, { service: 'aibot', confirmEmail: aiBotAccountEmail })
const accountClient = getAccountClient(token)
const personUuid = await accountClient.findPerson(aiBotEmailSocialId)
const personUuid = await accountClient.findPersonBySocialKey(aiBotEmailSocialKey)
if (personUuid !== undefined) {
await confirmAccount(personUuid)
return personUuid as AccountUuid
account = personUuid as AccountUuid
return account
}
const result = await accountClient.signUp(aiBotEmailSocialId, config.Password, config.FirstName, config.LastName)
const result = await accountClient.signUp(aiBotAccountEmail, config.Password, config.FirstName, config.LastName)
if (result !== undefined) {
await confirmAccount(result.account)
return result.account
account = result.account
return account
}
return undefined
@@ -12,21 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import core, { Client, Ref, TxOperations, AccountUuid, PersonId } from '@hcengineering/core'
import core, { Client, Ref, TxOperations, AccountUuid } from '@hcengineering/core'
import { createClient } from '@hcengineering/server-client'
import contact, { Employee, Person } from '@hcengineering/contact'
import chunter, { DirectMessage } from '@hcengineering/chunter'
import { aiBotEmailSocialId } from '@hcengineering/ai-bot'
import { aiBotEmailSocialKey } from '@hcengineering/ai-bot'
import notification from '@hcengineering/notification'
export async function connectPlatform (token: string, endpoint: string): Promise<Client> {
return await createClient(endpoint, token)
}
export async function getAccountBySocialId (client: TxOperations, socialId: PersonId): Promise<AccountUuid | null> {
const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: socialId })
export async function getAccountBySocialKey (client: TxOperations, socialKey: string): Promise<AccountUuid | null> {
const socialIdentity = await client.findOne(contact.class.SocialIdentity, { key: socialKey })
if (socialIdentity === undefined) {
if (socialIdentity == null) {
return null
}
@@ -40,7 +40,7 @@ export async function getDirect (
account: AccountUuid,
aiPerson?: Ref<Person>
): Promise<Ref<DirectMessage> | undefined> {
const aibotAccount = await getAccountBySocialId(client, aiBotEmailSocialId)
const aibotAccount = await getAccountBySocialKey(client, aiBotEmailSocialKey)
if (aibotAccount == null) return undefined
const existingDm = (await client.findAll(chunter.class.DirectMessage, { members: aibotAccount })).find((dm) =>
@@ -14,7 +14,7 @@
//
import { ConnectMeetingRequest } from '@hcengineering/ai-bot'
import chunter from '@hcengineering/chunter'
import contact, { Person, pickPrimarySocialId } from '@hcengineering/contact'
import contact, { Person } from '@hcengineering/contact'
import core, {
concatLink,
Doc,
@@ -27,7 +27,8 @@ import core, {
TxOperations,
TxProcessor,
TxUpdateDoc,
WorkspaceUuid
WorkspaceUuid,
pickPrimarySocialId
} from '@hcengineering/core'
import love, {
getFreeRoomPlace,
@@ -180,7 +181,7 @@ export class LoveController {
attachedToClass: contact.class.Person
})
if (identities.length > 0) {
const id = pickPrimarySocialId(identities.map((si) => si.key))
const id = pickPrimarySocialId(identities)._id
this.socialIdByPerson.set(person, id)
}
}
@@ -13,7 +13,6 @@
// limitations under the License.
//
import {
aiBotEmailSocialId,
AIEventRequest,
ConnectMeetingRequest,
DisconnectMeetingRequest,
@@ -48,7 +47,8 @@ import core, {
TxOperations,
type WorkspaceUuid,
type WorkspaceIds,
AccountUuid
AccountUuid,
pickPrimarySocialId
} from '@hcengineering/core'
import { Room } from '@hcengineering/love'
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
@@ -76,6 +76,7 @@ export class WorkspaceClient {
rate = new RateLimiter(1)
primarySocialId: SocialId
aiPerson: Person | undefined
personUuidBySocialId = new Map<PersonId, PersonUuid>()
@@ -102,21 +103,22 @@ export class WorkspaceClient {
void this.opClient.then((opClient) => {
this.opClient = opClient
})
this.primarySocialId = pickPrimarySocialId(this.socialIds)
}
private async ensureEmployee (client: Client): Promise<void> {
const me: Account = {
uuid: this.personUuid,
role: AccountRole.User,
primarySocialId: aiBotEmailSocialId,
socialIds: this.socialIds.map((it) => it.key)
primarySocialId: this.primarySocialId._id,
socialIds: this.socialIds.map((it) => it._id)
}
await ensureEmployee(this.ctx, me, client, this.socialIds, async () => await getGlobalPerson(this.token))
}
private async initClient (): Promise<TxOperations> {
this.client = await connectPlatform(this.token, this.transactorUrl)
const opClient = new TxOperations(this.client, aiBotEmailSocialId)
const opClient = new TxOperations(this.client, this.primarySocialId._id)
await this.ensureEmployee(this.client)
await this.checkEmployeeInfo(opClient)
@@ -305,7 +307,7 @@ export class WorkspaceClient {
const { user, objectId, objectClass, messageClass } = event
const client = await this.opClient
const accountClient = getAccountClient(this.token)
const personUuid = this.personUuidBySocialId.get(user) ?? (await accountClient.findPerson(user))
const personUuid = this.personUuidBySocialId.get(user) ?? (await accountClient.findPersonBySocialId(user))
if (personUuid === undefined) {
return
@@ -24,7 +24,7 @@ import recruit, { recruitId } from '@hcengineering/recruit'
import time, { timeId } from '@hcengineering/time'
import tracker, { trackerId } from '@hcengineering/tracker'
import workbench, { WorkbenchEvents } from '@hcengineering/workbench'
import { Class, Doc, Hierarchy, Markup, PersonId, Ref, TxOperations } from '@hcengineering/core'
import { AccountUuid, Class, Doc, Hierarchy, Markup, Ref, TxOperations } from '@hcengineering/core'
import { MarkupNode, MarkupNodeType, MarkupMark, MarkupMarkType } from '@hcengineering/text'
import { translate } from '@hcengineering/platform'
@@ -454,16 +454,16 @@ function parseHash (hash: string): string {
return decodeURIComponent(hash)
}
export function getOnboardingMessage (personId: PersonId, workspace: string, name: string): Markup {
export function getOnboardingMessage (account: AccountUuid, workspace: string, name: string): Markup {
const nodes: MarkupNode[] = [
toText('New user for onboarding: '),
toText('name', 'bold'),
toText(' - '),
toText(name),
toText(', '),
toText('social id', 'bold'),
toText('account', 'bold'),
toText(' - '),
toText(personId),
toText(account),
toText(', '),
toText('workspace', 'bold'),
toText(' - '),
@@ -17,6 +17,7 @@ import analyticsCollector, { AnalyticEvent, OnboardingChannel } from '@hcenginee
import chunter, { Channel, ChatMessage } from '@hcengineering/chunter'
import { getPrimarySocialId, type Person } from '@hcengineering/contact'
import core, {
AccountUuid,
Doc,
generateId,
PersonId,
@@ -92,14 +93,14 @@ export class SupportWsClient extends WorkspaceClient {
private async getOrCreateOnboardingChannel (
client: TxOperations,
workspace: WorkspaceUuid,
personId: PersonId,
account: AccountUuid,
person: Person
): Promise<{
channelId: Ref<OnboardingChannel> | undefined
isCreated: boolean
workspace?: WorkspaceInfoWithStatus
}> {
const key = `${personId}-${workspace}`
const key = `${account}-${workspace}`
if (this.channelIdByKey.has(key)) {
return {
@@ -122,7 +123,7 @@ export class SupportWsClient extends WorkspaceClient {
const [channel, isCreated] = await getOrCreateOnboardingChannel(
this.ctx,
client,
personId,
account,
{
workspaceId: workspace,
workspaceName: wsInfo?.name ?? '',
@@ -167,9 +168,9 @@ export class SupportWsClient extends WorkspaceClient {
const client = await this.opClient
const op = client.apply(undefined, 'processEvents')
const wsString = workspace
const personId = await this.getPersonId(person._id)
const account = person.personUuid as AccountUuid
if (personId === undefined) {
if (account === undefined) {
return
}
@@ -177,7 +178,7 @@ export class SupportWsClient extends WorkspaceClient {
channelId,
isCreated,
workspace: workspaceInfo
} = await this.getOrCreateOnboardingChannel(op, wsString, personId, person)
} = await this.getOrCreateOnboardingChannel(op, wsString, account, person)
if (channelId === undefined) {
return
@@ -191,7 +192,7 @@ export class SupportWsClient extends WorkspaceClient {
analyticsCollector.space.GeneralOnboardingChannel,
chunter.class.Channel,
'messages',
{ message: getOnboardingMessage(personId, workspaceInfo?.url ?? wsString, person.name) },
{ message: getOnboardingMessage(account, workspaceInfo?.url ?? wsString, person.name) },
messageId
)
@@ -198,7 +198,7 @@ export class CalendarClient {
async startSync (): Promise<void> {
try {
await this.syncCalendars()
const calendars = this.workspace.getMyCalendars(this.user.userId)
const calendars = await this.workspace.getMyCalendars(this.user.userId)
for (const calendar of calendars) {
if (calendar.externalId !== undefined) {
await this.sync(calendar.externalId)
@@ -439,7 +439,7 @@ export class CalendarClient {
private async syncEvent (calendarId: string, event: calendar_v3.Schema$Event, accessRole: string): Promise<void> {
this.updateTimer()
if (event.id != null) {
const calendars = this.workspace.getMyCalendars(this.user.userId)
const calendars = await this.workspace.getMyCalendars(this.user.userId)
const _calendar =
calendars.find((p) => p.externalId === event.organizer?.email) ??
calendars.find((p) => p.externalId === calendarId) ??
@@ -881,7 +881,7 @@ export class CalendarClient {
const events = await this.client.findAll(calendar.class.Event, {
access: 'owner',
createdBy: this.user.userId,
calendar: { $in: this.workspace.getMyCalendars(this.user.userId).map((p) => p._id) }
calendar: { $in: (await this.workspace.getMyCalendars(this.user.userId)).map((p) => p._id) }
})
for (const event of events) {
await this.syncMyEvent(event)
@@ -21,12 +21,12 @@ import contact, {
getPrimarySocialId,
getPersonRefBySocialId,
getPersonRefsBySocialIds,
type Employee
type Employee,
SocialIdentityRef
} from '@hcengineering/contact'
import core, {
PersonId,
SocialIdType,
buildSocialIdString,
TxMixin,
RateLimiter,
TxOperations,
@@ -64,7 +64,7 @@ export class WorkspaceClient {
private readonly syncHistory: Collection<SyncHistory>
private readonly tokens: Collection<Token>
private channels = new Map<Ref<Channel>, Channel>()
private readonly calendarsByGoogleId = new Map<PersonId, ExternalCalendar[]>()
private readonly externalIdByPersonId = new Map<PersonId, string | null>()
readonly calendars = {
byId: new Map<Ref<ExternalCalendar>, ExternalCalendar>(),
byExternal: new Map<string, ExternalCalendar[]>()
@@ -433,12 +433,7 @@ export class WorkspaceClient {
const calendars = await this.client.findAll(calendar.class.ExternalCalendar, {})
this.calendars.byId = toIdMap(calendars)
this.calendars.byExternal.clear()
this.calendarsByGoogleId.clear()
for (const calendar of calendars) {
const googleId = buildSocialIdString({ type: SocialIdType.GOOGLE, value: calendar.externalUser })
const arr = this.calendarsByGoogleId.get(googleId) ?? []
arr.push(calendar)
this.calendarsByGoogleId.set(googleId, arr)
const arrByExt = this.calendars.byExternal.get(calendar.externalId) ?? []
arrByExt.push(calendar)
this.calendars.byExternal.set(calendar.externalId, arrByExt)
@@ -450,8 +445,25 @@ export class WorkspaceClient {
})
}
getMyCalendars (personId: PersonId): ExternalCalendar[] {
return this.calendarsByGoogleId.get(personId) ?? []
async getExtIdByPersonId (personId: PersonId): Promise<string | null | undefined> {
if (!this.externalIdByPersonId.has(personId)) {
const socialIdentity = await this.client.findOne(contact.class.SocialIdentity, {
_id: personId as SocialIdentityRef,
type: SocialIdType.GOOGLE
})
this.externalIdByPersonId.set(personId, socialIdentity?.value ?? null)
}
return this.externalIdByPersonId.get(personId)
}
async getMyCalendars (personId: PersonId): Promise<ExternalCalendar[]> {
const extId = await this.getExtIdByPersonId(personId)
if (extId == null) {
return []
}
return this.calendars.byExternal.get(extId) ?? []
}
private async txCalendarHandler (actualTx: Tx): Promise<void> {
@@ -462,27 +474,16 @@ export class WorkspaceClient {
const arr = this.calendars.byExternal.get(calendar.externalId) ?? []
arr.push(calendar)
this.calendars.byExternal.set(calendar.externalId, arr)
const googleId = buildSocialIdString({ type: SocialIdType.GOOGLE, value: calendar.externalUser })
const arrByExt = this.calendarsByGoogleId.get(googleId) ?? []
arrByExt.push(calendar)
this.calendarsByGoogleId.set(googleId, arrByExt)
}
}
if (actualTx._class === core.class.TxRemoveDoc) {
const remTx = actualTx as TxRemoveDoc<ExternalCalendar>
const calendar = this.calendars.byId.get(remTx.objectId)
if (calendar !== undefined) {
const googleId = buildSocialIdString({ type: SocialIdType.GOOGLE, value: calendar.externalUser })
const arr = this.calendarsByGoogleId.get(googleId) ?? []
const index = arr.findIndex((p) => p._id === calendar._id)
if (index !== -1) {
arr.splice(index, 1)
this.calendarsByGoogleId.set(googleId, arr)
}
this.calendars.byId.delete(remTx.objectId)
const arrByExt = this.calendars.byExternal.get(calendar.externalId) ?? []
const indexByExt = arrByExt.findIndex((p) => p._id === calendar._id)
if (index !== -1) {
if (indexByExt !== -1) {
arrByExt.splice(indexByExt, 1)
this.calendars.byExternal.set(calendar.externalId, arrByExt)
}
+8 -8
View File
@@ -18,14 +18,13 @@ import client, { ClientSocket } from '@hcengineering/client'
import core, {
AccountUuid,
Blob,
buildSocialIdString,
Class,
Client,
Doc,
generateId,
MeasureContext,
type PersonId,
Ref,
SocialIdType,
Space,
TxOperations,
WorkspaceIds
@@ -118,6 +117,7 @@ type AsyncRequestHandler = (
res: Response,
wsIds: WorkspaceIds,
token: string,
socialId: PersonId,
next: NextFunction
) => Promise<void>
@@ -133,12 +133,15 @@ const handleRequest = async (
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
throw new ApiError(401, "Couldn't find workspace with the provided token")
}
if (wsLoginInfo.socialId === undefined) {
throw new ApiError(401, 'Social ID is missing')
}
const wsIds = {
uuid: wsLoginInfo.workspace,
dataId: wsLoginInfo.workspaceDataId,
url: wsLoginInfo.workspaceUrl
}
await fn(req, res, wsIds, token, next)
await fn(req, res, wsIds, token, wsLoginInfo.socialId, next)
} catch (err: unknown) {
next(err)
}
@@ -159,7 +162,7 @@ export function createServer (storageConfig: StorageConfiguration): { app: Expre
app.get(
'/export',
wrapRequest(async (req, res, wsIds, token) => {
wrapRequest(async (req, res, wsIds, token, socialId) => {
const classId = req.query.class as Ref<Class<Doc<Space>>>
const exportType = req.query.type as ExportType
const attributesOnly = req.query.attributesOnly === 'true'
@@ -171,10 +174,7 @@ export function createServer (storageConfig: StorageConfiguration): { app: Expre
const platformClient = await createPlatformClient(token)
const { account, workspace } = decodeToken(token)
const txOperations = new TxOperations(
platformClient,
buildSocialIdString({ type: SocialIdType.EMAIL, value: account })
)
const txOperations = new TxOperations(platformClient, socialId)
res.status(200).send({ message: 'Export started' })

Some files were not shown because too many files have changed in this diff Show More