diff --git a/packages/api/package.json b/packages/api/package.json index 0b4d96e33..7a6ad2eaf 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -105,5 +105,6 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" - } + }, + "type": "module" } diff --git a/packages/api/scripts/connectorUpdate.js b/packages/api/scripts/connectorUpdate.js new file mode 100755 index 000000000..3cfe2aeeb --- /dev/null +++ b/packages/api/scripts/connectorUpdate.js @@ -0,0 +1,284 @@ +import { log } from 'console'; +import * as fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Function to scan the directory for new service directories +function scanDirectory(dir) { + const directories = fs + .readdirSync(dir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + return directories; +} + +function replaceRelativePaths(path) { + const pattern = /^\.\.\/src\//; + if (pattern.test(path)) { + return path.replace(pattern, '@'); + } + return path; +} + +// Function to generate import statements for new service types +function generateImportStatements(serviceNames, basePath, objectType) { + return serviceNames.map((serviceName) => { + const importPath = `${basePath}/${serviceName}/types`; + const name = + serviceName.substring(0, 1).toUpperCase() + + serviceName.substring(1) + + objectType; + return `import { ${name}Input, ${name}Output } from '${replaceRelativePaths( + importPath, + )}';`; + }); +} + +function updateTargetFile(file, importStatements, serviceNames, objectType) { + let fileContent = fs.readFileSync(file, 'utf8'); + + // Append the import statements + fileContent = importStatements.join('\n') + '\n\n' + fileContent; + + // Create updates for OriginalObjectTypeInput and OriginalObjectTypeOutput + serviceNames.forEach((serviceName) => { + const typeName = + serviceName.charAt(0).toUpperCase() + serviceName.slice(1) + objectType; // Assuming naming convention + const inputTypeName = `${typeName}Input`; + const outputTypeName = `${typeName}Output`; + + // Update OriginalObjectTypeInput + const inputRegex = new RegExp(`(export type Original${objectType}Input =)`); + if (inputRegex.test(fileContent)) { + fileContent = fileContent.replace(inputRegex, `$1\n | ${inputTypeName}`); + } else { + // If the type doesn't exist, add it + fileContent += `\nexport type Original${objectType}Input =\n | ${inputTypeName};\n`; + } + + // Update OriginalObjectTypeOutput + const outputRegex = new RegExp( + `(export type Original${objectType}Output =)`, + ); + if (outputRegex.test(fileContent)) { + fileContent = fileContent.replace( + outputRegex, + `$1\n | ${outputTypeName}`, + ); + } else { + // If the type doesn't exist, add it + fileContent += `\nexport type Original${objectType}Output =\n | ${outputTypeName};\n`; + } + }); + + fs.writeFileSync(file, fileContent); +} + +function readFileContents(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +// Function to update the contents of a file +function updateFileContents(filePath, newContents) { + fs.writeFileSync(filePath, newContents); +} + +function updateMappingsFile(mappingsFile, newServiceDirs, objectType) { + let fileContent = fs.readFileSync(mappingsFile, 'utf8'); + + // Identify where the existing content before the first import starts, to preserve any comments or metadata at the start of the file + const firstImportIndex = fileContent.indexOf('import '); + const beforeFirstImport = + firstImportIndex > -1 ? fileContent.substring(0, firstImportIndex) : ''; + + // Prepare sections of the file content for updates + const afterFirstImport = + firstImportIndex > -1 + ? fileContent.substring(firstImportIndex) + : fileContent; + + const mappingStartIndex = afterFirstImport.indexOf( + `export const ${objectType.toLowerCase()}UnificationMapping = {`, + ); + const beforeMappingObject = afterFirstImport.substring(0, mappingStartIndex); + const mappingObjectContent = afterFirstImport.substring(mappingStartIndex); + + let newImports = ''; + let newInstances = ''; + let newMappings = ''; + newServiceDirs.forEach((newServiceName) => { + const serviceNameCapitalized = + newServiceName.charAt(0).toUpperCase() + newServiceName.slice(1); + const mapperClassName = `${serviceNameCapitalized}${objectType}Mapper`; + const mapperInstanceName = `${newServiceName.toLowerCase()}${objectType}Mapper`; + + // Prepare the import statement and instance declaration + const importStatement = `import { ${mapperClassName} } from '../services/${newServiceName}/mappers';\n`; + const instanceDeclaration = `const ${mapperInstanceName} = new ${mapperClassName}();\n`; + const mappingEntry = ` ${newServiceName.toLowerCase()}: {\n unify: ${mapperInstanceName}.unify.bind(${mapperInstanceName}),\n desunify: ${mapperInstanceName}.desunify,\n },\n`; + + // Check and append new import if it's not already present + if (!fileContent.includes(importStatement)) { + newImports += importStatement; + } + + // Append instance declaration if not already present before the mapping object + if (!beforeMappingObject.includes(instanceDeclaration)) { + newInstances += instanceDeclaration; + } + + // Prepare and append new mapping entry if not already present in the mapping object + if (!mappingObjectContent.includes(` ${newServiceName}: {`)) { + newMappings += mappingEntry; + } + }); + + // Combine updates with the original sections of the file content + const updatedContentBeforeMapping = + beforeFirstImport + + newImports + + beforeMappingObject.trim() + + '\n\n' + + newInstances; + + // Update the mapping object content with new mappings + const insertionPoint = mappingObjectContent.lastIndexOf('};'); + const updatedMappingObjectContent = [ + mappingObjectContent.slice(0, insertionPoint), + newMappings, + mappingObjectContent.slice(insertionPoint), + ].join(''); + + // Reassemble the complete updated file content + const updatedFileContent = + updatedContentBeforeMapping + updatedMappingObjectContent; + + // Write the updated content back to the file + fs.writeFileSync(mappingsFile, updatedFileContent); +} + +// Function to extract the array from a file +function extractArrayFromFile(filePath, arrayName) { + const fileContents = readFileContents(filePath); + const regex = new RegExp(`export const ${arrayName} = \\[([^\\]]+)\\];`); + const match = fileContents.match(regex); + if (match) { + return match[1].split(',').map((item) => item.trim().replace(/['"]/g, '')); + } + return []; +} + +// Function to update the array in a file +function updateArrayInFile(filePath, arrayName, newArray) { + const fileContents = readFileContents(filePath); + const regex = new RegExp(`export const ${arrayName} = \\[([^\\]]+)\\];`); + const newContents = fileContents.replace( + regex, + `export const ${arrayName} = [${newArray + .map((item) => `'${item}'`) + .join(', ')}];`, + ); + updateFileContents(filePath, newContents); +} + +function updateModuleFile(moduleFile, newServiceDirs) { + let moduleFileContent = fs.readFileSync(moduleFile, 'utf8'); + + // Generate and insert new service imports + newServiceDirs.forEach((serviceName) => { + const serviceClass = + serviceName.charAt(0).toUpperCase() + serviceName.slice(1) + 'Service'; + const importStatement = `import { ${serviceClass} } from './services/${serviceName}';\n`; + if (!moduleFileContent.includes(importStatement)) { + moduleFileContent = importStatement + moduleFileContent; + } + + // Add new service to the providers array if not already present + const providerRegex = /providers: \[\n([\s\S]*?)\n \],/; + const match = moduleFileContent.match(providerRegex); + if (match && !match[1].includes(serviceClass)) { + const updatedProviders = match[1] + ` ${serviceClass},\n`; + moduleFileContent = moduleFileContent.replace( + providerRegex, + `providers: [\n${updatedProviders} ],`, + ); + } + }); + + fs.writeFileSync(moduleFile, moduleFileContent); +} + +// Main script logic +function updateObjectTypes(baseDir, objectType, vertical) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const servicesDir = path.join(__dirname, baseDir); + const targetFile = path.join( + __dirname, + `../src/@core/utils/types/original/original.${vertical}.ts`, + ); + + const newServiceDirs = scanDirectory(servicesDir); + // Extract the current provider arrays from providers.ts and enum.ts + const providersFilePath = path.join( + __dirname, + '../../shared/src/providers.ts', + ); + const enumFilePath = path.join(__dirname, '../../shared/src/enum.ts'); + const currentProviders = extractArrayFromFile( + providersFilePath, + `${vertical.toUpperCase()}_PROVIDERS`, + ); + const currentEnum = extractArrayFromFile(enumFilePath, `ProviderVertical`); + + // Compare the extracted arrays with the new service names + const newProviders = newServiceDirs.filter( + (service) => !currentProviders.includes(service), + ); + const newEnum = newServiceDirs.filter( + (service) => !currentEnum.includes(service), + ); + + // Add any new services to the arrays + const updatedProviders = [...currentProviders, ...newProviders]; + const updatedEnum = [...currentEnum, ...newEnum]; + + // Update the arrays in the files + updateArrayInFile( + providersFilePath, + `${vertical.toUpperCase()}_PROVIDERS`, + updatedProviders, + ); + updateArrayInFile( + enumFilePath, + `${vertical.toUpperCase()}Providers`, + updatedEnum, + ); + const moduleFile = path.join( + __dirname, + `../src/${vertical}/${objectType.toLowerCase()}/${objectType.toLowerCase()}.module.ts`, + ); + + updateModuleFile(moduleFile, newServiceDirs, servicesDir); + + // Path to the mappings file + const mappingsFile = path.join( + __dirname, + `../src/${vertical}/${objectType.toLowerCase()}/types/mappingsTypes.ts`, + ); + + // Call updateMappingsFile to update the mappings file with new services + updateMappingsFile(mappingsFile, newServiceDirs, objectType); + + // Continue with the rest of the updateObjectTypes function... + const importStatements = generateImportStatements( + newProviders, + baseDir, + objectType, + ); + updateTargetFile(targetFile, importStatements, newProviders, objectType); +} + +// Example usage for ticketing/team +updateObjectTypes('../src/ticketing/team/services', 'Team', 'ticketing'); +// Example usage for crm/contact +//updateObjectTypes('path/to/crm/contact/services', 'Contact', 'crm'); 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 613ff98ae..47b873dd9 100644 --- a/packages/api/src/@core/utils/types/original/original.ticketing.ts +++ b/packages/api/src/@core/utils/types/original/original.ticketing.ts @@ -1,19 +1,8 @@ import { - ZendeskCommentInput, - ZendeskTicketInput, - ZendeskUserInput, - ZendeskTicketOutput, - ZendeskCommentOutput, - ZendeskUserOutput, - ZendeskAccountInput, - ZendeskAccountOutput, - ZendeskContactInput, - ZendeskContactOutput, - ZendeskTagInput, - ZendeskTagOutput, - ZendeskTeamInput, - ZendeskTeamOutput, -} from '@ticketing/@utils/@types'; + ClickupTeamInput, + ClickupTeamOutput, +} from '@ticketing/team/services/clickup/types'; + import { FrontAccountInput, FrontAccountOutput, @@ -129,8 +118,35 @@ import { JiraTagInput, JiraTagOutput, } from '@ticketing/tag/services/jira/types'; -import { GorgiasCollectionOutput } from '@ticketing/collection/services/gorgias/types'; import { JiraCollectionOutput } from '@ticketing/collection/services/jira/types'; +import { + ZendeskTicketInput, + ZendeskTicketOutput, +} from '@ticketing/ticket/services/zendesk/types'; +import { + ZendeskCommentInput, + ZendeskCommentOutput, +} from '@ticketing/comment/services/zendesk/types'; +import { + ZendeskAccountInput, + ZendeskAccountOutput, +} from '@ticketing/account/services/zendesk/types'; +import { + ZendeskTeamInput, + ZendeskTeamOutput, +} from '@ticketing/team/services/zendesk/types'; +import { + ZendeskTagInput, + ZendeskTagOutput, +} from '@ticketing/tag/services/zendesk/types'; +import { + ZendeskContactInput, + ZendeskContactOutput, +} from '@ticketing/contact/services/zendesk/types'; +import { + ZendeskUserInput, + ZendeskUserOutput, +} from '@ticketing/user/services/zendesk/types'; /* INPUT */ @@ -182,6 +198,7 @@ export type OriginalTagInput = | JiraTagInput; /* team */ export type OriginalTeamInput = + | ClickupTeamInput | ZendeskTeamInput | GithubTeamInput | FrontTeamInput @@ -251,6 +268,7 @@ export type OriginalTagOutput = /* team */ export type OriginalTeamOutput = + | ClickupTeamOutput | ZendeskTeamOutput | GithubTeamOutput | FrontTeamOutput @@ -267,9 +285,7 @@ export type OriginalAttachmentOutput = /* collection */ -export type OriginalCollectionOutput = - | GorgiasCollectionOutput - | JiraCollectionOutput; +export type OriginalCollectionOutput = JiraCollectionOutput; export type TicketingObjectOutput = | OriginalTicketOutput diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index dc8ba2022..61b15b6b6 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -18,7 +18,6 @@ import { CoreModule } from '@@core/core.module'; import { BullModule } from '@nestjs/bull'; import { TicketingModule } from '@ticketing/ticketing.module'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; -import { ProjectModule } from './project/project.module'; @Module({ imports: [ @@ -68,7 +67,6 @@ import { ProjectModule } from './project/project.module'; port: 6379, }, }), - ProjectModule, ], controllers: [AppController], providers: [ diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index f124bbd36..3952c49c3 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -105,12 +105,3 @@ export type ITicketingService = | ITeamService | ITagService | ICollectionService; - -/*TODO: export all providers */ -export * from '../../ticket/services/zendesk/types'; -export * from '../../comment/services/zendesk/types'; -export * from '../../user/services/zendesk/types'; -export * from '../../contact/services/zendesk/types'; -export * from '../../account/services/zendesk/types'; -export * from '../../team/services/zendesk/types'; -export * from '../../tag/services/zendesk/types'; diff --git a/packages/api/src/ticketing/team/services/clickup/index.ts b/packages/api/src/ticketing/team/services/clickup/index.ts new file mode 100644 index 000000000..e97b13e4f --- /dev/null +++ b/packages/api/src/ticketing/team/services/clickup/index.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { TicketingObject } from '@ticketing/@utils/@types'; +import { ApiResponse } from '@@core/utils/types'; +import axios from 'axios'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { ServiceRegistry } from '../registry.service'; +import { ITeamService } from '@ticketing/team/types'; +import { ClickupTeamOutput } from './types'; + +@Injectable() +export class ClickupService implements ITeamService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + TicketingObject.team.toUpperCase() + ':' + ClickupService.name, + ); + this.registry.registerService('front', this); + } + + async syncTeams( + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'front', + }, + }); + + const resp = await axios.get('https://api2.frontapp.com/teams', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced front teams !`); + + return { + data: resp.data._results, + message: 'Clickup teams retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Clickup', + TicketingObject.team, + ActionType.GET, + ); + } + } +} diff --git a/packages/api/src/ticketing/team/services/clickup/mappers.ts b/packages/api/src/ticketing/team/services/clickup/mappers.ts new file mode 100644 index 000000000..afe83bb61 --- /dev/null +++ b/packages/api/src/ticketing/team/services/clickup/mappers.ts @@ -0,0 +1,38 @@ +import { ITeamMapper } from '@ticketing/team/types'; +import { ClickupTeamInput, ClickupTeamOutput } from './types'; +import { + UnifiedTeamInput, + UnifiedTeamOutput, +} from '@ticketing/team/types/model.unified'; + +export class ClickupTeamMapper implements ITeamMapper { + desunify( + source: UnifiedTeamInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): ClickupTeamInput { + return; + } + + unify( + source: ClickupTeamOutput | ClickupTeamOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput | UnifiedTeamOutput[] { + return; + } + + private mapSingleTeamToUnified( + team: ClickupTeamOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedTeamOutput { + return; + } +} diff --git a/packages/api/src/ticketing/team/services/clickup/types.ts b/packages/api/src/ticketing/team/services/clickup/types.ts new file mode 100644 index 000000000..8274999b7 --- /dev/null +++ b/packages/api/src/ticketing/team/services/clickup/types.ts @@ -0,0 +1,7 @@ +export type ClickupTeamInput = { + id: string; +}; + +export type ClickupTeamOutput = { + id: string; +}; diff --git a/packages/api/src/ticketing/team/team.module.ts b/packages/api/src/ticketing/team/team.module.ts index c24f4403d..76839eb6a 100644 --- a/packages/api/src/ticketing/team/team.module.ts +++ b/packages/api/src/ticketing/team/team.module.ts @@ -1,3 +1,4 @@ +import { ClickupService } from './services/clickup'; import { Module } from '@nestjs/common'; import { TeamController } from './team.controller'; import { SyncService } from './sync/sync.service'; @@ -37,6 +38,7 @@ import { GorgiasService } from './services/gorgias'; GithubService, JiraService, GorgiasService, + ClickupService, ], exports: [SyncService], }) diff --git a/packages/api/src/ticketing/team/types/mappingsTypes.ts b/packages/api/src/ticketing/team/types/mappingsTypes.ts index 2e8f2e22c..37cf9e808 100644 --- a/packages/api/src/ticketing/team/types/mappingsTypes.ts +++ b/packages/api/src/ticketing/team/types/mappingsTypes.ts @@ -1,3 +1,5 @@ +import { ClickupTeamMapper } from '../services/clickup/mappers'; +import { JiraTeamMapper } from '../services/jira/mappers'; import { FrontTeamMapper } from '../services/front/mappers'; import { GithubTeamMapper } from '../services/github/mappers'; import { GorgiasTeamMapper } from '../services/gorgias/mappers'; @@ -8,6 +10,8 @@ const frontTeamMapper = new FrontTeamMapper(); const githubTeamMapper = new GithubTeamMapper(); const gorgiasTeamMapper = new GorgiasTeamMapper(); +const clickupTeamMapper = new ClickupTeamMapper(); +const jiraTeamMapper = new JiraTeamMapper(); export const teamUnificationMapping = { zendesk_tcg: { unify: zendeskTeamMapper.unify.bind(zendeskTeamMapper), @@ -25,4 +29,16 @@ export const teamUnificationMapping = { unify: gorgiasTeamMapper.unify.bind(gorgiasTeamMapper), desunify: gorgiasTeamMapper.desunify, }, + clickup: { + unify: clickupTeamMapper.unify.bind(clickupTeamMapper), + desunify: clickupTeamMapper.desunify, + }, + jira: { + unify: jiraTeamMapper.unify.bind(jiraTeamMapper), + desunify: jiraTeamMapper.desunify, + }, + zendesk: { + unify: zendeskTeamMapper.unify.bind(zendeskTeamMapper), + desunify: zendeskTeamMapper.desunify, + }, }; diff --git a/packages/shared/src/enum.ts b/packages/shared/src/enum.ts index f160d7802..3e1cd4a51 100644 --- a/packages/shared/src/enum.ts +++ b/packages/shared/src/enum.ts @@ -23,7 +23,7 @@ export enum TicketingProviders { FRONT = 'front', GITHUB = 'github', JIRA = 'jira', - LINEAR = 'linear', + GORGIAS = 'gorgias', } export enum AccountingProviders { diff --git a/packages/shared/src/providers.ts b/packages/shared/src/providers.ts index 1f8fac1e7..1880a421f 100644 --- a/packages/shared/src/providers.ts +++ b/packages/shared/src/providers.ts @@ -14,15 +14,14 @@ export const CRM_PROVIDERS = [ export const HRIS_PROVIDERS = ['']; export const ATS_PROVIDERS = ['']; export const ACCOUNTING_PROVIDERS = ['']; -export const TICKETING_PROVIDERS = ['zendesk', 'front', 'github', 'jira', 'linear']; +export const TICKETING_PROVIDERS = ['zendesk', 'front', 'github', 'jira', 'gorgias']; export const MARKETING_AUTOMATION_PROVIDERS = ['']; export const FILE_STORAGE_PROVIDERS = ['']; export function getProviderVertical(providerName: string): ProviderVertical { if (CRM_PROVIDERS.includes(providerName)) { - return ProviderVertical.CRM; - } + return ProviderVertical.CRM; } if (HRIS_PROVIDERS.includes(providerName)) { return ProviderVertical.HRIS; } diff --git a/packages/shared/src/standardObjects.ts b/packages/shared/src/standardObjects.ts index a46878a4a..ddc486460 100644 --- a/packages/shared/src/standardObjects.ts +++ b/packages/shared/src/standardObjects.ts @@ -28,6 +28,7 @@ export enum TicketingObject { account = 'account', tag = 'tag', team = 'team', + collection = 'collection' } // Utility function to prepend prefix to enum values // eslint-disable-next-line @typescript-eslint/no-explicit-any