mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-26 06:12:23 +02:00
895 lines
26 KiB
TypeScript
895 lines
26 KiB
TypeScript
//
|
|
// Copyright © 2023 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import {
|
|
Account,
|
|
AccountRole,
|
|
AccountUuid,
|
|
AttachedData,
|
|
buildSocialIdString,
|
|
Class,
|
|
Client,
|
|
Doc,
|
|
FindResult,
|
|
generateId,
|
|
Hierarchy,
|
|
MeasureContext,
|
|
notEmpty,
|
|
Person as GlobalPerson,
|
|
PersonId,
|
|
pickPrimarySocialId,
|
|
Ref,
|
|
SocialId,
|
|
toIdMap,
|
|
TxFactory,
|
|
DocumentUpdate
|
|
} from '@hcengineering/core'
|
|
import platform, { getMetadata, PlatformError } from '@hcengineering/platform'
|
|
import { ColorDefinition } from '@hcengineering/ui'
|
|
import contact, {
|
|
AvatarProvider,
|
|
AvatarType,
|
|
Channel,
|
|
Contact,
|
|
Employee,
|
|
Person,
|
|
PersonSpace,
|
|
SocialIdentity,
|
|
SocialIdentityRef
|
|
} from '.'
|
|
|
|
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
|
|
import ContactCache from './cache'
|
|
|
|
let currentEmployee: Ref<Employee>
|
|
let currentEmployeeSpace: Ref<PersonSpace>
|
|
|
|
const employeeListeners: ((ref: Ref<Employee>) => void)[] = []
|
|
/**
|
|
* @public
|
|
* @returns
|
|
*/
|
|
export function getCurrentEmployee (): Ref<Employee> {
|
|
return currentEmployee
|
|
}
|
|
|
|
export function getCurrentEmployeeSpace (): Ref<PersonSpace> {
|
|
return currentEmployeeSpace
|
|
}
|
|
|
|
export function addEmployeeListenrer (l: (ref: Ref<Employee>) => void): void {
|
|
employeeListeners.push(l)
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
* @param employee -
|
|
*/
|
|
export function setCurrentEmployee (employee: Ref<Employee>): void {
|
|
currentEmployee = employee
|
|
for (const l of employeeListeners) {
|
|
l(employee)
|
|
}
|
|
}
|
|
|
|
export function setCurrentEmployeeSpace (space: Ref<PersonSpace>): void {
|
|
currentEmployeeSpace = space
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAvatarColorForId (id: string | null | undefined): string {
|
|
if (id == null) return AVATAR_COLORS[0].color
|
|
let hash = 0
|
|
|
|
for (let i = 0; i < id.length; i++) {
|
|
hash += id.charCodeAt(i)
|
|
}
|
|
|
|
return AVATAR_COLORS[hash % AVATAR_COLORS.length].color
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAvatarColors (): readonly ColorDefinition[] {
|
|
return AVATAR_COLORS
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAvatarColorName (color: string): string {
|
|
return AVATAR_COLORS.find((col) => col.color === color)?.name ?? AVATAR_COLORS[0].name
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAvatarProviderId (kind: AvatarType): Ref<AvatarProvider> | undefined {
|
|
switch (kind) {
|
|
case AvatarType.GRAVATAR:
|
|
return contact.avatarProvider.Gravatar
|
|
case AvatarType.COLOR:
|
|
return contact.avatarProvider.Color
|
|
}
|
|
return contact.avatarProvider.Image
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getGravatarUrl (
|
|
gravatarId: string,
|
|
width: number = 64,
|
|
placeholder: GravatarPlaceholderType = 'identicon'
|
|
): string {
|
|
return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> {
|
|
try {
|
|
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 2048, '404'))).ok
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function findContacts (
|
|
client: Client,
|
|
_class: Ref<Class<Doc>>,
|
|
name: string,
|
|
channels: AttachedData<Channel>[]
|
|
): Promise<{ contacts: Contact[], channels: AttachedData<Channel>[] }> {
|
|
if (channels.length === 0 && name.length === 0) {
|
|
return { contacts: [], channels: [] }
|
|
}
|
|
// Take only first part of first name for match.
|
|
const values = channels.map((it) => it.value)
|
|
|
|
// Same name persons
|
|
|
|
const potentialChannels = await client.findAll(contact.class.Channel, { value: { $in: values } }, { limit: 1000 })
|
|
let potentialContactIds = Array.from(new Set(potentialChannels.map((it) => it.attachedTo as Ref<Contact>)).values())
|
|
|
|
if (potentialContactIds.length === 0) {
|
|
if (client.getHierarchy().isDerived(_class, contact.class.Person)) {
|
|
const firstName = getFirstName(name).split(' ').shift() ?? ''
|
|
const lastName = getLastName(name)
|
|
// try match using just first/last name
|
|
potentialContactIds = (
|
|
await client.findAll(contact.class.Contact, { name: { $like: `${lastName}%${firstName}%` } }, { limit: 100 })
|
|
).map((it) => it._id)
|
|
if (potentialContactIds.length === 0) {
|
|
return { contacts: [], channels: [] }
|
|
}
|
|
} else if (client.getHierarchy().isDerived(_class, contact.class.Organization)) {
|
|
// try match using just first/last name
|
|
potentialContactIds = (
|
|
await client.findAll(contact.class.Contact, { name: { $like: `${name}` } }, { limit: 100 })
|
|
).map((it) => it._id)
|
|
if (potentialContactIds.length === 0) {
|
|
return { contacts: [], channels: [] }
|
|
}
|
|
}
|
|
}
|
|
|
|
const potentialPersons: FindResult<Contact> = await client.findAll(
|
|
contact.class.Contact,
|
|
{ _id: { $in: potentialContactIds } },
|
|
{
|
|
lookup: {
|
|
_id: {
|
|
channels: contact.class.Channel
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
const result: Contact[] = []
|
|
const resChannels: AttachedData<Channel>[] = []
|
|
for (const c of potentialPersons) {
|
|
let matches = 0
|
|
if (c.name === name) {
|
|
matches++
|
|
}
|
|
for (const ch of (c.$lookup?.channels as Channel[]) ?? []) {
|
|
for (const chc of channels) {
|
|
if (chc.provider === ch.provider && chc.value === ch.value.trim()) {
|
|
// We have matched value
|
|
resChannels.push(chc)
|
|
matches += 2
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matches > 0) {
|
|
result.push(c)
|
|
}
|
|
}
|
|
return { contacts: result, channels: resChannels }
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function findPerson (client: Client, name: string, channels: AttachedData<Channel>[]): Promise<Person[]> {
|
|
const result = await findContacts(client, contact.class.Person, name, channels)
|
|
return result.contacts as Person[]
|
|
}
|
|
|
|
const SEP = ','
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function combineName (first: string, last: string): string {
|
|
return (last ?? '') + SEP + (first ?? '')
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getFirstName (name: string): string {
|
|
return name !== undefined ? name.substring(name.indexOf(SEP) + 1) : ''
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getLastName (name: string): string {
|
|
return name !== undefined ? name.substring(0, name.indexOf(SEP)) : ''
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function formatName (name: string, lastNameFirst?: string): string {
|
|
const lastNameFirstCombined =
|
|
lastNameFirst !== undefined ? lastNameFirst === 'true' : getMetadata(contact.metadata.LastNameFirst) === true
|
|
return lastNameFirstCombined
|
|
? getLastName(name) + ' ' + getFirstName(name)
|
|
: getFirstName(name) + ' ' + getLastName(name)
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getName (hierarchy: Hierarchy, value: Contact, lastNameFirst?: string): string {
|
|
if (isPerson(hierarchy, value)) {
|
|
return formatName(value.name, lastNameFirst)
|
|
}
|
|
return value.name
|
|
}
|
|
|
|
function isPerson (hierarchy: Hierarchy, value: Contact): value is Person {
|
|
return isPersonClass(hierarchy, value._class)
|
|
}
|
|
|
|
function isPersonClass (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): boolean {
|
|
return hierarchy.isDerived(_class, contact.class.Person)
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function formatContactName (
|
|
hierarchy: Hierarchy,
|
|
_class: Ref<Class<Doc>>,
|
|
name: string,
|
|
lastNameFirst?: string
|
|
): string {
|
|
if (isPersonClass(hierarchy, _class)) {
|
|
return formatName(name, lastNameFirst)
|
|
}
|
|
return name
|
|
}
|
|
|
|
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 })
|
|
if (socialId === undefined) return undefined
|
|
return await client.findOne(contact.class.Person, { _id: socialId?.attachedTo, _class: socialId?.attachedToClass })
|
|
}
|
|
|
|
export async function getEmployeeBySocialId (client: Client, socialIdString: PersonId): Promise<Employee | undefined> {
|
|
const socialId = await client.findOne(contact.class.SocialIdentity, { _id: socialIdString as SocialIdentityRef })
|
|
|
|
if (socialId === undefined) return undefined
|
|
|
|
return await client.findOne(contact.mixin.Employee, { _id: socialId.attachedTo as Ref<Employee> })
|
|
}
|
|
|
|
export async function getPersonRefsBySocialIds (
|
|
client: Client,
|
|
ids: PersonId[] = []
|
|
): Promise<Record<PersonId, Ref<Person>>> {
|
|
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._id] = socialId.attachedTo
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export async function getPrimarySocialId (client: Client, person: Ref<Person>): Promise<PersonId | undefined> {
|
|
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person, verifiedOn: { $gt: 0 } })
|
|
|
|
if (socialIds.length === 0) {
|
|
return
|
|
}
|
|
|
|
return pickPrimarySocialId(socialIds)._id
|
|
}
|
|
|
|
export async function getAllSocialStringsByPersonId (client: Client, personId: PersonId): Promise<PersonId[]> {
|
|
const socialId = await client.findOne(contact.class.SocialIdentity, {
|
|
_id: personId as SocialIdentityRef,
|
|
attachedToClass: contact.class.Person
|
|
})
|
|
|
|
if (socialId === undefined) {
|
|
return []
|
|
}
|
|
|
|
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: socialId.attachedTo })
|
|
|
|
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._id)
|
|
}
|
|
|
|
export async function getSocialStringsByEmployee (client: Client): Promise<Record<Ref<Person>, PersonId[]>> {
|
|
const employees = await client.findAll(contact.mixin.Employee, { active: true })
|
|
const socialIds = await client.findAll(contact.class.SocialIdentity, {
|
|
attachedTo: { $in: employees.map((it) => it._id) },
|
|
attachedToClass: contact.class.Person
|
|
})
|
|
const socialStringsByPerson: Record<Ref<Employee>, PersonId[]> = {}
|
|
|
|
for (const socialId of socialIds) {
|
|
const socialStrings = socialStringsByPerson[socialId.attachedTo as Ref<Employee>]
|
|
|
|
if (socialStrings === undefined) {
|
|
socialStringsByPerson[socialId.attachedTo as Ref<Employee>] = []
|
|
}
|
|
socialStrings.push(socialId._id)
|
|
}
|
|
|
|
return socialStringsByPerson
|
|
}
|
|
|
|
export async function getAllAccounts (client: Client): Promise<AccountUuid[]> {
|
|
const employees = await client.findAll(contact.mixin.Employee, { active: true })
|
|
|
|
return employees.map((it) => it.personUuid).filter(notEmpty)
|
|
}
|
|
|
|
export async function getAllUserAccounts (client: Client): Promise<AccountUuid[]> {
|
|
const employees = await client.findAll(contact.mixin.Employee, { active: true })
|
|
|
|
return employees.map((it) => it.personUuid).filter(notEmpty)
|
|
}
|
|
|
|
export async function ensureEmployee (
|
|
ctx: MeasureContext,
|
|
me: Account,
|
|
client: Pick<Client, 'findOne' | 'findAll' | 'tx'>,
|
|
socialIds: SocialId[],
|
|
getGlobalPerson: () => Promise<GlobalPerson | undefined>
|
|
): Promise<Ref<Employee> | null> {
|
|
const globalPerson = await getGlobalPerson()
|
|
return await ensureEmployeeForPerson(ctx, me, me, client, socialIds, globalPerson)
|
|
}
|
|
|
|
export async function ensureEmployeeForPerson (
|
|
ctx: MeasureContext,
|
|
me: Account,
|
|
person: Account,
|
|
client: Pick<Client, 'findOne' | 'findAll' | 'tx'>,
|
|
socialIds: SocialId[],
|
|
globalPerson?: GlobalPerson
|
|
): Promise<Ref<Employee> | null> {
|
|
let personRef: Ref<Person> | undefined
|
|
try {
|
|
const txFactory = new TxFactory(me.primarySocialId)
|
|
const personByUuid = await client.findOne(contact.class.Person, { personUuid: person.uuid })
|
|
personRef = personByUuid?._id
|
|
|
|
if (personRef === undefined) {
|
|
const socialIdentity = await client.findOne(contact.class.SocialIdentity, {
|
|
_id: { $in: person.socialIds as SocialIdentityRef[] }
|
|
})
|
|
|
|
// This social id is confirmed globally as we only have ids of confirmed social identities in socialIds array
|
|
personRef = socialIdentity?.attachedTo
|
|
}
|
|
|
|
if (personRef === undefined) {
|
|
// Local person not found: neither by personUuid nor by a local social identity
|
|
// Creating a new local person
|
|
await ctx.with('create-person', {}, async () => {
|
|
if (globalPerson === undefined) {
|
|
console.error('Cannot get global person')
|
|
return null
|
|
}
|
|
|
|
const data = {
|
|
personUuid: person.uuid,
|
|
name: combineName(globalPerson.firstName, globalPerson.lastName),
|
|
city: '',
|
|
avatarType: AvatarType.COLOR
|
|
}
|
|
personRef = generateId()
|
|
|
|
const createPersonTx = txFactory.createTxCreateDoc(
|
|
contact.class.Person,
|
|
contact.space.Contacts,
|
|
data,
|
|
personRef
|
|
)
|
|
await client.tx(createPersonTx)
|
|
})
|
|
} else if (personByUuid === undefined) {
|
|
// Local person found only by social identity, need to set personUuid
|
|
const updatePersonTx = txFactory.createTxUpdateDoc(contact.class.Person, contact.space.Contacts, personRef, {
|
|
personUuid: person.uuid
|
|
})
|
|
await client.tx(updatePersonTx)
|
|
}
|
|
|
|
const existingIdentifiers = toIdMap(
|
|
await client.findAll(contact.class.SocialIdentity, { _id: { $in: person.socialIds as SocialIdentityRef[] } })
|
|
)
|
|
|
|
for (const socialId of socialIds) {
|
|
const existing = existingIdentifiers.get(socialId._id as SocialIdentityRef)
|
|
|
|
if (existing == null) {
|
|
await ctx.with('create-social-identity', {}, async () => {
|
|
if (personRef === undefined) {
|
|
// something went wrong
|
|
console.error('Person not found')
|
|
return null
|
|
}
|
|
|
|
const createSocialIdTx = txFactory.createTxCollectionCUD(
|
|
contact.class.Person,
|
|
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?
|
|
verifiedOn: socialId.verifiedOn,
|
|
isDeleted: socialId.isDeleted ?? false
|
|
},
|
|
socialId._id as SocialIdentityRef
|
|
)
|
|
)
|
|
await client.tx(createSocialIdTx)
|
|
})
|
|
} else {
|
|
// If not confirmed locally can be attached to a different person (persons merge scenario)
|
|
// Confirmed social identity should not be attached to a different person for now
|
|
// It will change with accounts merge function
|
|
if (existing.verifiedOn != null && existing.attachedTo !== personRef) {
|
|
throw new Error('Confirmed social identity is attached to the wrong person')
|
|
}
|
|
|
|
// Check and update if needed. It can:
|
|
// 1. Become verified (maybe with persons merge) (changes verifiedOn, attachedTo)
|
|
const sidUpdate: DocumentUpdate<SocialIdentity> = {}
|
|
let needUpdate = false
|
|
|
|
// become verified
|
|
if (existing.verifiedOn == null) {
|
|
sidUpdate.verifiedOn = socialId.verifiedOn
|
|
needUpdate = true
|
|
}
|
|
|
|
// merged from another person
|
|
if (existing.attachedTo !== personRef) {
|
|
sidUpdate.attachedTo = personRef
|
|
// Bump collection in Person?
|
|
needUpdate = true
|
|
}
|
|
|
|
// become deleted
|
|
if (existing.isDeleted !== socialId.isDeleted && socialId.isDeleted === true) {
|
|
sidUpdate.value = socialId.value
|
|
sidUpdate.key = socialId.key
|
|
sidUpdate.isDeleted = socialId.isDeleted
|
|
needUpdate = true
|
|
}
|
|
|
|
if (needUpdate) {
|
|
const updateSocialIdentityTx = txFactory.createTxUpdateDoc(
|
|
contact.class.SocialIdentity,
|
|
contact.space.Contacts,
|
|
existing._id,
|
|
sidUpdate
|
|
)
|
|
|
|
await client.tx(updateSocialIdentityTx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: it is important to create Employee after Person and SocialIdentities are ensured so all the triggers applied
|
|
// on Employee creation will be able to properly map things
|
|
const employeeRole =
|
|
person.role === AccountRole.Guest || person.role === AccountRole.ReadOnlyGuest ? 'GUEST' : 'USER'
|
|
const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref<Employee> })
|
|
|
|
if (
|
|
employee === undefined ||
|
|
!Hierarchy.hasMixin(employee, contact.mixin.Employee) ||
|
|
!employee.active ||
|
|
employee.role !== employeeRole
|
|
) {
|
|
await ctx.with('create-employee', {}, async () => {
|
|
if (personRef === undefined) {
|
|
// something went wrong
|
|
console.error('Person not found')
|
|
return null
|
|
}
|
|
|
|
const createEmployeeTx = txFactory.createTxMixin(
|
|
personRef,
|
|
contact.class.Person,
|
|
contact.space.Contacts,
|
|
contact.mixin.Employee,
|
|
{
|
|
active: true,
|
|
role: employeeRole
|
|
}
|
|
)
|
|
await client.tx(createEmployeeTx)
|
|
})
|
|
}
|
|
|
|
// TODO: check for merged persons with this one and do the merge
|
|
return personRef as Ref<Employee>
|
|
} catch (err: any) {
|
|
// If Forbidden error occurred, return personRef if it exists, otherwise throw error
|
|
if (err instanceof PlatformError && err.status.code === platform.status.Forbidden) {
|
|
if (personRef != null) {
|
|
ctx.info('Skip employee update for person without full access rights')
|
|
return personRef as Ref<Employee>
|
|
}
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
export const contactCache = ContactCache.instance
|
|
|
|
export async function loadCachesForPersonId (client: Client, personId: PersonId): Promise<void> {
|
|
if (personId == null || personId === '') return
|
|
const sidObj = await client.findOne(
|
|
contact.class.SocialIdentity,
|
|
{
|
|
_id: personId as SocialIdentityRef
|
|
},
|
|
{
|
|
lookup: {
|
|
attachedTo: contact.class.Person
|
|
}
|
|
}
|
|
)
|
|
|
|
contactCache.fillCachesForPersonId(personId, sidObj)
|
|
}
|
|
|
|
export async function loadCachesForPersonIds (client: Client, personIds: PersonId[]): Promise<void> {
|
|
const sidObjsMap = toIdMap(
|
|
await client.findAll(
|
|
contact.class.SocialIdentity,
|
|
{
|
|
_id: { $in: personIds as SocialIdentityRef[] }
|
|
},
|
|
{
|
|
lookup: {
|
|
attachedTo: contact.class.Person
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
for (const personId of personIds) {
|
|
const sidObj = sidObjsMap.get(personId as SocialIdentityRef)
|
|
|
|
contactCache.fillCachesForPersonId(personId, sidObj)
|
|
}
|
|
}
|
|
|
|
export async function loadCachesForPersonRef (client: Client, personRef: Ref<Person>): Promise<void> {
|
|
const person = await client.findOne(
|
|
contact.class.Person,
|
|
{
|
|
_id: personRef
|
|
},
|
|
{
|
|
lookup: {
|
|
_id: { socialIds: contact.class.SocialIdentity }
|
|
}
|
|
}
|
|
)
|
|
|
|
contactCache.fillCachesForPersonRef(personRef, person)
|
|
}
|
|
|
|
export async function loadCachesForPersonRefs (client: Client, personRefs: Array<Ref<Person>>): Promise<void> {
|
|
const persons = toIdMap(
|
|
await client.findAll(
|
|
contact.class.Person,
|
|
{
|
|
_id: { $in: personRefs }
|
|
},
|
|
{
|
|
lookup: {
|
|
_id: { socialIds: contact.class.SocialIdentity }
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
for (const personRef of personRefs) {
|
|
const person = persons.get(personRef)
|
|
|
|
contactCache.fillCachesForPersonRef(personRef, person)
|
|
}
|
|
}
|
|
|
|
export async function getPersonRefByPersonId (client: Client, personId: PersonId): Promise<Ref<Person> | null> {
|
|
if (!contactCache.personRefByPersonId.has(personId)) {
|
|
await loadCachesForPersonId(client, personId)
|
|
}
|
|
|
|
return contactCache.personRefByPersonId.get(personId) ?? null
|
|
}
|
|
|
|
export function getPersonRefByPersonIdCb (
|
|
client: Client,
|
|
personId: PersonId,
|
|
cb: (person: Ref<Person> | null) => void
|
|
): void {
|
|
let personRef: Ref<Person> | null | undefined = contactCache.personRefByPersonId.get(personId)
|
|
if (personRef !== undefined) {
|
|
cb(personRef)
|
|
} else {
|
|
void loadCachesForPersonId(client, personId).then(() => {
|
|
personRef = contactCache.personRefByPersonId.get(personId)
|
|
cb(personRef ?? null)
|
|
})
|
|
}
|
|
}
|
|
|
|
function getPersonRefsByPersonIdsFromCache (personIds: PersonId[]): Map<PersonId, Ref<Person>> {
|
|
return new Map(
|
|
personIds
|
|
.map((pid) => {
|
|
const ref = contactCache.personRefByPersonId.get(pid)
|
|
return ref != null ? ([pid, ref] as const) : undefined
|
|
})
|
|
.filter(notEmpty)
|
|
)
|
|
}
|
|
|
|
export async function getPersonRefsByPersonIds (
|
|
client: Client,
|
|
personIds: PersonId[]
|
|
): Promise<Map<PersonId, Ref<Person>>> {
|
|
if (personIds.some((personId) => !contactCache.personRefByPersonId.has(personId))) {
|
|
await loadCachesForPersonIds(client, personIds)
|
|
}
|
|
|
|
return getPersonRefsByPersonIdsFromCache(personIds)
|
|
}
|
|
|
|
export function getPersonRefsByPersonIdsCb (
|
|
client: Client,
|
|
personIds: PersonId[],
|
|
cb: (personRefs: Map<PersonId, Ref<Person>>) => void
|
|
): void {
|
|
if (personIds.some((personId) => !contactCache.personRefByPersonId.has(personId))) {
|
|
void loadCachesForPersonIds(client, personIds).then(() => {
|
|
const personRefs = getPersonRefsByPersonIdsFromCache(personIds)
|
|
cb(personRefs)
|
|
})
|
|
} else {
|
|
const personRefs = getPersonRefsByPersonIdsFromCache(personIds)
|
|
cb(personRefs)
|
|
}
|
|
}
|
|
|
|
export async function getPersonByPersonId (client: Client, personId: PersonId): Promise<Readonly<Person> | null> {
|
|
if (!contactCache.personByPersonId.has(personId)) {
|
|
await loadCachesForPersonId(client, personId)
|
|
}
|
|
|
|
return contactCache.personByPersonId.get(personId) ?? null
|
|
}
|
|
|
|
export function getPersonByPersonIdCb (
|
|
client: Client,
|
|
personId: PersonId,
|
|
cb: (person: Readonly<Person> | null) => void
|
|
): void {
|
|
let person: Readonly<Person> | null | undefined = contactCache.personByPersonId.get(personId)
|
|
if (person !== undefined) {
|
|
cb(person)
|
|
} else {
|
|
void loadCachesForPersonId(client, personId).then(() => {
|
|
person = contactCache.personByPersonId.get(personId) ?? null
|
|
cb(person ?? null)
|
|
})
|
|
}
|
|
}
|
|
|
|
function getPersonsByPersonIdsFromCache (personIds: PersonId[]): Map<PersonId, Readonly<Person>> {
|
|
return new Map(
|
|
personIds
|
|
.map((pid) => {
|
|
const person = contactCache.personByPersonId.get(pid)
|
|
return person != null ? ([pid, person] as const) : undefined
|
|
})
|
|
.filter(notEmpty)
|
|
)
|
|
}
|
|
|
|
export async function getPersonsByPersonIds (
|
|
client: Client,
|
|
personIds: PersonId[]
|
|
): Promise<Map<PersonId, Readonly<Person>>> {
|
|
if (personIds.some((personId) => !contactCache.personByPersonId.has(personId))) {
|
|
await loadCachesForPersonIds(client, personIds)
|
|
}
|
|
|
|
return getPersonsByPersonIdsFromCache(personIds)
|
|
}
|
|
|
|
export function getPersonsByPersonIdsCb (
|
|
client: Client,
|
|
personIds: PersonId[],
|
|
cb: (persons: Map<PersonId, Readonly<Person>>) => void
|
|
): void {
|
|
if (personIds.some((personId) => !contactCache.personByPersonId.has(personId))) {
|
|
void loadCachesForPersonIds(client, personIds).then(() => {
|
|
const persons = getPersonsByPersonIdsFromCache(personIds)
|
|
cb(persons)
|
|
})
|
|
} else {
|
|
const persons = getPersonsByPersonIdsFromCache(personIds)
|
|
cb(persons)
|
|
}
|
|
}
|
|
|
|
export async function getPersonByPersonRef (client: Client, personRef: Ref<Person>): Promise<Readonly<Person> | null> {
|
|
if (!contactCache.personByRef.has(personRef)) {
|
|
await loadCachesForPersonRef(client, personRef)
|
|
}
|
|
|
|
return contactCache.personByRef.get(personRef) ?? null
|
|
}
|
|
|
|
export function getPersonByPersonRefCb (
|
|
client: Client,
|
|
personRef: Ref<Person>,
|
|
cb: (person: Readonly<Person> | null) => void
|
|
): void {
|
|
let person: Readonly<Person> | null | undefined = contactCache.personByRef.get(personRef)
|
|
if (person !== undefined) {
|
|
cb(person)
|
|
} else {
|
|
void loadCachesForPersonRef(client, personRef).then(() => {
|
|
person = contactCache.personByRef.get(personRef) ?? null
|
|
cb(person ?? null)
|
|
})
|
|
}
|
|
}
|
|
|
|
function getPersonsByPersonRefsFromCache (personRefs: Array<Ref<Person>>): Map<Ref<Person>, Readonly<Person>> {
|
|
return new Map(
|
|
personRefs
|
|
.map((personRef) => {
|
|
const person = contactCache.personByRef.get(personRef)
|
|
return person != null ? ([personRef, person] as const) : undefined
|
|
})
|
|
.filter(notEmpty)
|
|
)
|
|
}
|
|
|
|
export async function getPersonsByPersonRefs (
|
|
client: Client,
|
|
personRefs: Array<Ref<Person>>
|
|
): Promise<Map<Ref<Person>, Readonly<Person>>> {
|
|
if (personRefs.some((personRef) => !contactCache.personByRef.has(personRef))) {
|
|
await loadCachesForPersonRefs(client, personRefs)
|
|
}
|
|
|
|
return getPersonsByPersonRefsFromCache(personRefs)
|
|
}
|
|
|
|
export function getPersonsByPersonRefsCb (
|
|
client: Client,
|
|
personRefs: Array<Ref<Person>>,
|
|
cb: (persons: Map<Ref<Person>, Readonly<Person>>) => void
|
|
): void {
|
|
if (personRefs.some((personRef) => !contactCache.personByRef.has(personRef))) {
|
|
void loadCachesForPersonRefs(client, personRefs).then(() => {
|
|
const persons = getPersonsByPersonRefsFromCache(personRefs)
|
|
cb(persons)
|
|
})
|
|
} else {
|
|
const persons = getPersonsByPersonRefsFromCache(personRefs)
|
|
cb(persons)
|
|
}
|
|
}
|
|
|
|
export async function getSocialIdByPersonId (client: Client, personId: PersonId): Promise<SocialIdentity | null> {
|
|
if (!contactCache.personRefByPersonId.has(personId)) {
|
|
await loadCachesForPersonId(client, personId)
|
|
}
|
|
|
|
return contactCache.socialIdByPersonId.get(personId) ?? null
|
|
}
|
|
|
|
export function getSocialIdByPersonIdCb (
|
|
client: Client,
|
|
personId: PersonId,
|
|
cb: (socialId: SocialIdentity | null) => void
|
|
): void {
|
|
let socialId: SocialIdentity | null | undefined = contactCache.socialIdByPersonId.get(personId)
|
|
if (socialId !== undefined) {
|
|
cb(socialId)
|
|
} else {
|
|
void loadCachesForPersonId(client, personId).then(() => {
|
|
socialId = contactCache.socialIdByPersonId.get(personId) ?? null
|
|
cb(socialId ?? null)
|
|
})
|
|
}
|
|
}
|
|
|
|
export type { Change as ContactCacheChange } from './cache'
|