diff --git a/fern/definition/common/errors.yml b/fern/definition/common/errors.yml index 746a22408..f7564b94c 100644 --- a/fern/definition/common/errors.yml +++ b/fern/definition/common/errors.yml @@ -16,3 +16,6 @@ errors: BadRequestError: status-code: 400 type: BaseError + NotImplementedError: + status-code: 500 + type: BaseError diff --git a/fern/definition/sync.yml b/fern/definition/sync.yml new file mode 100644 index 000000000..17939644a --- /dev/null +++ b/fern/definition/sync.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + errors: common/errors.yml + types: common/types.yml + +types: + TriggerSyncResponse: + properties: + status: types.ResponseStatus + +service: + base-path: /sync + auth: false + audiences: + - external + endpoints: + triggerSync: + docs: Trigger sync for a specific tenant + method: POST + path: '' + request: + name: TriggerSyncRequest + headers: + x-revert-api-token: + type: string + docs: Your official API key for accessing revert apis. + x-revert-t-id: + type: string + docs: The unique customer id used when the customer linked their account. + x-connection-api-key: + type: optional + docs: API key for third party provider + response: TriggerSyncResponse + errors: + - errors.UnAuthorizedError + - errors.InternalServerError + - errors.NotFoundError + - errors.NotImplementedError diff --git a/packages/backend/.env.example b/packages/backend/.env.example index 4d99f8149..ccd942b1d 100644 --- a/packages/backend/.env.example +++ b/packages/backend/.env.example @@ -40,4 +40,6 @@ JIRA_CLIENT_ID= JIRA_CLIENT_SECRET= MS_DYNAMICS_SALES_CLIENT_ID= MS_DYNAMICS_SALES_CLIENT_SECRET= -MS_DYNAMICS_SALES_ORG_URL= \ No newline at end of file +MS_DYNAMICS_SALES_ORG_URL= +OPEN_INT_API_KEY= +TWENTY_ACCOUNT_ID= \ No newline at end of file diff --git a/packages/backend/config.ts b/packages/backend/config.ts index 5ab0ef292..b8969e088 100644 --- a/packages/backend/config.ts +++ b/packages/backend/config.ts @@ -50,6 +50,8 @@ const config = { MS_DYNAMICS_SALES_ORG_URL: process.env.MS_DYNAMICS_SALES_ORG_URL!, BITBUCKET_CLIENT_ID: process.env.BITBUCKET_CLIENT_ID!, BITBUCKET_CLIENT_SECRET: process.env.BITBUCKET_CLIENT_SECRET!, + OPEN_INT_API_KEY: process.env.OPEN_INT_API_KEY, + TWENTY_ACCOUNT_ID: process.env.TWENTY_ACCOUNT_ID, }; export default config; diff --git a/packages/backend/oas/openapi.yml b/packages/backend/oas/openapi.yml index d4c5f32b6..c6134b42a 100644 --- a/packages/backend/oas/openapi.yml +++ b/packages/backend/oas/openapi.yml @@ -2775,6 +2775,57 @@ paths: application/json: schema: $ref: '#/components/schemas/commonBaseError' + /sync: + post: + description: Trigger sync for a specific tenant + operationId: sync_triggerSync + tags: + - Sync + parameters: + - name: x-revert-api-token + in: header + description: Your official API key for accessing revert apis. + required: true + schema: + type: string + - name: x-revert-t-id + in: header + description: The unique customer id used when the customer linked their account. + required: true + schema: + type: string + - name: x-connection-api-key + in: header + description: API key for third party provider + required: false + schema: + type: string + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerSyncResponse' + '401': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/commonBaseError' + '404': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/commonBaseError' + '500': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/commonBaseError' /ticket/collections: get: description: Get all the collections @@ -4322,7 +4373,7 @@ components: type: string tp_customer_id: type: string - description: The email id of the user who connected the app. + description: The emailId or a unique ID id of the user who connected the app. t_id: type: string tp_account_url: @@ -4334,6 +4385,7 @@ components: type: string app_id: type: string + description: Can be obtained from the integration dashboard. required: - tp_id - tp_access_token @@ -5171,6 +5223,14 @@ components: enum: - active - inactive + TriggerSyncResponse: + title: TriggerSyncResponse + type: object + properties: + status: + $ref: '#/components/schemas/commonResponseStatus' + required: + - status ticketGetCollectionsResponse: title: ticketGetCollectionsResponse type: object diff --git a/packages/backend/package.json b/packages/backend/package.json index 1fbe99949..2deba7fbe 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -56,6 +56,8 @@ }, "dependencies": { "@linear/sdk": "^13.0.0", + "@opensdks/runtime": "0.0.16", + "@opensdks/sdk-venice": "0.0.14", "@prisma/client": "^4.16.0", "@sentry/node": "^7.55.2", "@shortloop/node": "0.0.12", diff --git a/packages/backend/routes/index.ts b/packages/backend/routes/index.ts index 5aa02c60c..740bbd8ee 100644 --- a/packages/backend/routes/index.ts +++ b/packages/backend/routes/index.ts @@ -43,6 +43,7 @@ import { userServiceTicket } from '../services/ticket/user'; import { collectionServiceTicket } from '../services/ticket/collection'; import { commentServiceTicket } from '../services/ticket/comment'; import { proxyServiceTicket } from '../services/ticket/proxy'; +import { syncService } from '../services/sync'; const router = express.Router(); @@ -178,6 +179,7 @@ register(router, { collection: collectionServiceTicket, proxy: proxyServiceTicket, }, + sync: syncService, }); export default router; diff --git a/packages/backend/services/custom/twenty.ts b/packages/backend/services/custom/twenty.ts new file mode 100644 index 000000000..b2da61011 --- /dev/null +++ b/packages/backend/services/custom/twenty.ts @@ -0,0 +1,141 @@ +import { Connection } from '../../generated/typescript/api/resources/common/index.js'; +import config from '../../config'; +const { initSDK } = require('@opensdks/runtime'); +const { veniceSdkDef } = require('@opensdks/sdk-venice'); + +const syncTwentyConnection = async (connection: Connection, connectionAPIKey: string) => { + const connectionId = connection.t_id; + const openIntApiKey = config.OPEN_INT_API_KEY; + if (!config.OPEN_INT_API_KEY || !connectionAPIKey) { + console.log('Credentials absent to make this sync happen for: ', connection.t_id, ' returning early'); + return; + } + const venice = initSDK( + { + ...veniceSdkDef, + oasMeta: { + ...veniceSdkDef.oasMeta, + // servers: [{ url: 'http://localhost:4000/api/v0' }], + }, + }, + { + headers: { + 'x-apikey': openIntApiKey, + }, + } + ); + + const twentyAccessToken = connectionAPIKey; + + const connectorConfig = await venice.GET('/core/connector_config'); + + const getConnectorConfig = (cName: string, connectorConfig: any) => + connectorConfig?.data?.filter((c: any) => c.connectorName === cName)[0]; + + const twenty = getConnectorConfig('twenty', connectorConfig); + const revert = getConnectorConfig('revert', connectorConfig); + + let revertResource = await venice.GET('/core/resource', { + params: { + query: { + connectorConfigId: revert.id, + endUserId: connectionId, + }, + }, + }); + + let twentyResource = await venice.GET('/core/resource', { + params: { + query: { + connectorConfigId: twenty.id, + endUserId: connectionId, + }, + }, + }); + + let twentyResourceId = twentyResource?.data[0]?.id; + let revertResourceId = revertResource?.data[0]?.id; + + twentyResource.data = twentyResource?.data[0]; + revertResource.data = revertResource?.data[0]; + + // Create a twenty resource for this connection + if (!twentyResourceId) { + twentyResourceId = await venice.POST('/core/resource', { + body: { + connectorName: twenty.connectorName, + displayName: null, + endUserId: connectionId, + connectorConfigId: twenty.id, + integrationId: null, + settings: { access_token: twentyAccessToken }, + }, + }); + + console.log('Created twenty resourceId', twentyResourceId.data); + + twentyResource = await venice.GET(`/core/resource/${twentyResourceId.data}`); + + console.log('Created twenty resource', twentyResource); + } + + // Create a revert resource for this connection in this environment of Twenty if not created in Twenty’s organisation, + if (!revertResourceId) { + revertResourceId = await venice.POST('/core/resource', { + body: { + connectorName: revert.connectorName, + displayName: null, + endUserId: connectionId, + connectorConfigId: revert.id, + integrationId: null, + settings: { tenant_id: connectionId }, + }, + }); + console.log('Created revert resourceId', revertResourceId.data); + revertResource = await venice.GET(`/core/resource/${revertResourceId.data}`); + + console.log('Created revert resource', revertResource); + } + + console.log('REVERT RESOURCE', revertResourceId); + + console.log('TWENTY RESOURCE', twentyResourceId); + + // Create a pipeline between them if not created, + // Trigger this created pipeline + const syncPipeline = await venice.GET('/core/pipeline', { + params: { + query: { + resourceIds: [twentyResource?.data, revertResource?.data], + }, + }, + }); + + if (!syncPipeline?.data[0]) { + const createdPipeline = await venice.POST('/core/pipeline', { + body: { + id: `pipe_${connectionId}`, + sourceId: revertResource?.data.id, + destinationId: twentyResource?.data.id, + }, + }); + syncPipeline.data = [createdPipeline.data]; + console.log('Created pipeline', syncPipeline?.data); + } + + console.log('PIPELINE', syncPipeline?.data[0]); + + void venice + .POST(`/core/pipeline/${syncPipeline?.data[0]?.id}/_sync`, { + params: { + async: true, + }, + }) + .then() + .catch((e: Error) => console.log('Error', e)) + .finally(() => { + console.log('TRIGGERED'); + }); +}; + +export { syncTwentyConnection }; diff --git a/packages/backend/services/sync.ts b/packages/backend/services/sync.ts new file mode 100644 index 000000000..c527d7a8e --- /dev/null +++ b/packages/backend/services/sync.ts @@ -0,0 +1,28 @@ +import config from '../config'; +import { Connection, NotImplementedError } from '../generated/typescript/api/resources/common'; +import { SyncService } from '../generated/typescript/api/resources/sync/service/SyncService'; +import revertAuthMiddleware from '../helpers/authMiddleware'; +import revertTenantMiddleware from '../helpers/tenantIdMiddleware'; +import { syncTwentyConnection } from './custom/twenty'; + +const syncService = new SyncService( + { + async triggerSync(req, res) { + const account = res.locals.account; + if (account.id === config.TWENTY_ACCOUNT_ID) { + const { 'x-connection-api-key': twentyAPIKey } = req.headers; + const connection = res.locals.connection as Connection; + await syncTwentyConnection(connection, twentyAPIKey as string); + res.send({ + status: 'ok', + }); + } else + throw new NotImplementedError({ + error: 'Syncing is not yet available for your account, sorry! Contact us on discord or email to get access!', + }); + }, + }, + [revertAuthMiddleware(), revertTenantMiddleware()] +); + +export { syncService }; diff --git a/yarn.lock b/yarn.lock index 00b618f88..55e55d6a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5553,6 +5553,34 @@ __metadata: languageName: node linkType: hard +"@opensdks/fetch-links@npm:0.0.17": + version: 0.0.17 + resolution: "@opensdks/fetch-links@npm:0.0.17" + checksum: c81f3214bb97680e0784494caa4585136da324c6395d466a7ab6c1f0f0ac7ea852b5ebbe2fef9ffc46f4c0f055528853ebe0b2101b57ee8f9ecbfcd6db4b24ea + languageName: node + linkType: hard + +"@opensdks/runtime@npm:0.0.16": + version: 0.0.16 + resolution: "@opensdks/runtime@npm:0.0.16" + dependencies: + "@opensdks/fetch-links": 0.0.17 + openapi-fetch: 0.8.1 + openapi-typescript-helpers: 0.0.4 + openapi3-ts: 4.1.2 + checksum: 8664e6b416b7e419335d5b0ab04df37a266c1aa0237a5131f13d172feda3608db1edf375e3bf64331176788524aa65e1eecb9e7bdeadebb30962f3ae67946a8a + languageName: node + linkType: hard + +"@opensdks/sdk-venice@npm:0.0.14": + version: 0.0.14 + resolution: "@opensdks/sdk-venice@npm:0.0.14" + peerDependencies: + "@opensdks/runtime": 0.0.14 + checksum: d46f6e7fa32f03563ffb83dd78e4cfcad65f0f6435e74fd0ed28f16c8623f772721d83f8b9b0989403492cbfa1610750059c9be7525512352e943c47c2c44b32 + languageName: node + linkType: hard + "@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": version: 0.5.10 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.10" @@ -5706,6 +5734,8 @@ __metadata: "@babel/preset-env": ^7.19.4 "@babel/preset-typescript": ^7.18.6 "@linear/sdk": ^13.0.0 + "@opensdks/runtime": 0.0.16 + "@opensdks/sdk-venice": 0.0.14 "@prisma/client": ^4.16.0 "@sentry/node": ^7.55.2 "@shortloop/node": 0.0.12 @@ -23273,6 +23303,31 @@ __metadata: languageName: node linkType: hard +"openapi-fetch@npm:0.8.1": + version: 0.8.1 + resolution: "openapi-fetch@npm:0.8.1" + dependencies: + openapi-typescript-helpers: ^0.0.4 + checksum: 9bdc37b21bc662af2826c51165ea1e1b0fa5ac05076852870457800d85b0a9bbf3466f905919fba014badd7143946e86640f748dd5720d49445d32d16429ccab + languageName: node + linkType: hard + +"openapi-typescript-helpers@npm:0.0.4, openapi-typescript-helpers@npm:^0.0.4": + version: 0.0.4 + resolution: "openapi-typescript-helpers@npm:0.0.4" + checksum: d06d62f9669db5110f32ef36a70f9f2698dd7ad03ba41968baf13e1091caaf23c4af6f8a79103cfd68f65046da4284dcc2f75e9950e9170115189b545e3030fc + languageName: node + linkType: hard + +"openapi3-ts@npm:4.1.2": + version: 4.1.2 + resolution: "openapi3-ts@npm:4.1.2" + dependencies: + yaml: ^2.2.2 + checksum: 019a66f4195966bf58ee29f913b91e39975616458fdb40a15507a3b3e338eacfc882def0dda1a5001edf962e382ed6f46c8a0d40684adcccb868960267df30d9 + languageName: node + linkType: hard + "opener@npm:^1.5.2": version: 1.5.2 resolution: "opener@npm:1.5.2" @@ -31156,6 +31211,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.2": + version: 2.4.1 + resolution: "yaml@npm:2.4.1" + bin: + yaml: bin.mjs + checksum: 4c391d07a5d5e935e058babb71026c9cdc9a6fd889e35dd91b53cfb0a12691b67c6c5c740858e71345fef18cd9c13c554a6dda9196f59820d769d94041badb0b + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9"