From 26f23559c239906830a6d52a469e5070ededbb3c Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Fri, 25 Apr 2025 04:53:58 +0400 Subject: [PATCH] UBERF-10346: Combined ensure person (#8701) --- desktop/src/main/start.ts | 5 +- packages/api-client/src/rest/rest.ts | 27 ++++++ packages/api-client/src/rest/types.ts | 9 ++ pods/server/src/rpc.ts | 93 ++++++++++++++++++- pods/server/src/server_http.ts | 2 +- ws-tests/api-tests/src/__tests__/rest.test.ts | 44 +++++++-- ws-tests/docker-compose.yaml | 1 - ws-tests/prepare.sh | 7 +- 8 files changed, 172 insertions(+), 16 deletions(-) diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts index 13a6fa7edd..f47e3b1cd1 100644 --- a/desktop/src/main/start.ts +++ b/desktop/src/main/start.ts @@ -136,9 +136,10 @@ function hookOpenWindow (window: BrowserWindow): void { } function setupCookieHandler (config: Config): void { + const normalizedAccountsUrl = config.ACCOUNTS_URL.endsWith('/') ? config.ACCOUNTS_URL : config.ACCOUNTS_URL + '/' const urls = [ - config.ACCOUNTS_URL, - config.ACCOUNTS_URL + '*' + normalizedAccountsUrl, + normalizedAccountsUrl + '*' ] session.defaultSession.webRequest.onHeadersReceived({ urls }, handleSetCookie) diff --git a/packages/api-client/src/rest/rest.ts b/packages/api-client/src/rest/rest.ts index 0da9b80f79..25be15d7f5 100644 --- a/packages/api-client/src/rest/rest.ts +++ b/packages/api-client/src/rest/rest.ts @@ -25,10 +25,13 @@ import { Hierarchy, MeasureMetricsContext, ModelDb, + PersonId, + PersonUuid, type Ref, type SearchOptions, type SearchQuery, type SearchResult, + SocialIdType, type Tx, type TxResult, type WithLookup @@ -237,4 +240,28 @@ export class RestClientImpl implements RestClient { } return await extractJson(response) } + + async ensurePerson ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ): Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> { + const requestUrl = concatLink(this.endpoint, `/api/v1/ensure-person/${this.workspace}`) + const response = await fetch(requestUrl, { + method: 'POST', + headers: this.jsonHeaders(), + keepalive: true, + body: JSON.stringify({ + socialType, + socialValue, + firstName, + lastName + }) + }) + if (!response.ok) { + throw new PlatformError(unknownError(response.statusText)) + } + return await extractJson<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }>(response) + } } diff --git a/packages/api-client/src/rest/types.ts b/packages/api-client/src/rest/types.ts index b117d90c60..e8fe2c1114 100644 --- a/packages/api-client/src/rest/types.ts +++ b/packages/api-client/src/rest/types.ts @@ -14,6 +14,9 @@ // import { + PersonId, + PersonUuid, + SocialIdType, type Account, type Class, type Doc, @@ -36,4 +39,10 @@ export interface RestClient extends Storage { ) => Promise | undefined> getModel: () => Promise<{ hierarchy: Hierarchy, model: ModelDb }> + ensurePerson: ( + socialType: SocialIdType, + socialValue: string, + firstName: string, + lastName: string + ) => Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> } diff --git a/pods/server/src/rpc.ts b/pods/server/src/rpc.ts index 83793c32b0..da62defb9d 100644 --- a/pods/server/src/rpc.ts +++ b/pods/server/src/rpc.ts @@ -1,6 +1,11 @@ import core, { + buildSocialIdString, generateId, + pickPrimarySocialId, + TxFactory, TxProcessor, + type AttachedData, + type Data, type Class, type Doc, type MeasureContext, @@ -11,8 +16,16 @@ import core, { } from '@hcengineering/core' import type { ClientSessionCtx, ConnectionSocket, Session, SessionManager } from '@hcengineering/server-core' import { decodeToken } from '@hcengineering/server-token' - import { rpcJSONReplacer, type RateLimitInfo } from '@hcengineering/rpc' +import contact, { + AvatarType, + combineName, + type SocialIdentity, + type Person, + type SocialIdentityRef +} from '@hcengineering/contact' +import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client' + import { createHash } from 'crypto' import { type Express, type Response as ExpressResponse, type Request } from 'express' import type { OutgoingHttpHeaders } from 'http2' @@ -110,13 +123,22 @@ async function sendJson ( res.end(body) } -export function registerRPC (app: Express, sessions: SessionManager, ctx: MeasureContext): void { +export function registerRPC (app: Express, sessions: SessionManager, ctx: MeasureContext, accountsUrl: string): void { const rpcSessions = new Map() + function getAccountClient (token?: string): AccountClient { + return getAccountClientRaw(accountsUrl, token) + } + async function withSession ( req: Request, res: ExpressResponse, - operation: (ctx: ClientSessionCtx, session: Session, rateLimit?: RateLimitInfo) => Promise + operation: ( + ctx: ClientSessionCtx, + session: Session, + rateLimit: RateLimitInfo | undefined, + token: string + ) => Promise ): Promise { try { if (req.params.workspaceId === undefined || req.params.workspaceId === '') { @@ -156,7 +178,7 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur const rpc = transactorRpc const rateLimit = await sessions.handleRPC(ctx, rpc.session, rpc.client, async (ctx, rateLimit) => { - await operation(ctx, rpc.session, rateLimit) + await operation(ctx, rpc.session, rateLimit, token) }) if (rateLimit !== undefined) { const { remaining, limit, reset, retryAfter } = rateLimit @@ -302,6 +324,69 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur }) }) + app.post('/api/v1/ensure-person/:workspaceId', (req, res) => { + void withSession(req, res, async (ctx, session, rateLimit, token) => { + const { socialType, socialValue, firstName, lastName } = (await retrieveJson(req)) ?? {} + const accountClient = getAccountClient(token) + + const { uuid, socialId } = await accountClient.ensurePerson(socialType, socialValue, firstName, lastName) + const primaryPersonId = pickPrimarySocialId(session.getSocialIds()) + const txFactory: TxFactory = new TxFactory(primaryPersonId._id) + + const [person] = await session.findAllRaw(ctx, contact.class.Person, { personUuid: uuid }, { limit: 1 }) + let personRef: Ref = person?._id + + if (personRef === undefined) { + const createPersonTx = txFactory.createTxCreateDoc(contact.class.Person, contact.space.Contacts, { + avatarType: AvatarType.COLOR, + name: combineName(firstName, lastName), + personUuid: uuid + }) + + await session.txRaw(ctx, createPersonTx) + personRef = createPersonTx.objectId + } + + const [socialIdentity] = await session.findAllRaw( + ctx, + contact.class.SocialIdentity, + { + attachedTo: personRef, + type: socialType, + value: socialValue + }, + { limit: 1 } + ) + + if (socialIdentity === undefined) { + const data: AttachedData = { + key: buildSocialIdString({ type: socialType, value: socialValue }), + type: socialType, + value: socialValue + } + + const addSocialIdentityTx = txFactory.createTxCollectionCUD( + contact.class.Person, + personRef, + contact.space.Contacts, + 'socialIds', + txFactory.createTxCreateDoc( + contact.class.SocialIdentity, + contact.space.Contacts, + data as Data, + socialId as SocialIdentityRef + ) + ) + + await session.txRaw(ctx, addSocialIdentityTx) + } + + const result = { uuid, socialId, localPerson: personRef } + + await sendJson(req, res, result, rateLimitToHeaders(rateLimit)) + }) + }) + // To use in non-js (rust) clients that can't link to @hcengineering/core app.get('/api/v1/generate-id/:workspaceId', (req, res) => { void withSession(req, res, async (ctx, session, rateLimit) => { diff --git a/pods/server/src/server_http.ts b/pods/server/src/server_http.ts index 0705223178..8eeb993d6d 100644 --- a/pods/server/src/server_http.ts +++ b/pods/server/src/server_http.ts @@ -396,7 +396,7 @@ export function startHttpServer ( }) ) - registerRPC(app, sessions, ctx) + registerRPC(app, sessions, ctx, accountsUrl) app.put('/api/v1/broadcast', (req, res) => { try { diff --git a/ws-tests/api-tests/src/__tests__/rest.test.ts b/ws-tests/api-tests/src/__tests__/rest.test.ts index e1bdf8c4ab..d109618cdd 100644 --- a/ws-tests/api-tests/src/__tests__/rest.test.ts +++ b/ws-tests/api-tests/src/__tests__/rest.test.ts @@ -22,25 +22,27 @@ import { type WorkspaceToken } from '@hcengineering/api-client' import core, { + buildSocialIdString, generateId, MeasureMetricsContext, pickPrimarySocialId, + SocialIdType, + type Ref, type SocialId, type Space, type TxCreateDoc, type TxOperations } from '@hcengineering/core' - -import { getClient as getAccountClient } from '@hcengineering/account-client' - +import { type AccountClient, getClient as getAccountClient } from '@hcengineering/account-client' import chunter from '@hcengineering/chunter' -import contact, { ensureEmployee } from '@hcengineering/contact' +import contact, { ensureEmployee, type SocialIdentityRef, type Person } from '@hcengineering/contact' describe('rest-api-server', () => { const testCtx = new MeasureMetricsContext('test', {}) const wsName = 'api-tests' let apiWorkspace1: WorkspaceToken let apiWorkspace2: WorkspaceToken + let accountClient: AccountClient beforeAll(async () => { const config = await loadServerConfig('http://huly.local:8083') @@ -65,10 +67,10 @@ describe('rest-api-server', () => { config ) - const account = getAccountClient(config.ACCOUNTS_URL, apiWorkspace1.token) - const person = await account.getPerson() + accountClient = getAccountClient(config.ACCOUNTS_URL, apiWorkspace1.token) + const person = await accountClient.getPerson() - const socialIds: SocialId[] = await account.getSocialIds() + const socialIds: SocialId[] = await accountClient.getSocialIds() // Ensure employee is created @@ -214,7 +216,35 @@ describe('rest-api-server', () => { expect(employee.length).toBeGreaterThanOrEqual(1) expect(employee[0].active).toBe(true) }) + + it('ensure-person', async () => { + const socialType = SocialIdType.TELEGRAM + const socialValue = '123456789' + const first = 'John' + const last = 'Doe' + const conn = connect() + const { uuid, socialId, localPerson } = await conn.ensurePerson(socialType, socialValue, first, last) + const globalPerson = await accountClient.findPersonBySocialKey( + buildSocialIdString({ type: socialType, value: socialValue }) + ) + + expect(globalPerson).toBe(uuid) + + const person = await conn.findOne(contact.class.Person, { _id: localPerson as Ref, personUuid: uuid }) + + expect(person).not.toBeNull() + + const socialIdObj = await conn.findOne(contact.class.SocialIdentity, { + type: socialType, + value: socialValue, + attachedTo: person?._id, + _id: socialId as SocialIdentityRef + }) + + expect(socialIdObj).not.toBeNull() + }) }) + async function checkFindPerformance (conn: RestClient): Promise { let ops = 0 let total = 0 diff --git a/ws-tests/docker-compose.yaml b/ws-tests/docker-compose.yaml index 2d968faad8..067486a57b 100644 --- a/ws-tests/docker-compose.yaml +++ b/ws-tests/docker-compose.yaml @@ -384,7 +384,6 @@ services: - STATS_URL=http://huly.local:4901 - REKONI_URL=http://huly.local:4007 - ACCOUNTS_URL=http://huly.local:3003 - - SERVER_SECRET=secret restart: unless-stopped aiBot: image: hardcoreeng/ai-bot diff --git a/ws-tests/prepare.sh b/ws-tests/prepare.sh index 9e5ee6b99e..54fcc4bc7c 100755 --- a/ws-tests/prepare.sh +++ b/ws-tests/prepare.sh @@ -28,13 +28,18 @@ fi ./wait-elastic.sh 9201 -# Create user record in accounts +echo "Creating user accounts..." ./tool.sh create-account admin -f Super -l Admin -p 1234 ./tool.sh create-account user1 -f John -l Appleseed -p 1234 ./tool.sh create-account user2 -f Kainin -l Dirak -p 1234 +echo "Creating workspace api-tests..." ./tool.sh create-workspace api-tests email:user1 + +echo "Creating workspace api-tests-cr..." ./tool-europe.sh create-workspace api-tests-cr email:user1 --region 'europe' + +echo "Assigning user1 to workspaces..." ./tool.sh assign-workspace user1 api-tests ./tool.sh assign-workspace user1 api-tests-cr