Skip to content

Commit dbdd989

Browse files
UBERF-12970: Migrate integrations data (#9640)
1 parent d0503f0 commit dbdd989

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

dev/tool/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ import { existsSync } from 'fs'
122122
import { mkdir, writeFile } from 'fs/promises'
123123
import { dirname } from 'path'
124124
import { restoreMarkupRefs } from './markup'
125+
import { performIntegrationMigrations } from './integrations'
125126

126127
const colorConstants = {
127128
colorRed: '\u001b[31m',
@@ -2697,6 +2698,16 @@ export function devTool (
26972698
})
26982699
})
26992700

2701+
program
2702+
.command('migrate-integrations')
2703+
.option('--db <db>', 'DB name', 'gmail-service')
2704+
.option('--region <region>', 'DB region')
2705+
.action(async (cmd: { db: string, region?: string }) => {
2706+
const { dbUrl, txes } = prepareTools()
2707+
2708+
await performIntegrationMigrations(dbUrl, cmd.region ?? null, txes)
2709+
})
2710+
27002711
extendProgram?.(program)
27012712

27022713
process.on('unhandledRejection', (reason, promise) => {

dev/tool/src/integrations.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
type Client,
3+
type IntegrationKind,
4+
MeasureMetricsContext,
5+
type Ref,
6+
type Tx,
7+
type WorkspaceInfoWithStatus,
8+
type WorkspaceUuid,
9+
systemAccountUuid
10+
} from '@hcengineering/core'
11+
import { getAccountClient } from '@hcengineering/server-client'
12+
import { generateToken } from '@hcengineering/server-token'
13+
import { type Integration } from '@hcengineering/account-client'
14+
import setting, { type IntegrationType, type Integration as IntegrationSetting } from '@hcengineering/setting'
15+
16+
import { type PipelineFactory, createDummyStorageAdapter, wrapPipeline } from '@hcengineering/server-core'
17+
import { createBackupPipeline, createEmptyBroadcastOps } from '@hcengineering/server-pipeline'
18+
19+
interface WorkspaceInfoProvider {
20+
getWorkspaceInfo: (workspaceUuid: WorkspaceUuid) => Promise<WorkspaceInfoWithStatus | undefined>
21+
}
22+
23+
const GMAIL_INTEGRATION: IntegrationKind = 'gmail' as any
24+
const CALENDAR_INTEGRATION: IntegrationKind = 'google-calendar' as any
25+
const GITHUB_INTEGRATION: IntegrationKind = 'github' as any
26+
27+
export async function performIntegrationMigrations (dbUrl: string, region: string | null, txes: Tx[]): Promise<void> {
28+
console.log('Start integration migrations')
29+
const token = generateToken(systemAccountUuid, undefined, { service: 'admin', admin: 'true' })
30+
const accountClient = getAccountClient(token)
31+
32+
const allWorkspaces = await accountClient.listWorkspaces(region)
33+
const byId = new Map(allWorkspaces.map((it) => [it.uuid, it]))
34+
const workspaceProvider: WorkspaceInfoProvider = {
35+
getWorkspaceInfo: async (workspaceUuid: WorkspaceUuid) => {
36+
const ws = byId.get(workspaceUuid as any)
37+
if (ws == null) {
38+
console.error('No workspace found for token', workspaceUuid)
39+
return undefined
40+
}
41+
return ws
42+
}
43+
}
44+
const metricsContext = new MeasureMetricsContext('integrations-migrate', {})
45+
46+
const factory: PipelineFactory = createBackupPipeline(metricsContext, dbUrl, txes, {
47+
externalStorage: createDummyStorageAdapter(),
48+
usePassedCtx: true
49+
})
50+
51+
await migrateIntegrations(token, workspaceProvider, factory, metricsContext)
52+
53+
console.log('Finished integration migrations')
54+
}
55+
56+
async function migrateIntegrations (
57+
token: string,
58+
workspaceProvider: WorkspaceInfoProvider,
59+
factory: PipelineFactory,
60+
metricsContext: MeasureMetricsContext
61+
): Promise<void> {
62+
try {
63+
console.log('Start integrations data migrations')
64+
const accountClient = getAccountClient(token)
65+
const integrations = await accountClient.listIntegrations({})
66+
console.log('Integrations count', integrations.length)
67+
68+
const workspaceUuids = new Set<WorkspaceUuid>()
69+
const integrationsByWorkspaces = integrations.reduce<Record<string, typeof integrations>>((acc, integration) => {
70+
const workspaceId = integration.workspaceUuid
71+
if (workspaceId == null) {
72+
return acc
73+
}
74+
if (acc[workspaceId] === undefined) {
75+
workspaceUuids.add(workspaceId)
76+
acc[workspaceId] = []
77+
}
78+
acc[workspaceId].push(integration)
79+
return acc
80+
}, {})
81+
82+
for (const workspace of workspaceUuids) {
83+
const wsInfo = await workspaceProvider.getWorkspaceInfo(workspace)
84+
if (wsInfo == null) {
85+
console.error('No workspace found for token', workspace)
86+
continue
87+
}
88+
const pipeline = await factory(metricsContext, wsInfo, createEmptyBroadcastOps(), null)
89+
const client = wrapPipeline(metricsContext, pipeline, wsInfo, false)
90+
const integrations = integrationsByWorkspaces[workspace]
91+
for (const integration of integrations) {
92+
try {
93+
const integrationSetting = await getIntegrationSetting(client, integration)
94+
if (integrationSetting === undefined) {
95+
console.warn('No integration setting found for integration', integration)
96+
continue
97+
}
98+
const integrationData = getIntegrationData(integrationSetting, integration)
99+
const updatedData = {
100+
...(integration.data ?? {}),
101+
...integrationData
102+
}
103+
const updatedIntegration: Integration = {
104+
...integration,
105+
data: updatedData
106+
}
107+
await accountClient.updateIntegration(updatedIntegration)
108+
console.log('Updated integration', updatedIntegration.socialId, updatedIntegration.workspaceUuid)
109+
} catch (e: any) {
110+
console.error('Error updating integration', integration.socialId, integration.workspaceUuid, e)
111+
}
112+
}
113+
}
114+
console.log('Integrations migrations done')
115+
} catch (e) {
116+
console.error('Error migrating integrations', e)
117+
}
118+
}
119+
120+
async function getIntegrationSetting (
121+
client: Client,
122+
integration: Integration
123+
): Promise<IntegrationSetting | undefined> {
124+
const integrationType = getIntegrationType(integration)
125+
if (integrationType === undefined) {
126+
return undefined
127+
}
128+
let integrations = await client.findAll(setting.class.Integration, {
129+
createdBy: integration.socialId,
130+
type: integrationType
131+
})
132+
if (integrations.length === 0) {
133+
integrations = await client.findAll(setting.class.Integration, {
134+
modifiedBy: integration.socialId,
135+
type: integrationType
136+
})
137+
}
138+
if (integrations.length === 0) {
139+
return undefined
140+
}
141+
const enabledIntegration = integrations.find((i) => !i.disabled && i.value !== undefined && i.value !== '')
142+
if (enabledIntegration !== undefined) {
143+
return enabledIntegration
144+
}
145+
return integrations.find((i) => i.value !== undefined && i.value !== '')
146+
}
147+
148+
function getIntegrationData (setting: IntegrationSetting, integration: Integration): Record<string, any> | undefined {
149+
if (setting.value == null || setting.value === '') {
150+
return {}
151+
}
152+
if (integration.kind === GMAIL_INTEGRATION) {
153+
return {
154+
email: setting.value
155+
}
156+
}
157+
if (integration.kind === CALENDAR_INTEGRATION) {
158+
return {
159+
email: setting.value
160+
}
161+
}
162+
if (integration.kind === GITHUB_INTEGRATION) {
163+
return {
164+
installationId: setting.value
165+
}
166+
}
167+
}
168+
169+
function getIntegrationType (integration: Integration): Ref<IntegrationType> | undefined {
170+
if (integration.kind === GMAIL_INTEGRATION) {
171+
return 'gmail:integrationType:Gmail' as any
172+
}
173+
if (integration.kind === CALENDAR_INTEGRATION) {
174+
return 'calendar:integrationType:Calendar' as any
175+
}
176+
if (integration.kind === GITHUB_INTEGRATION) {
177+
return 'github:integrationType:Github' as any
178+
}
179+
180+
return undefined
181+
}

0 commit comments

Comments
 (0)