From 659d94caef9300221fdfd22b2f33a96a5fef1204 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Tue, 6 Aug 2024 01:15:28 +0530 Subject: [PATCH 01/12] adding linear users --- packages/api/scripts/init.sql | 3 +- packages/api/scripts/seed.sql | 8 +-- .../types/original/original.ticketing.ts | 7 +- .../ticketing/user/services/linear/index.ts | 65 ++++++++++++++++++ .../ticketing/user/services/linear/mappers.ts | 68 +++++++++++++++++++ .../ticketing/user/services/linear/types.ts | 8 +++ .../api/src/ticketing/user/user.module.ts | 4 ++ packages/shared/src/connectors/enum.ts | 3 +- packages/shared/src/connectors/index.ts | 2 +- 9 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 packages/api/src/ticketing/user/services/linear/index.ts create mode 100644 packages/api/src/ticketing/user/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/user/services/linear/types.ts diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index f6d1d46bd..f68a70b34 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -374,7 +374,8 @@ CREATE TABLE connector_sets crm_zendesk boolean NULL, crm_close boolean NULL, fs_box boolean NULL, - CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) + tcg_linear boolean NULL, +CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) ); -- ************************************** connection_strategies diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index 6bd5a3ec2..3f52493c9 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -1,10 +1,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES ('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora'); -INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box) VALUES - ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); +INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_linear) VALUES + ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'), diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index ff69acd3b..3783aac50 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,6 @@ +import { GitlabUserInput, GitlabUserOutput } from '@ticketing/user/services/gitlab/types'; +import { LinearUserInput, LinearUserOutput } from '@ticketing/user/services/linear/types'; + import { FrontAccountInput, FrontAccountOutput, @@ -147,7 +150,7 @@ export type OriginalUserInput = | ZendeskUserInput | FrontUserInput | GorgiasUserInput - | JiraUserInput; + | JiraUserInput | GitlabUserInput | LinearUserInput; //| JiraServiceMgmtUserInput; /* account */ export type OriginalAccountInput = ZendeskAccountInput | FrontAccountInput; @@ -211,7 +214,7 @@ export type OriginalUserOutput = | ZendeskUserOutput | FrontUserOutput | GorgiasUserOutput - | JiraUserOutput; + | JiraUserOutput | GitlabUserOutput | LinearUserOutput; /* account */ export type OriginalAccountOutput = ZendeskAccountOutput | FrontAccountOutput; /* contact */ diff --git a/packages/api/src/ticketing/user/services/linear/index.ts b/packages/api/src/ticketing/user/services/linear/index.ts new file mode 100644 index 000000000..4e094e0fd --- /dev/null +++ b/packages/api/src/ticketing/user/services/linear/index.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { IUserService } from '@ticketing/user/types'; +import { LinearUserOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LinearService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.user.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const userQuery = { + "query": "query { users { nodes { id, name, email } }}" + }; + + let resp = await axios.post( + `${connection.account_url}`, + userQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear users !`); + + return { + data: resp.data.users.nodes, + message: 'Linear users retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ticketing/user/services/linear/mappers.ts b/packages/api/src/ticketing/user/services/linear/mappers.ts new file mode 100644 index 000000000..8c94e50d8 --- /dev/null +++ b/packages/api/src/ticketing/user/services/linear/mappers.ts @@ -0,0 +1,68 @@ +import { IUserMapper } from '@ticketing/user/types'; +import { + UnifiedTicketingUserInput, + UnifiedTicketingUserOutput, +} from '@ticketing/user/types/model.unified'; +import { LinearUserInput, LinearUserOutput } from './types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@ticketing/@lib/@utils'; + +@Injectable() +export class LinearUserMapper implements IUserMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('ticketing', 'user', 'linear', this); + } + desunify( + source: UnifiedTicketingUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): LinearUserInput { + return; + } + + async unify( + source: LinearUserOutput | LinearUserOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const sourcesArray = Array.isArray(source) ? source : [source]; + return sourcesArray.map((user) => + this.mapSingleUserToUnified(user, connectionId, customFieldMappings), + ); + } + + private mapSingleUserToUnified( + user: LinearUserOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingUserOutput { + // Initialize field_mappings array from customFields, if provided + const field_mappings = customFieldMappings + ? customFieldMappings + .map((mapping) => ({ + key: mapping.slug, + value: user ? user[mapping.remote_id] : undefined, + })) + .filter((mapping) => mapping.value !== undefined) + : []; + + const unifiedUser: UnifiedTicketingUserOutput = { + remote_id: user.id, + remote_data: user, + name: user.name, + email_address: user.email || null, + field_mappings, + }; + + return unifiedUser; + } +} diff --git a/packages/api/src/ticketing/user/services/linear/types.ts b/packages/api/src/ticketing/user/services/linear/types.ts new file mode 100644 index 000000000..d0633d48a --- /dev/null +++ b/packages/api/src/ticketing/user/services/linear/types.ts @@ -0,0 +1,8 @@ +interface LinearUser { + id: string + name: string + email: string +} + +export type LinearUserInput = Partial; +export type LinearUserOutput = LinearUserInput; diff --git a/packages/api/src/ticketing/user/user.module.ts b/packages/api/src/ticketing/user/user.module.ts index 13fc5aaae..d3d79e0f8 100644 --- a/packages/api/src/ticketing/user/user.module.ts +++ b/packages/api/src/ticketing/user/user.module.ts @@ -1,3 +1,5 @@ +import { LinearUserMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { Module } from '@nestjs/common'; import { FrontService } from './services/front'; @@ -39,6 +41,8 @@ import { ZendeskUserMapper } from './services/zendesk/mappers'; JiraUserMapper, GorgiasUserMapper, GitlabUserMapper, + LinearService, + LinearUserMapper, ], exports: [SyncService, ServiceRegistry, WebhookService, IngestDataService], }) diff --git a/packages/shared/src/connectors/enum.ts b/packages/shared/src/connectors/enum.ts index ce0ef4aaf..f4d17486d 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -12,7 +12,8 @@ export enum TicketingConnectors { FRONT = 'front', JIRA = 'jira', GORGIAS = 'gorgias', - GITLAB = 'gitlab' + GITLAB = 'gitlab', + LINEAR = 'linear' } export enum AccountingConnectors { diff --git a/packages/shared/src/connectors/index.ts b/packages/shared/src/connectors/index.ts index a32615e52..1ab01e45c 100644 --- a/packages/shared/src/connectors/index.ts +++ b/packages/shared/src/connectors/index.ts @@ -2,6 +2,6 @@ export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio' export const HRIS_PROVIDERS = []; export const ATS_PROVIDERS = []; export const ACCOUNTING_PROVIDERS = []; -export const TICKETING_PROVIDERS = ['zendesk', 'front', 'jira', 'gorgias', 'gitlab']; +export const TICKETING_PROVIDERS = ['zendesk', 'front', 'jira', 'gorgias', 'gitlab', 'linear']; export const MARKETINGAUTOMATION_PROVIDERS = []; export const FILESTORAGE_PROVIDERS = []; From 929210ccc6ec544f2c7b5637552a41882593ccff Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 7 Aug 2024 16:14:45 +0530 Subject: [PATCH 02/12] adding placeholder for linear creds --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 2463c0671..12e635810 100644 --- a/.env.example +++ b/.env.example @@ -95,6 +95,9 @@ FRONT_TICKETING_CLOUD_CLIENT_SECRET= # Gitlab GITLAB_TICKETING_CLOUD_CLIENT_ID= GITLAB_TICKETING_CLOUD_CLIENT_SECRET= +# Linear +LINEAR_TICKETING_CLOUD_CLIENT_ID= +LINEAR_TICKETING_CLOUD_CLIENT_SECRET= # ================================================ # File Storage From 6221b10acabada5a960f6ad0248cfc0886e2d709 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 7 Aug 2024 16:42:43 +0530 Subject: [PATCH 03/12] fixing linear users sync --- packages/api/src/ticketing/user/services/linear/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/ticketing/user/services/linear/index.ts b/packages/api/src/ticketing/user/services/linear/index.ts index 4e094e0fd..09ec92f0e 100644 --- a/packages/api/src/ticketing/user/services/linear/index.ts +++ b/packages/api/src/ticketing/user/services/linear/index.ts @@ -54,7 +54,7 @@ export class LinearService implements IUserService { this.logger.log(`Synced linear users !`); return { - data: resp.data.users.nodes, + data: resp.data.data.users.nodes, message: 'Linear users retrieved', statusCode: 200, }; From 0ab3e0321580646e45b7eaea891a7675d2a62816 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 7 Aug 2024 17:27:40 +0530 Subject: [PATCH 04/12] adding linear teams --- .../types/original/original.ticketing.ts | 6 +- .../ticketing/team/services/linear/index.ts | 67 +++++++++++++++++++ .../ticketing/team/services/linear/mappers.ts | 59 ++++++++++++++++ .../ticketing/team/services/linear/types.ts | 8 +++ .../api/src/ticketing/team/team.module.ts | 4 ++ 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/ticketing/team/services/linear/index.ts create mode 100644 packages/api/src/ticketing/team/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/team/services/linear/types.ts diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index 3783aac50..a8f782bb2 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,5 @@ +import { LinearTeamInput, LinearTeamOutput } from '@ticketing/team/services/linear/types'; + import { GitlabUserInput, GitlabUserOutput } from '@ticketing/user/services/gitlab/types'; import { LinearUserInput, LinearUserOutput } from '@ticketing/user/services/linear/types'; @@ -173,7 +175,7 @@ export type OriginalTeamInput = | ZendeskTeamInput | FrontTeamInput | GorgiasTeamInput - | JiraTeamInput; + | JiraTeamInput | LinearTeamInput; /* attachment */ export type OriginalAttachmentInput = null; @@ -236,7 +238,7 @@ export type OriginalTeamOutput = | ZendeskTeamOutput | FrontTeamOutput | GorgiasTeamOutput - | JiraTeamOutput; + | JiraTeamOutput | LinearTeamOutput; /* attachment */ export type OriginalAttachmentOutput = diff --git a/packages/api/src/ticketing/team/services/linear/index.ts b/packages/api/src/ticketing/team/services/linear/index.ts new file mode 100644 index 000000000..952af6ad4 --- /dev/null +++ b/packages/api/src/ticketing/team/services/linear/index.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITeamService } from '@ticketing/team/types'; +import { LinearTeamOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LinearService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const teamQuery = { + "query": "query { teams { nodes { id, name, description } }}" + }; + + let resp = await axios.post( + `${connection.account_url}`, + teamQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear teams !`); + + return { + data: resp.data.data.teams.nodes, + message: 'Linear teams retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} + + diff --git a/packages/api/src/ticketing/team/services/linear/mappers.ts b/packages/api/src/ticketing/team/services/linear/mappers.ts new file mode 100644 index 000000000..d298b56a7 --- /dev/null +++ b/packages/api/src/ticketing/team/services/linear/mappers.ts @@ -0,0 +1,59 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { LinearTeamInput, LinearTeamOutput } from './types'; +import { + UnifiedTicketingTeamInput, + UnifiedTicketingTeamOutput, +} from '@ticketing/team/types/model.unified'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@ticketing/@lib/@utils'; + +@Injectable() +export class LinearTeamMapper implements ITeamMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('ticketing', 'team', 'linear', this); + } + desunify( + source: UnifiedTicketingTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): LinearTeamInput { + return; + } + + unify( + source: LinearTeamOutput | LinearTeamOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingTeamOutput | UnifiedTicketingTeamOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((team) => + this.mapSingleTeamToUnified(team, connectionId, customFieldMappings), + ); + } + + private mapSingleTeamToUnified( + team: LinearTeamOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingTeamOutput { + const unifiedTeam: UnifiedTicketingTeamOutput = { + remote_id: team.id, + remote_data: team, + name: team.name, + description: team.description, + }; + + return unifiedTeam; + } +} diff --git a/packages/api/src/ticketing/team/services/linear/types.ts b/packages/api/src/ticketing/team/services/linear/types.ts new file mode 100644 index 000000000..ec9064371 --- /dev/null +++ b/packages/api/src/ticketing/team/services/linear/types.ts @@ -0,0 +1,8 @@ +export type LinearTeam = { + id: string + name: string + description: string +}; + +export type LinearTeamInput = Partial; +export type LinearTeamOutput = LinearTeamInput; \ No newline at end of file diff --git a/packages/api/src/ticketing/team/team.module.ts b/packages/api/src/ticketing/team/team.module.ts index d868b6185..fb61f5bac 100644 --- a/packages/api/src/ticketing/team/team.module.ts +++ b/packages/api/src/ticketing/team/team.module.ts @@ -1,3 +1,5 @@ +import { LinearTeamMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; @@ -36,6 +38,8 @@ import { TeamController } from './team.controller'; FrontTeamMapper, JiraTeamMapper, GorgiasTeamMapper, + LinearService, + LinearTeamMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) From fe83ba5d78178a974464f4070e151bda6594244d Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Thu, 8 Aug 2024 10:39:30 +0530 Subject: [PATCH 05/12] adding linear tag --- .../types/original/original.ticketing.ts | 6 +- .../ticketing/tag/services/linear/index.ts | 65 +++++++++++++++++++ .../ticketing/tag/services/linear/mappers.ts | 58 +++++++++++++++++ .../ticketing/tag/services/linear/types.ts | 7 ++ packages/api/src/ticketing/tag/tag.module.ts | 4 ++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/ticketing/tag/services/linear/index.ts create mode 100644 packages/api/src/ticketing/tag/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/tag/services/linear/types.ts diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index a8f782bb2..daff2c4e0 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,5 @@ +import { LinearTagInput, LinearTagOutput } from '@ticketing/tag/services/linear/types'; + import { LinearTeamInput, LinearTeamOutput } from '@ticketing/team/services/linear/types'; import { GitlabUserInput, GitlabUserOutput } from '@ticketing/user/services/gitlab/types'; @@ -168,7 +170,7 @@ export type OriginalTagInput = | FrontTagInput | GorgiasTagInput | JiraTagInput - | GitlabTagInput; + | GitlabTagInput | LinearTagInput; /* team */ export type OriginalTeamInput = @@ -231,7 +233,7 @@ export type OriginalTagOutput = | FrontTagOutput | GorgiasTagOutput | JiraTagOutput - | GitlabTagOutput; + | GitlabTagOutput | LinearTagOutput; /* team */ export type OriginalTeamOutput = diff --git a/packages/api/src/ticketing/tag/services/linear/index.ts b/packages/api/src/ticketing/tag/services/linear/index.ts new file mode 100644 index 000000000..7f5a6b072 --- /dev/null +++ b/packages/api/src/ticketing/tag/services/linear/index.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITagService } from '@ticketing/tag/types'; +import { LinearTagOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LinearService implements ITagService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.tag.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_ticket } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const labelQuery = { + "query": "query { issueLabels { nodes { id name } }}" + }; + + let resp = await axios.post( + `${connection.account_url}`, + labelQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear tags !`); + + return { + data: resp.data.data.issueLabels.nodes, + message: 'Linear tags retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ticketing/tag/services/linear/mappers.ts b/packages/api/src/ticketing/tag/services/linear/mappers.ts new file mode 100644 index 000000000..736ee385d --- /dev/null +++ b/packages/api/src/ticketing/tag/services/linear/mappers.ts @@ -0,0 +1,58 @@ +import { ITagMapper } from '@ticketing/tag/types'; +import { LinearTagInput, LinearTagOutput } from './types'; +import { + UnifiedTicketingTagInput, + UnifiedTicketingTagOutput, +} from '@ticketing/tag/types/model.unified'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@ticketing/@lib/@utils'; + +@Injectable() +export class LinearTagMapper implements ITagMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('ticketing', 'tag', 'linear', this); + } + desunify( + source: UnifiedTicketingTagInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): LinearTagInput { + return; + } + + unify( + source: LinearTagOutput | LinearTagOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingTagOutput | UnifiedTicketingTagOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((tag) => + this.mapSingleTagToUnified(tag, connectionId, customFieldMappings), + ); + } + + private mapSingleTagToUnified( + tag: LinearTagOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingTagOutput { + const unifiedTag: UnifiedTicketingTagOutput = { + remote_id: tag.id, + remote_data: tag, + name: tag.name, + }; + + return unifiedTag; + } +} diff --git a/packages/api/src/ticketing/tag/services/linear/types.ts b/packages/api/src/ticketing/tag/services/linear/types.ts new file mode 100644 index 000000000..e1a52557b --- /dev/null +++ b/packages/api/src/ticketing/tag/services/linear/types.ts @@ -0,0 +1,7 @@ +interface LinearTag { + id: string + name: string +} + +export type LinearTagInput = Partial; +export type LinearTagOutput = LinearTagInput; diff --git a/packages/api/src/ticketing/tag/tag.module.ts b/packages/api/src/ticketing/tag/tag.module.ts index 1358ea4b4..479676e5a 100644 --- a/packages/api/src/ticketing/tag/tag.module.ts +++ b/packages/api/src/ticketing/tag/tag.module.ts @@ -1,3 +1,5 @@ +import { LinearTagMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @@ -41,6 +43,8 @@ import { GitlabTagMapper } from './services/gitlab/mappers'; JiraTagMapper, GorgiasTagMapper, GitlabTagMapper, + LinearService, + LinearTagMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) From 33e34eaa3e2480a8f40b2a992929dd6060b2a4d6 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Thu, 8 Aug 2024 10:51:39 +0530 Subject: [PATCH 06/12] adding linear comments --- .../types/original/original.ticketing.ts | 6 +- .../ticketing/collection/collection.module.ts | 4 ++ .../collection/services/linear/index.ts | 65 +++++++++++++++++ .../collection/services/linear/mappers.ts | 69 +++++++++++++++++++ .../collection/services/linear/types.ts | 8 +++ 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/ticketing/collection/services/linear/index.ts create mode 100644 packages/api/src/ticketing/collection/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/collection/services/linear/types.ts diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index daff2c4e0..e61199b19 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,5 @@ +import { LinearCollectionInput, LinearCollectionOutput } from '@ticketing/collection/services/linear/types'; + import { LinearTagInput, LinearTagOutput } from '@ticketing/tag/services/linear/types'; import { LinearTeamInput, LinearTeamOutput } from '@ticketing/team/services/linear/types'; @@ -183,7 +185,7 @@ export type OriginalTeamInput = export type OriginalAttachmentInput = null; export type OriginalCollectionInput = | JiraCollectionInput - | GitlabCollectionInput; + | GitlabCollectionInput | LinearCollectionInput; export type TicketingObjectInput = | OriginalTicketInput @@ -253,7 +255,7 @@ export type OriginalAttachmentOutput = export type OriginalCollectionOutput = | JiraCollectionOutput - | GitlabCollectionOutput; + | GitlabCollectionOutput | LinearCollectionOutput; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/ticketing/collection/collection.module.ts b/packages/api/src/ticketing/collection/collection.module.ts index efc18590c..10fb43695 100644 --- a/packages/api/src/ticketing/collection/collection.module.ts +++ b/packages/api/src/ticketing/collection/collection.module.ts @@ -1,3 +1,5 @@ +import { LinearCollectionMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; @@ -36,6 +38,8 @@ import { IngestDataService } from '@@core/@core-services/unification/ingest-data /* PROVIDERS MAPPERS */ JiraCollectionMapper, GitlabCollectionMapper, + LinearService, + LinearCollectionMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/ticketing/collection/services/linear/index.ts b/packages/api/src/ticketing/collection/services/linear/index.ts new file mode 100644 index 000000000..357681884 --- /dev/null +++ b/packages/api/src/ticketing/collection/services/linear/index.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ICollectionService } from '@ticketing/collection/types'; +import { LinearCollectionOutput, LinearCollectionInput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LinearService implements ICollectionService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.collection.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const projectQuery = { + "query": "query { projects { nodes { id, name, description } }}" + }; + + let resp = await axios.post( + `${connection.account_url}`, + projectQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear collections !`); + + return { + data: resp.data.data.projects.nodes, + message: 'Linear collections retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ticketing/collection/services/linear/mappers.ts b/packages/api/src/ticketing/collection/services/linear/mappers.ts new file mode 100644 index 000000000..62bb465a2 --- /dev/null +++ b/packages/api/src/ticketing/collection/services/linear/mappers.ts @@ -0,0 +1,69 @@ +import { ICollectionMapper } from '@ticketing/collection/types'; +import { LinearCollectionInput, LinearCollectionOutput } from './types'; +import { + UnifiedTicketingCollectionInput, + UnifiedTicketingCollectionOutput, +} from '@ticketing/collection/types/model.unified'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@ticketing/@lib/@utils'; + +@Injectable() +export class LinearCollectionMapper implements ICollectionMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService( + 'ticketing', + 'collection', + 'linear', + this, + ); + } + desunify( + source: UnifiedTicketingCollectionInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): LinearCollectionInput { + return; + } + + unify( + source: LinearCollectionOutput | LinearCollectionOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingCollectionOutput | UnifiedTicketingCollectionOutput[] { + // If the source is not an array, convert it to an array for mapping + const sourcesArray = Array.isArray(source) ? source : [source]; + + return sourcesArray.map((collection) => + this.mapSingleCollectionToUnified( + collection, + connectionId, + customFieldMappings, + ), + ); + } + + private mapSingleCollectionToUnified( + collection: LinearCollectionOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTicketingCollectionOutput { + const unifiedCollection: UnifiedTicketingCollectionOutput = { + remote_id: collection.id, + remote_data: collection, + name: collection.name, + description: collection.description, + collection_type: 'PROJECT', + }; + + return unifiedCollection; + } +} diff --git a/packages/api/src/ticketing/collection/services/linear/types.ts b/packages/api/src/ticketing/collection/services/linear/types.ts new file mode 100644 index 000000000..e96b73023 --- /dev/null +++ b/packages/api/src/ticketing/collection/services/linear/types.ts @@ -0,0 +1,8 @@ +interface LinearCollection { + id: string + name: string + description: string +} + +export type LinearCollectionInput = Partial; +export type LinearCollectionOutput = LinearCollectionInput; From 5c520ff3e31ab697c706e998fce900eb26ff4b0a Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Thu, 8 Aug 2024 22:48:25 +0530 Subject: [PATCH 07/12] adding linear comment --- .../types/original/original.ticketing.ts | 6 +- .../src/ticketing/comment/comment.module.ts | 4 + .../comment/services/linear/index.ts | 118 ++++++++++++++++++ .../comment/services/linear/mappers.ts | 99 +++++++++++++++ .../comment/services/linear/types.ts | 13 ++ 5 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/ticketing/comment/services/linear/index.ts create mode 100644 packages/api/src/ticketing/comment/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/comment/services/linear/types.ts diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index e61199b19..fb5c5e3f0 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,5 @@ +import { LinearCommentInput, LinearCommentOutput } from '@ticketing/comment/services/linear/types'; + import { LinearCollectionInput, LinearCollectionOutput } from '@ticketing/collection/services/linear/types'; import { LinearTagInput, LinearTagOutput } from '@ticketing/tag/services/linear/types'; @@ -149,7 +151,7 @@ export type OriginalCommentInput = | FrontCommentInput | GorgiasCommentInput | JiraCommentInput - | GitlabCommentInput; + | GitlabCommentInput | LinearCommentInput; //| JiraCommentServiceMgmtInput; /* user */ export type OriginalUserInput = @@ -214,7 +216,7 @@ export type OriginalCommentOutput = | FrontCommentOutput | GorgiasCommentOutput | JiraCommentOutput - | GitlabCommentOutput; + | GitlabCommentOutput | LinearCommentOutput; /* user */ export type OriginalUserOutput = | ZendeskUserOutput diff --git a/packages/api/src/ticketing/comment/comment.module.ts b/packages/api/src/ticketing/comment/comment.module.ts index 9984d7218..e9a3e7d33 100644 --- a/packages/api/src/ticketing/comment/comment.module.ts +++ b/packages/api/src/ticketing/comment/comment.module.ts @@ -1,3 +1,5 @@ +import { LinearCommentMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; @@ -42,6 +44,8 @@ import { SyncService } from './sync/sync.service'; JiraCommentMapper, GorgiasCommentMapper, GitlabCommentMapper, + LinearService, + LinearCommentMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/ticketing/comment/services/linear/index.ts b/packages/api/src/ticketing/comment/services/linear/index.ts new file mode 100644 index 000000000..4c642a346 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/linear/index.ts @@ -0,0 +1,118 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { Injectable } from '@nestjs/common'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { Utils } from '@ticketing/@lib/@utils'; +import { ICommentService } from '@ticketing/comment/types'; +import axios from 'axios'; +import * as fs from 'fs'; +import { ServiceRegistry } from '../registry.service'; +import { LinearCommentInput, LinearCommentOutput } from './types'; +import { LinearTicketOutput } from '@ticketing/ticket/services/linear/types'; + +@Injectable() +export class LinearService implements ICommentService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private utils: Utils, + ) { + this.logger.setContext( + TicketingObject.comment.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + + async addComment( + commentData: LinearCommentInput, + linkedUserId: string, + remoteIdTicket: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + // Skipping Storing the attachment in unified object as Linear stores attachment as link in Markdown Format + + const createCommentMutation = { + "query": `mutation { commentCreate( input: { body: \"${commentData.body}\" issueId: \"${remoteIdTicket}\" } ) { comment { body issue { id } user { id } } }}` + }; + + let resp = await axios.post( + `${connection.account_url}`, + createCommentMutation, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { + data: resp.data.data.issueLabels.nodes, + message: 'Linear comment created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_ticket } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const ticket = await this.prisma.tcg_tickets.findUnique({ + where: { + id_tcg_ticket: id_ticket as string, + }, + select: { + remote_id: true, + }, + }); + + const commentQuery = { + "query": `query { issue(id: \"${ticket.remote_id}\") { comments { nodes { id body user { id } issue { id } } } }}` + }; + + let resp = await axios.post( + `${connection.account_url}`, + commentQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear comments !`); + + return { + data: resp.data.data.issue.comments.nodes, + message: 'Linear comments retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} \ No newline at end of file diff --git a/packages/api/src/ticketing/comment/services/linear/mappers.ts b/packages/api/src/ticketing/comment/services/linear/mappers.ts new file mode 100644 index 000000000..5d72d1b46 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/linear/mappers.ts @@ -0,0 +1,99 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.ticketing'; +import { Injectable } from '@nestjs/common'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { Utils } from '@ticketing/@lib/@utils'; +import { UnifiedTicketingAttachmentOutput } from '@ticketing/attachment/types/model.unified'; +import { ICommentMapper } from '@ticketing/comment/types'; +import { + UnifiedTicketingCommentInput, + UnifiedTicketingCommentOutput, +} from '@ticketing/comment/types/model.unified'; +import { LinearCommentInput, LinearCommentOutput } from './types'; + +@Injectable() +export class LinearCommentMapper implements ICommentMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ticketing', + 'comment', + 'linear', + this, + ); + } + + async desunify( + source: UnifiedTicketingCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + // project_id and issue_id will be extracted and used so We do not need to set user (author) field here + + // TODO - Add attachments attribute + + const result: LinearCommentInput = { + body: source.body, + }; + return result; + } + + async unify( + source: LinearCommentOutput | LinearCommentOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleCommentToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified( + comment, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCommentToUnified( + comment: LinearCommentOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + let user_id: string; + + if (comment.user.id) { + user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.user.id), + connectionId, + ); + } + + return { + remote_id: comment.id, + remote_data: comment, + body: comment.body || null, + ticket_id: comment.issue.id, + user_id: user_id, + creator_type: 'USER', + }; + } +} \ No newline at end of file diff --git a/packages/api/src/ticketing/comment/services/linear/types.ts b/packages/api/src/ticketing/comment/services/linear/types.ts new file mode 100644 index 000000000..d03ffe496 --- /dev/null +++ b/packages/api/src/ticketing/comment/services/linear/types.ts @@ -0,0 +1,13 @@ +interface LinearComment { + id: string + body: string + user: { + id: string + } + issue: { + id: string + } +} + +export type LinearCommentInput = Partial; +export type LinearCommentOutput = LinearCommentInput; From 1aefde993f77b2c45170a01d29ccb26290ecc041 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Fri, 9 Aug 2024 16:23:37 +0530 Subject: [PATCH 08/12] adding linear issue --- .../types/original/original.ticketing.ts | 6 +- .../ticketing/ticket/services/linear/index.ts | 122 +++++++++++ .../ticket/services/linear/mappers.ts | 189 ++++++++++++++++++ .../ticketing/ticket/services/linear/types.ts | 33 +++ .../api/src/ticketing/ticket/ticket.module.ts | 4 + 5 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/ticketing/ticket/services/linear/index.ts create mode 100644 packages/api/src/ticketing/ticket/services/linear/mappers.ts create mode 100644 packages/api/src/ticketing/ticket/services/linear/types.ts diff --git a/packages/api/src/@core/utils/types/original/original.ticketing.ts b/packages/api/src/@core/utils/types/original/original.ticketing.ts index 094ffc962..3823aa975 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,3 +1,5 @@ +import { LinearTicketInput, LinearTicketOutput } from '@ticketing/ticket/services/linear/types'; + import { LinearCommentInput, LinearCommentOutput } from '@ticketing/comment/services/linear/types'; import { LinearCollectionInput, LinearCollectionOutput } from '@ticketing/collection/services/linear/types'; @@ -155,7 +157,7 @@ export type OriginalTicketInput = | FrontTicketInput | GorgiasTicketInput | JiraTicketInput - | GitlabTicketInput | GithubTicketInput; + | GitlabTicketInput | GithubTicketInput | LinearTicketInput; //| JiraServiceMgmtTicketInput; /* comment */ @@ -221,7 +223,7 @@ export type OriginalTicketOutput = | FrontTicketOutput | GorgiasTicketOutput | JiraTicketOutput - | GitlabTicketOutput | GithubTicketOutput; + | GitlabTicketOutput | GithubTicketOutput | LinearTicketOutput; /* comment */ export type OriginalCommentOutput = diff --git a/packages/api/src/ticketing/ticket/services/linear/index.ts b/packages/api/src/ticketing/ticket/services/linear/index.ts new file mode 100644 index 000000000..724cce27a --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/linear/index.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { ITicketService } from '@ticketing/ticket/types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { LinearTicketInput, LinearTicketOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { LinearCollectionOutput } from '@ticketing/collection/services/linear/types'; + +@Injectable() +export class LinearService implements ITicketService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.ticket.toUpperCase() + ':' + LinearService.name, + ); + this.registry.registerService('linear', this); + } + async addTicket( + ticketData: LinearTicketInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + if (!ticketData.team.id) { + throw new ReferenceError( + `team_id is required field and cant be empty while creating a ticket.`, + ); + } + + const createIssueMutation = { + "query": `mutation ($issueCreateInput: IssueCreateInput!) { issueCreate(input: $issueCreateInput) { issue { id title description dueDate parent{ id } state { name } project{ id } labels { nodes { id } } completedAt priorityLabel assignee { id } comments { nodes { id } } } }}`, + "variables": { + "issueCreateInput": { + "title": ticketData.title, + "description": ticketData.description, + "assigneeId": ticketData.assignee.id, + "parentId": ticketData.parent.id, + "labelIds": ticketData.labels.nodes, + "projectId": ticketData.project.id, + "sourceCommentId": ticketData.comments.nodes, + "dueDate": ticketData.dueDate, + "teamId": ticketData.team.id + } + } + }; + + let resp = await axios.post( + `${connection.account_url}`, + createIssueMutation, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { + data: resp.data.data.issueCreate.issue, + message: 'Linear ticket created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'linear', + vertical: 'ticketing', + }, + }); + + const issueQuery = { + "query": "query { issues { nodes { id title description dueDate parent{ id } state { name } project{ id } labels { nodes { id } } completedAt priorityLabel assignee { id } comments { nodes { id } } } }}" + }; + + let resp = await axios.post( + `${connection.account_url}`, + issueQuery, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced linear tickets !`); + + return { + data: resp.data.data.users.nodes, + message: 'Linear tickets retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ticketing/ticket/services/linear/mappers.ts b/packages/api/src/ticketing/ticket/services/linear/mappers.ts new file mode 100644 index 000000000..f4108a216 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/linear/mappers.ts @@ -0,0 +1,189 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { OriginalTagOutput } from '@@core/utils/types/original/original.ticketing'; +import { UnifiedTicketingTagOutput } from '@ticketing/tag/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { TicketingObject } from '@ticketing/@lib/@types'; +import { Utils } from '@ticketing/@lib/@utils'; +import { ITicketMapper } from '@ticketing/ticket/types'; +import { + UnifiedTicketingTicketInput, + UnifiedTicketingTicketOutput, +} from '@ticketing/ticket/types/model.unified'; +import { LinearTicketInput, LinearTicketOutput } from './types'; +import { LinearTagInput, LinearTagOutput } from '@ticketing/tag/services/linear/types'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { UnifiedTicketingCommentOutput } from '@ticketing/comment/types/model.unified'; +import { LinearCommentInput } from '@ticketing/comment/services/linear/types'; + +@Injectable() +export class LinearTicketMapper implements ITicketMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + private ingestService: IngestDataService, + ) { + this.mappersRegistry.registerService('ticketing', 'ticket', 'linear', this); + } + + async desunify( + source: UnifiedTicketingTicketInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + connectionId?: string, + ): Promise { + // const remote_project_id = await this.utils.getCollectionRemoteIdFromUuid( + // source.collections[0] as string, + // ); + + const result: LinearTicketInput = { + title: source.name, + description: source.description ? source.description : null, + // Passing new Field to retreive repositroy info to add ticket to that repo + project: source.collections[0] as string + }; + + + + if (source.assigned_to && source.assigned_to.length > 0) { + const data = await this.utils.getAsigneeRemoteIdFromUserUuid( + source.assigned_to[0], + ); + if (data) { + result.assignee = { id: data }; + } + } + const tags = source.tags as LinearTagInput[]; + if (tags) { + result.labels.nodes = tags; + } + + if (source.comment) { + const comment = + (await this.coreUnificationService.desunify({ + sourceObject: source.comment, + targetType: TicketingObject.comment, + providerName: 'linear', + vertical: 'ticketing', + connectionId: connectionId, + customFieldMappings: [], + })) as LinearCommentInput; + result.comments.nodes = [comment]; + } + + // TODO - Custom fields mapping + // if (customFieldMappings && source.field_mappings) { + // result.meta = {}; // Ensure meta exists + // for (const [k, v] of Object.entries(source.field_mappings)) { + // const mapping = customFieldMappings.find( + // (mapping) => mapping.slug === k, + // ); + // if (mapping) { + // result.meta[mapping.remote_id] = v; + // } + // } + // } + + return result; + } + + async unify( + source: LinearTicketOutput | LinearTicketOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const sourcesArray = Array.isArray(source) ? source : [source]; + return Promise.all( + sourcesArray.map(async (ticket) => + this.mapSingleTicketToUnified( + ticket, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleTicketToUnified( + ticket: LinearTicketOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = ticket[mapping.remote_id]; + } + } + + let opts: any = {}; + if (ticket.state) { + opts = { ...opts, type: (ticket.state.name === 'Canceled' || ticket.state.name === 'Done') ? 'CLOSED' : 'OPEN' }; + } + + if (ticket.assignee) { + //fetch the right assignee uuid from remote id + const user_id = await this.utils.getUserUuidFromRemoteId( + String(ticket.assignee.id), + connectionId, + ); + if (user_id) { + opts = { ...opts, assigned_to: [user_id] }; + } + } + + if (ticket.labels.nodes.length > 0) { + const tags = await this.ingestService.ingestData< + UnifiedTicketingTagOutput, + LinearTagOutput + >( + ticket.labels.nodes.map( + (label) => + ({ + name: label.name, + } as LinearTagOutput), + ), + 'linear', + connectionId, + 'ticketing', + TicketingObject.tag, + [], + ); + opts = { + ...opts, + tags: tags.map((tag) => tag.id_tcg_tag), + }; + } + + if (ticket.project) { + const tcg_collection_id = await this.utils.getCollectionUuidFromRemoteId( + String(ticket.project.id), + connectionId, + ); + if (tcg_collection_id) { + opts = { ...opts, collections: [tcg_collection_id] }; + } + } + + const unifiedTicket: UnifiedTicketingTicketOutput = { + remote_id: ticket.id, + remote_data: ticket, + name: ticket.title, + description: ticket.description || null, + due_date: ticket.dueDate ? new Date(ticket.dueDate) : null, + field_mappings, + ...opts, + }; + + return unifiedTicket; + } +} diff --git a/packages/api/src/ticketing/ticket/services/linear/types.ts b/packages/api/src/ticketing/ticket/services/linear/types.ts new file mode 100644 index 000000000..b50e7c306 --- /dev/null +++ b/packages/api/src/ticketing/ticket/services/linear/types.ts @@ -0,0 +1,33 @@ +import { LinearCommentInput } from "@ticketing/comment/services/linear/types" +import { LinearTagInput } from "@ticketing/tag/services/linear/types" + +interface LinearTicket { + id: string + title: string + description?: string + dueDate?: string + parent?: { + id: string + } + state: { + name: string + } + project?: any + labels?: { + nodes: LinearTagInput[] + } + completedAt?: any + priorityLabel?: string + assignee?: { + id: string + } + comments?: { + nodes: LinearCommentInput[] + } + team?: { + id: string + } +} + +export type LinearTicketInput = Partial; +export type LinearTicketOutput = Partial; diff --git a/packages/api/src/ticketing/ticket/ticket.module.ts b/packages/api/src/ticketing/ticket/ticket.module.ts index 7ba04aabe..d58ae8ea0 100644 --- a/packages/api/src/ticketing/ticket/ticket.module.ts +++ b/packages/api/src/ticketing/ticket/ticket.module.ts @@ -1,3 +1,5 @@ +import { LinearTicketMapper } from './services/linear/mappers'; +import { LinearService } from './services/linear'; import { GithubTicketMapper } from './services/github/mappers'; import { GithubService } from './services/github'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; @@ -42,6 +44,8 @@ import { TicketController } from './ticket.controller'; GitlabTicketMapper, GithubService, GithubTicketMapper, + LinearService, + LinearTicketMapper, ], exports: [SyncService, ServiceRegistry, WebhookService, IngestDataService], }) From 77851bd5f0340a9be0fe1062bf21b5165f14ceab Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 14 Aug 2024 00:19:58 +0530 Subject: [PATCH 09/12] fixing add linear comment func --- packages/api/src/ticketing/comment/services/linear/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/ticketing/comment/services/linear/index.ts b/packages/api/src/ticketing/comment/services/linear/index.ts index 4c642a346..bc7ff4dd6 100644 --- a/packages/api/src/ticketing/comment/services/linear/index.ts +++ b/packages/api/src/ticketing/comment/services/linear/index.ts @@ -58,10 +58,10 @@ export class LinearService implements ICommentService { )}`, }, }); - + this.logger.log(`Created linear comment !`); return { - data: resp.data.data.issueLabels.nodes, + data: resp.data.data.commentCreate.comment, message: 'Linear comment created', statusCode: 201, }; From 3055309d7b919f18466989f9122489152b335c25 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 14 Aug 2024 00:33:46 +0530 Subject: [PATCH 10/12] fixing linear issue sync --- packages/api/src/ticketing/ticket/services/linear/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/ticketing/ticket/services/linear/index.ts b/packages/api/src/ticketing/ticket/services/linear/index.ts index 724cce27a..c979de927 100644 --- a/packages/api/src/ticketing/ticket/services/linear/index.ts +++ b/packages/api/src/ticketing/ticket/services/linear/index.ts @@ -71,7 +71,7 @@ export class LinearService implements ITicketService { )}`, }, }); - + this.logger.log(`Created linear ticket !`); return { data: resp.data.data.issueCreate.issue, @@ -111,7 +111,7 @@ export class LinearService implements ITicketService { this.logger.log(`Synced linear tickets !`); return { - data: resp.data.data.users.nodes, + data: resp.data.data.issues.nodes, message: 'Linear tickets retrieved', statusCode: 200, }; From e7d95d3be11407665b731a13bfb698b1aec466b3 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 14 Aug 2024 13:08:41 +0530 Subject: [PATCH 11/12] fixing linear create issue func --- packages/api/src/ticketing/@lib/@utils/index.ts | 16 +++++++++++++++- .../ticketing/ticket/services/linear/index.ts | 10 +++++----- .../ticketing/ticket/services/linear/mappers.ts | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/api/src/ticketing/@lib/@utils/index.ts b/packages/api/src/ticketing/@lib/@utils/index.ts index 30f4dda2c..7dc10a419 100644 --- a/packages/api/src/ticketing/@lib/@utils/index.ts +++ b/packages/api/src/ticketing/@lib/@utils/index.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; @Injectable() export class Utils { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService) { } async fetchFileStreamFromURL(file_url: string) { return fs.createReadStream(file_url); @@ -27,6 +27,20 @@ export class Utils { } } + async getTeamRemoteIdFromUuid(uuid: string) { + try { + const res = await this.prisma.tcg_teams.findFirst({ + where: { + id_tcg_team: uuid, + }, + }); + if (!res) return undefined; + return res.remote_id; + } catch (error) { + throw error; + } + } + async getRemoteIdFromTagName(name: string, connection_id: string) { try { const res = await this.prisma.tcg_tags.findFirst({ diff --git a/packages/api/src/ticketing/ticket/services/linear/index.ts b/packages/api/src/ticketing/ticket/services/linear/index.ts index c979de927..c1f9bf0af 100644 --- a/packages/api/src/ticketing/ticket/services/linear/index.ts +++ b/packages/api/src/ticketing/ticket/services/linear/index.ts @@ -50,11 +50,11 @@ export class LinearService implements ITicketService { "issueCreateInput": { "title": ticketData.title, "description": ticketData.description, - "assigneeId": ticketData.assignee.id, - "parentId": ticketData.parent.id, - "labelIds": ticketData.labels.nodes, - "projectId": ticketData.project.id, - "sourceCommentId": ticketData.comments.nodes, + "assigneeId": ticketData.assignee?.id, + "parentId": ticketData.parent?.id, + "labelIds": ticketData.labels?.nodes, + "projectId": ticketData.project?.id, + "sourceCommentId": ticketData.comments?.nodes, "dueDate": ticketData.dueDate, "teamId": ticketData.team.id } diff --git a/packages/api/src/ticketing/ticket/services/linear/mappers.ts b/packages/api/src/ticketing/ticket/services/linear/mappers.ts index f4108a216..05cdd4639 100644 --- a/packages/api/src/ticketing/ticket/services/linear/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/linear/mappers.ts @@ -43,7 +43,8 @@ export class LinearTicketMapper implements ITicketMapper { title: source.name, description: source.description ? source.description : null, // Passing new Field to retreive repositroy info to add ticket to that repo - project: source.collections[0] as string + project: source.collections ? source.collections[0] as string : null, + team: { id: await this.utils.getTeamRemoteIdFromUuid(source.field_mappings["team_id"]) }, }; From 69369f39b46abf5aa92c3d2fb4bb6ef69593d715 Mon Sep 17 00:00:00 2001 From: Praneeth Shetty Date: Wed, 14 Aug 2024 13:24:34 +0530 Subject: [PATCH 12/12] fixing project mapping and adding dueDate --- packages/api/src/ticketing/ticket/services/linear/mappers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/api/src/ticketing/ticket/services/linear/mappers.ts b/packages/api/src/ticketing/ticket/services/linear/mappers.ts index 05cdd4639..6d209c556 100644 --- a/packages/api/src/ticketing/ticket/services/linear/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/linear/mappers.ts @@ -43,12 +43,11 @@ export class LinearTicketMapper implements ITicketMapper { title: source.name, description: source.description ? source.description : null, // Passing new Field to retreive repositroy info to add ticket to that repo - project: source.collections ? source.collections[0] as string : null, + project: { id: source.collections ? await this.utils.getCollectionRemoteIdFromUuid(source.collections[0] as string) : null }, team: { id: await this.utils.getTeamRemoteIdFromUuid(source.field_mappings["team_id"]) }, + dueDate: source.due_date.toISOString(), }; - - if (source.assigned_to && source.assigned_to.length > 0) { const data = await this.utils.getAsigneeRemoteIdFromUserUuid( source.assigned_to[0],