diff --git a/apps/frontend-snippet/src/hooks/queries/useLinkedUser.tsx b/apps/frontend-snippet/src/hooks/queries/useLinkedUser.tsx index 62d5e0e33..157d7eee2 100644 --- a/apps/frontend-snippet/src/hooks/queries/useLinkedUser.tsx +++ b/apps/frontend-snippet/src/hooks/queries/useLinkedUser.tsx @@ -6,7 +6,7 @@ const useLinkedUser = (id: string) => { return useQuery({ queryKey: ['linked-users', id], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/linked-users?id=${id}`); + const response = await fetch(`${config.API_URL}/linked-users/single?id=${id}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/apps/frontend-snippet/src/hooks/queries/useUniqueMagicLink.tsx b/apps/frontend-snippet/src/hooks/queries/useUniqueMagicLink.tsx index 05da74285..f46ef44f8 100644 --- a/apps/frontend-snippet/src/hooks/queries/useUniqueMagicLink.tsx +++ b/apps/frontend-snippet/src/hooks/queries/useUniqueMagicLink.tsx @@ -6,7 +6,7 @@ const useUniqueMagicLink = (id: string) => { return useQuery({ queryKey: ['magic-link', id], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/magic-link?id=${id}`); + const response = await fetch(`${config.API_URL}/magic-link/single?id=${id}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/apps/frontend-snippet/src/lib/ProviderModal.tsx b/apps/frontend-snippet/src/lib/ProviderModal.tsx index 928505678..a0f8fe176 100644 --- a/apps/frontend-snippet/src/lib/ProviderModal.tsx +++ b/apps/frontend-snippet/src/lib/ProviderModal.tsx @@ -50,10 +50,12 @@ const ProviderModal = () => { } }, []); - + const {data: magicLink} = useUniqueMagicLink(uniqueMagicLinkId); const {data: linkedUser} = useLinkedUser(magicLink?.id_linked_user as string); + + //TODO: externalize that in the backend => from const { open, isReady } = useOAuth({ providerName: selectedProvider, // This will be set when a provider is clicked diff --git a/apps/webapp/src/components/configuration/components/FieldMappingModal.tsx b/apps/webapp/src/components/configuration/components/FieldMappingModal.tsx index 29f6d7fa3..f834d4b01 100644 --- a/apps/webapp/src/components/configuration/components/FieldMappingModal.tsx +++ b/apps/webapp/src/components/configuration/components/FieldMappingModal.tsx @@ -29,7 +29,6 @@ import useDefineFieldMutation from "@/hooks/mutations/useDefineFieldMutation" import useMapFieldMutation from "@/hooks/mutations/useMapFieldMutation" import { useEffect, useState } from "react" import useFieldMappings from "@/hooks/useFieldMappings" -import { useStandardObjects } from "@/hooks/useStandardObjects" import useProviderProperties from "@/hooks/useProviderProperties" export function FModal({ onClose }: {onClose: () => void}) { @@ -50,11 +49,18 @@ export function FModal({ onClose }: {onClose: () => void}) { const { mutate: mutateMapField } = useMapFieldMutation(); const { data: sourceCustomFields, error, isLoading } = useProviderProperties(linkedUserId,sourceProvider,standardModel); - const { data: sObjects } = useStandardObjects(); - + //const { data: sObjects } = useStandardObjects(); + //TODO: get this from shared types + const sObjects = [ + 'contact', + 'note', + 'task' + ] useEffect(() => { - if (sourceCustomFields && !isLoading && !error) { - setSourceCustomFieldsData(sourceCustomFields); + if (sourceCustomFields && sourceCustomFields.data.length > 0 && !isLoading && !error) { + console.log("inside custom fields properties "); + + setSourceCustomFieldsData(sourceCustomFields.data); } }, [sourceCustomFields, isLoading, error]); @@ -72,7 +78,7 @@ export function FModal({ onClose }: {onClose: () => void}) { const handleMapSubmit = (e: React.FormEvent) => { e.preventDefault(); mutateMapField({ - attributeId: attributeId, + attributeId: attributeId.trim(), source_custom_field_id: sourceCustomFieldId, source_provider: sourceProvider, linked_user_id: linkedUserId, @@ -109,7 +115,7 @@ export function FModal({ onClose }: {onClose: () => void}) { {sObjects && sObjects .map(sObject => ( - {sObject.ressource_owner_id} + {sObject} )) } @@ -225,7 +231,7 @@ export function FModal({ onClose }: {onClose: () => void}) { {sourceCustomFieldsData.map(field => ( - {field.name} + {field.name} ))} diff --git a/apps/webapp/src/components/connections/ConnectionTable.tsx b/apps/webapp/src/components/connections/ConnectionTable.tsx index d7763caca..d29b9d6b2 100644 --- a/apps/webapp/src/components/connections/ConnectionTable.tsx +++ b/apps/webapp/src/components/connections/ConnectionTable.tsx @@ -25,19 +25,17 @@ export default function ConnectionTable() { const {uniqueLink} = useMagicLinkStore(); if (isLoading) { - console.log("loading connections.."); - } - - if (error) { - console.log("error connections.."); - } - if (!connections) { return
Connections not found....
; } - const linkedConnections = (filter: string) => connections.filter((connection) => connection.status == filter); + + if (error) { + console.log("error connections.."); + } + + const linkedConnections = (filter: string) => connections?.filter((connection) => connection.status == filter); const ts = connections?.map((connection) => ({ organisation: connection.id_project, // replace with actual mapping @@ -58,7 +56,7 @@ export default function ConnectionTable() { Linked -

{linkedConnections("0").length}

+

{linkedConnections("0")?.length}

@@ -67,7 +65,7 @@ export default function ConnectionTable() { Incomplete Link -

{linkedConnections("1").length}

+

{linkedConnections("1")?.length}

@@ -75,7 +73,7 @@ export default function ConnectionTable() { Relink Needed -

{linkedConnections("2").length}

+

{linkedConnections("2")?.length}

@@ -102,7 +100,7 @@ export default function ConnectionTable() { - diff --git a/apps/webapp/src/components/jobs/JobsTable.tsx b/apps/webapp/src/components/jobs/JobsTable.tsx index 36b818620..4648bd475 100644 --- a/apps/webapp/src/components/jobs/JobsTable.tsx +++ b/apps/webapp/src/components/jobs/JobsTable.tsx @@ -2,7 +2,7 @@ import { columns } from "./components/columns" import { DataTable } from "../shared/data-table" import useJobs from "@/hooks/useJobs"; import { DataTableLoading } from "../shared/data-table-loading"; -import { jobs as Job } from "api"; +import { events as Job } from "api"; export default function JobsTable() { const { data: jobs, isLoading, error } = useJobs(); diff --git a/apps/webapp/src/hooks/useJobs.tsx b/apps/webapp/src/hooks/useJobs.tsx index 7bb913c98..7683da956 100644 --- a/apps/webapp/src/hooks/useJobs.tsx +++ b/apps/webapp/src/hooks/useJobs.tsx @@ -1,6 +1,6 @@ import config from '@/utils/config'; import { useQuery } from '@tanstack/react-query'; -import { jobs as Job } from 'api'; +import { events as Job } from 'api'; const fetchJobs = async (): Promise => { const response = await fetch(`${config.API_URL}/jobs`); diff --git a/apps/webapp/src/hooks/useProviderProperties.tsx b/apps/webapp/src/hooks/useProviderProperties.tsx index aee71392e..3ed1013d1 100644 --- a/apps/webapp/src/hooks/useProviderProperties.tsx +++ b/apps/webapp/src/hooks/useProviderProperties.tsx @@ -5,9 +5,8 @@ import { getProviderVertical } from 'shared-types'; const useProviderProperties = (linkedUserId: string, providerId: string, standardObject: string) => { return useQuery({ queryKey: ['providerProperties', linkedUserId, providerId, standardObject], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - queryFn: async (): Promise[]> => { - const response = await fetch(`${config.API_URL}/${getProviderVertical(providerId).toLowerCase()}/${standardObject.toLowerCase()}/properties?linkedUserId=${linkedUserId}&providerId=${providerId}`); + queryFn: async () => { + const response = await fetch(`${config.API_URL}/${getProviderVertical(providerId).toLowerCase()}/contact/properties?linkedUserId=${linkedUserId}&providerId=${providerId}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/packages/api/package.json b/packages/api/package.json index 253bc3e84..beb1f3b3c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -51,8 +51,7 @@ "pino-pretty": "^10.2.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "uuid": "^9.0.1", - "shared-types": "workspace:*" + "uuid": "^9.0.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 7fd4decd1..58f1457fd 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -25,43 +25,31 @@ model crm_contacts { last_name String created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) - id_job String @db.Uuid + origin String + origin_id String id_crm_user String? @db.Uuid + id_event String @db.Uuid crm_addresses crm_addresses[] crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_23") - jobs jobs @relation(fields: [id_job], references: [id_job], onDelete: NoAction, onUpdate: NoAction, map: "job_id_crm_contact") + events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "job_id_crm_contact") crm_email_addresses crm_email_addresses[] crm_notes crm_notes[] crm_phone_numbers crm_phone_numbers[] - @@index([id_job], map: "crm_contact_id_job") + @@index([id_event], map: "crm_contact_id_job") @@index([id_crm_user], map: "fk_crm_contact_userid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model jobs { - id_job String @id(map: "pk_jobs") @db.Uuid - status String - timestamp DateTime @default(now()) @db.Timestamp(6) - id_linked_user String @db.Uuid - crm_companies crm_companies[] - crm_contacts crm_contacts[] - linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") - jobs_status_history jobs_status_history[] - - @@index([id_linked_user], map: "fk_linkeduserid_projectid") -} - /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model jobs_status_history { id_jobs_status_history String @id(map: "pk_jobs_status_history") @db.Uuid timestamp DateTime @default(now()) @db.Timestamp(6) previous_status String new_status String - id_job String @db.Uuid - jobs jobs @relation(fields: [id_job], references: [id_job], onDelete: NoAction, onUpdate: NoAction, map: "fk_4") + id_event String @db.Uuid + events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_4") - @@index([id_job], map: "id_job_jobs_status_history") + @@index([id_event], map: "id_job_jobs_status_history") } model organizations { @@ -76,6 +64,8 @@ model projects { id_project String @id(map: "pk_projects") @db.Uuid name String id_organization String @db.Uuid + sync_mode String + pull_frequency BigInt? api_keys api_keys[] connections connections[] linked_users linked_users[] @@ -128,8 +118,8 @@ model linked_users { alias String id_project String @db.Uuid connections connections[] + events events[] invite_links invite_links[] - jobs jobs[] projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_10") @@index([id_project], map: "fk_proectid_linked_users") @@ -176,10 +166,10 @@ model crm_companies { number_of_employees BigInt? created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) - id_job String @db.Uuid id_crm_user String? @db.Uuid + id_event String @db.Uuid crm_addresses crm_addresses[] - jobs jobs @relation(fields: [id_job], references: [id_job], onDelete: NoAction, onUpdate: NoAction, map: "fk_13") + events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_13") crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_24") crm_email_addresses crm_email_addresses[] crm_engagements crm_engagements[] @@ -188,7 +178,7 @@ model crm_companies { crm_tasks crm_tasks[] @@index([id_crm_user], map: "fk_crm_company_crm_userid") - @@index([id_job], map: "fk_crm_company_jobid") + @@index([id_event], map: "fk_crm_company_jobid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -307,10 +297,10 @@ model attribute { data_type String remote_id String source String - id_entity String @db.Uuid + id_entity String? @db.Uuid scope String id_consumer String? @db.Uuid - entity entity @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_32") + entity entity? @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_32") value value[] @@index([id_entity], map: "fk_attribute_entityid") @@ -319,7 +309,7 @@ model attribute { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model entity { id_entity String @id(map: "pk_entity") @db.Uuid - ressource_owner_id String + ressource_owner_id String @db.Uuid attribute attribute[] value value[] } @@ -380,3 +370,19 @@ model invite_links { @@index([id_linked_user], map: "fk_invite_link_linkeduserid") } + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model events { + id_event String @id(map: "pk_jobs") @db.Uuid + status String + type String + direction String + timestamp DateTime @default(now()) @db.Timestamp(6) + id_linked_user String @db.Uuid + crm_companies crm_companies[] + crm_contacts crm_contacts[] + linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") + jobs_status_history jobs_status_history[] + + @@index([id_linked_user], map: "fk_linkeduserid_projectid") +} diff --git a/packages/api/scripts/seed.ts b/packages/api/scripts/seed.ts index 2de8bb00f..612ec30cd 100644 --- a/packages/api/scripts/seed.ts +++ b/packages/api/scripts/seed.ts @@ -27,6 +27,7 @@ async function main() { id_project: uuidv4(), name: `Project ${index + 1}`, id_organization: org.id_organization, + sync_mode: 'pool', })); await prisma.projects.createMany({ @@ -34,7 +35,7 @@ async function main() { }); // Seed the `linked_users` table with 10 linked users - const linkedUsersData = Array.from({ length: 10 }).map((_, index) => ({ + /*const linkedUsersData = Array.from({ length: 10 }).map((_, index) => ({ id_linked_user: uuidv4(), linked_user_origin_id: `acme_origin_id_${uuidv4()}`, alias: `Acme Inc`, @@ -186,13 +187,15 @@ async function main() { // Seed the `jobs` table with 20 jobs const jobsData = Array.from({ length: 10 }).map((_, index) => ({ - id_job: uuidv4(), // Generate a new UUID for each job + id_event: uuidv4(), // Generate a new UUID for each job status: 'initialized', // Use whatever status is appropriate + type: 'pull', + direction: '0', timestamp: new Date(), id_linked_user: linkedUsersData[index].id_linked_user, })); - const jobs = await prisma.jobs.createMany({ + const jobs = await prisma.events.createMany({ data: jobsData, skipDuplicates: true, // Set to true to ignore conflicts (optional) }); diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 52e1a9d94..82524cdca 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -4,7 +4,7 @@ import { CrmConnectionsService } from './crm/services/crm-connection.service'; import { LoggerService } from '@@core/logger/logger.service'; import { handleServiceError } from '@@core/utils/errors'; import { PrismaService } from '@@core/prisma/prisma.service'; -import { ProviderVertical, getProviderVertical } from 'shared-types'; +import { ProviderVertical, getProviderVertical } from '@@core/utils/types'; @Controller('connections') export class ConnectionsController { constructor( diff --git a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts index 86be1820d..5b806fd96 100644 --- a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts +++ b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts @@ -17,7 +17,7 @@ export class HubspotConnectionService { constructor(private prisma: PrismaService, private logger: LoggerService) { this.logger.setContext(HubspotConnectionService.name); } - async addLinkedUserAndProjectTest() { + /*async addLinkedUserAndProjectTest() { const newOrganization = { id_organization: uuidv4(), name: 'New Organization', @@ -34,6 +34,7 @@ export class HubspotConnectionService { id_project: uuidv4(), name: 'New Project', id_organization: newOrganization.id_organization, + sync_mode: 'pool', }; const data1 = await this.prisma.projects.create({ data: newProject, @@ -51,7 +52,7 @@ export class HubspotConnectionService { data: newLinkedUser, }); this.logger.log('Added new linked_user ' + data); - } + }*/ async handleHubspotCallback( linkedUserId: string, diff --git a/packages/api/src/@core/field-mapping/field-mapping.controller.ts b/packages/api/src/@core/field-mapping/field-mapping.controller.ts index 4a7b8e3c3..58cb4ad46 100644 --- a/packages/api/src/@core/field-mapping/field-mapping.controller.ts +++ b/packages/api/src/@core/field-mapping/field-mapping.controller.ts @@ -19,19 +19,19 @@ export class FieldMappingController { this.logger.setContext(FieldMappingController.name); } - @Post('addObjectEntity') + /*@Post('addObjectEntity') addStandardObjectEntity(@Body() dto: StandardObjectDto) { return this.fieldMappingService.addStandardObjectEntity( dto.standardObjectName, ); - } + }*/ - @Get('getObjectEntity') + /*@Get('getObjectEntity') getStandardObjectEntity(@Query('object') standardObjectName: string) { return this.fieldMappingService.getEntityId( standardObjectName as StandardObject, ); - } + }*/ @Get('entities') getEntities() { diff --git a/packages/api/src/@core/field-mapping/field-mapping.service.ts b/packages/api/src/@core/field-mapping/field-mapping.service.ts index f6e0cdf54..41974adb2 100644 --- a/packages/api/src/@core/field-mapping/field-mapping.service.ts +++ b/packages/api/src/@core/field-mapping/field-mapping.service.ts @@ -14,18 +14,6 @@ export class FieldMappingService { this.logger.setContext(FieldMappingService.name); } - /* UTILS */ - // create a set of entities inside our db and save their entity id - async addStandardObjectEntity(standardObjectName: string) { - const entity = await this.prisma.entity.create({ - data: { - id_entity: uuidv4(), - ressource_owner_id: standardObjectName, - }, - }); - return entity; - } - async getAttributes() { return await this.prisma.attribute.findMany(); } @@ -39,14 +27,14 @@ export class FieldMappingService { } // and then retrieve them by their name - async getEntityId(standardObject: StandardObject) { + /*async getEntityId(standardObject: StandardObject) { const res = await this.prisma.entity.findFirst({ where: { ressource_owner_id: standardObject as string, }, }); return res.id_entity; - } + }*/ async getCustomFieldMappings( integrationId: string, @@ -57,9 +45,7 @@ export class FieldMappingService { where: { source: integrationId, id_consumer: linkedUserId, - entity: { - ressource_owner_id: standard_object, - }, + ressource_owner_type: standard_object, }, select: { remote_id: true, @@ -70,8 +56,8 @@ export class FieldMappingService { async defineTargetField(dto: DefineTargetFieldDto) { // Create a new attribute in your system representing the target field - const id_entity = await this.getEntityId(dto.object_type_owner); - this.logger.log('id entity is ' + id_entity); + //const id_entity = await this.getEntityId(dto.object_type_owner); + //this.logger.log('id entity is ' + id_entity); const attribute = await this.prisma.attribute.create({ data: { id_attribute: uuidv4(), @@ -83,9 +69,8 @@ export class FieldMappingService { // below is done in step 2 remote_id: '', source: '', - id_entity: id_entity, + //id_entity: id_entity, scope: 'user', // [user | org] wide - //id_consumer: '00000000-0000-0000-0000-000000000000', //default }, }); @@ -93,32 +78,22 @@ export class FieldMappingService { } async mapFieldToProvider(dto: MapFieldToProviderDto) { - // todo: include a value inside value table as mapping is done here - const updatedAttribute = await this.prisma.attribute.update({ - where: { - id_attribute: dto.attributeId, - }, - data: { - remote_id: dto.source_custom_field_id, - source: dto.source_provider, - id_consumer: dto.linked_user_id, - status: 'mapped', - }, - }); - - return updatedAttribute; - - //insert inside the table value - - /*const valueInserted = await this.prisma.value.create({ - data: { - id_value: uuidv4(), - data: dto.data, - id_entity: updatedAttribute.id_entity, - id_attribute: dto.attributeId, - }, - }); + try { + const updatedAttribute = await this.prisma.attribute.update({ + where: { + id_attribute: dto.attributeId.trim(), + }, + data: { + remote_id: dto.source_custom_field_id, + source: dto.source_provider, + id_consumer: dto.linked_user_id.trim(), + status: 'mapped', + }, + }); - return updatedAttribute;*/ + return updatedAttribute; + } catch (error) { + throw new Error(error); + } } } diff --git a/packages/api/src/@core/jobs/jobs.controller.ts b/packages/api/src/@core/jobs/jobs.controller.ts index 63f3950ca..9701395dd 100644 --- a/packages/api/src/@core/jobs/jobs.controller.ts +++ b/packages/api/src/@core/jobs/jobs.controller.ts @@ -15,6 +15,6 @@ export class JobsController { @Get() async getJobs() { - return await this.prisma.jobs.findMany(); + return await this.prisma.events.findMany(); } } diff --git a/packages/api/src/@core/linked-users/linked-users.controller.ts b/packages/api/src/@core/linked-users/linked-users.controller.ts index 297ce2d42..ef1a84f11 100644 --- a/packages/api/src/@core/linked-users/linked-users.controller.ts +++ b/packages/api/src/@core/linked-users/linked-users.controller.ts @@ -20,7 +20,7 @@ export class LinkedUsersController { getLinkedUsers() { return this.linkedUsersService.getLinkedUsers(); } - @Get() + @Get('single') getLinkedUser(@Query('id') id: string) { return this.linkedUsersService.getLinkedUser(id); } diff --git a/packages/api/src/@core/linked-users/linked-users.service.ts b/packages/api/src/@core/linked-users/linked-users.service.ts index 79fc65479..6f76f2291 100644 --- a/packages/api/src/@core/linked-users/linked-users.service.ts +++ b/packages/api/src/@core/linked-users/linked-users.service.ts @@ -13,11 +13,15 @@ export class LinkedUsersService { return await this.prisma.linked_users.findMany(); } async getLinkedUser(id: string) { - return await this.prisma.linked_users.findFirst({ - where: { - id_linked_user: id, - }, - }); + try { + return await this.prisma.linked_users.findFirst({ + where: { + id_linked_user: id, + }, + }); + } catch (error) { + throw new Error(error); + } } async addLinkedUser(data: CreateLinkedUserDto) { const { id_project, ...rest } = data; diff --git a/packages/api/src/@core/magic-link/magic-link.controller.ts b/packages/api/src/@core/magic-link/magic-link.controller.ts index e533e2c44..f9c265e40 100644 --- a/packages/api/src/@core/magic-link/magic-link.controller.ts +++ b/packages/api/src/@core/magic-link/magic-link.controller.ts @@ -22,7 +22,7 @@ export class MagicLinkController { return this.magicLinkService.getMagicLinks(); } - @Get() + @Get('single') getMagicLink(@Query('id') id: string) { return this.magicLinkService.getMagicLink(id); } diff --git a/packages/api/src/@core/magic-link/magic-link.service.ts b/packages/api/src/@core/magic-link/magic-link.service.ts index 7d3d780b3..c06ec3afb 100644 --- a/packages/api/src/@core/magic-link/magic-link.service.ts +++ b/packages/api/src/@core/magic-link/magic-link.service.ts @@ -15,11 +15,15 @@ export class MagicLinkService { } async getMagicLink(id: string) { - return await this.prisma.invite_links.findFirst({ - where: { - id_invite_link: id, - }, - }); + try { + return await this.prisma.invite_links.findFirst({ + where: { + id_invite_link: id, + }, + }); + } catch (error) { + throw new Error(error); + } } async createUniqueLink(data: CreateMagicLinkDto) { diff --git a/packages/api/src/@core/passthrough/passthrough.module.ts b/packages/api/src/@core/passthrough/passthrough.module.ts index 6a6254070..d3ff5006d 100644 --- a/packages/api/src/@core/passthrough/passthrough.module.ts +++ b/packages/api/src/@core/passthrough/passthrough.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { PassthroughService } from './passthrough.service'; import { PassthroughController } from './passthrough.controller'; import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; @Module({ - providers: [PassthroughService, LoggerService], + providers: [PassthroughService, LoggerService, PrismaService], controllers: [PassthroughController], }) export class PassthroughModule {} diff --git a/packages/api/src/@core/passthrough/passthrough.service.ts b/packages/api/src/@core/passthrough/passthrough.service.ts index 23bc188db..112a4e22f 100644 --- a/packages/api/src/@core/passthrough/passthrough.service.ts +++ b/packages/api/src/@core/passthrough/passthrough.service.ts @@ -1,12 +1,11 @@ import { Injectable } from '@nestjs/common'; import { PassThroughRequestDto } from './dto/passthrough.dto'; -import { domains } from '@@core/utils/types'; +import { domains, getProviderVertical } from '@@core/utils/types'; import { PassThroughResponse } from './types'; import axios, { AxiosResponse } from 'axios'; import { PrismaService } from '@@core/prisma/prisma.service'; import { v4 as uuidv4 } from 'uuid'; import { decrypt } from '@@core/utils/crypto'; -import { getProviderVertical } from 'shared-types'; @Injectable() export class PassthroughService { @@ -17,11 +16,14 @@ export class PassthroughService { integrationId: string, linkedUserId: string, ): Promise { - const job_resp_create = await this.prisma.jobs.create({ + const job_resp_create = await this.prisma.events.create({ data: { - id_job: uuidv4(), + id_event: uuidv4(), // Generate a new UUID for each job + status: 'initialized', // Use whatever status is appropriate + type: 'pull', + direction: '0', + timestamp: new Date(), id_linked_user: linkedUserId, - status: 'initialized', }, }); const { method, path, data, headers } = requestParams; @@ -51,9 +53,9 @@ export class PassthroughService { console.error(error); throw error; } - const job_resp_update = await this.prisma.jobs.update({ + const job_resp_update = await this.prisma.events.update({ where: { - id_job: job_resp_create.id_job, + id_event: job_resp_create.id_event, }, data: { status: 'written', diff --git a/packages/api/src/@core/projects/projects.service.ts b/packages/api/src/@core/projects/projects.service.ts index 5ef3703d0..23e1aedbb 100644 --- a/packages/api/src/@core/projects/projects.service.ts +++ b/packages/api/src/@core/projects/projects.service.ts @@ -18,6 +18,7 @@ export class ProjectsService { const res = await this.prisma.projects.create({ data: { ...rest, + sync_mode: 'pool', id_project: uuidv4(), id_organization: id_organization, }, diff --git a/packages/api/src/@core/utils/crypto.ts b/packages/api/src/@core/utils/crypto.ts index 1d28ff759..95d297b96 100644 --- a/packages/api/src/@core/utils/crypto.ts +++ b/packages/api/src/@core/utils/crypto.ts @@ -8,7 +8,7 @@ export function encrypt(data: string): string { try { const cipher = crypto.createCipheriv( 'aes-256-cbc', - Buffer.from(secretKey), + Buffer.from(secretKey, 'utf-8'), iv, ); let encrypted = cipher.update(data, 'utf8', 'hex'); diff --git a/packages/api/src/@core/utils/types.ts b/packages/api/src/@core/utils/types.ts index bef58bf41..6a0e411d1 100644 --- a/packages/api/src/@core/utils/types.ts +++ b/packages/api/src/@core/utils/types.ts @@ -142,3 +142,74 @@ export const domains = { pipedrive: '', }, }; + +//TMP + +export enum ProviderVertical { + CRM = 'CRM', + HRIS = 'HRIS', + ATS = 'ATS', + Accounting = 'Accounting', + Ticketing = 'Ticketing', + MarketingAutomation = 'Marketing Automation', + FileStorage = 'File Storage', + Unknown = 'Unknown', +} + +export enum CrmProviders { + ZOHO = 'zoho', + ZENDESK = 'zendesk', + HUBSPOT = 'hubspot', + PIPEDRIVE = 'pipedrive', + FRESHSALES = 'freshsales', +} + +export enum AccountingProviders { + PENNYLANE = 'pennylane', + FRESHBOOKS = 'freshbooks', + CLEARBOOKS = 'clearbooks', + FREEAGENT = 'freeagent', + SAGE = 'sage', +} + +export const categoriesVerticals = Object.values(ProviderVertical); + +export const CRM_PROVIDERS = [ + 'zoho', + 'zendesk', + 'hubspot', + 'pipedrive', + 'freshsales', +]; + +export const HRIS_PROVIDERS = ['']; +export const ATS_PROVIDERS = ['']; +export const ACCOUNTING_PROVIDERS = ['']; +export const TICKETING_PROVIDERS = ['']; +export const MARKETING_AUTOMATION_PROVIDERS = ['']; +export const FILE_STORAGE_PROVIDERS = ['']; + +export function getProviderVertical(providerName: string): ProviderVertical { + if (CRM_PROVIDERS.includes(providerName)) { + return ProviderVertical.CRM; + } + if (HRIS_PROVIDERS.includes(providerName)) { + return ProviderVertical.HRIS; + } + if (ATS_PROVIDERS.includes(providerName)) { + return ProviderVertical.ATS; + } + if (ACCOUNTING_PROVIDERS.includes(providerName)) { + return ProviderVertical.Accounting; + } + if (TICKETING_PROVIDERS.includes(providerName)) { + return ProviderVertical.Ticketing; + } + if (MARKETING_AUTOMATION_PROVIDERS.includes(providerName)) { + return ProviderVertical.MarketingAutomation; + } + if (FILE_STORAGE_PROVIDERS.includes(providerName)) { + return ProviderVertical.FileStorage; + } + return ProviderVertical.Unknown; +} diff --git a/packages/api/src/@core/utils/unification/desunify.ts b/packages/api/src/@core/utils/unification/desunify.ts index ae616cbf9..97d40bfa1 100644 --- a/packages/api/src/@core/utils/unification/desunify.ts +++ b/packages/api/src/@core/utils/unification/desunify.ts @@ -1,7 +1,12 @@ import { CrmObject } from '@crm/@types'; -import { DesunifyReturnType, TargetObject, Unified } from '../types'; +import { + DesunifyReturnType, + ProviderVertical, + TargetObject, + Unified, + getProviderVertical, +} from '../types'; import { desunifyCrm } from '@crm/@unification'; -import { ProviderVertical, getProviderVertical } from 'shared-types'; /* to insert data diff --git a/packages/api/src/@core/utils/unification/unify.ts b/packages/api/src/@core/utils/unification/unify.ts index 78a6cbae5..ce086225c 100644 --- a/packages/api/src/@core/utils/unification/unify.ts +++ b/packages/api/src/@core/utils/unification/unify.ts @@ -1,7 +1,12 @@ import { CrmObject } from '@crm/@types'; -import { TargetObject, UnifyReturnType, UnifySourceType } from '../types'; +import { + ProviderVertical, + TargetObject, + UnifyReturnType, + UnifySourceType, + getProviderVertical, +} from '../types'; import { unifyCrm } from '@crm/@unification'; -import { ProviderVertical, getProviderVertical } from 'shared-types'; /* to fetch data diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index 73850ee25..de846b017 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -33,6 +33,19 @@ export class ContactController { ); } + @Get('sync') + syncContacts( + @Query('integrationId') integrationId: string, + @Query('linkedUserId') linkedUserId: string, + @Query('remote_data') remote_data?: boolean, + ) { + return this.contactService.syncContacts( + integrationId, + linkedUserId, + remote_data, + ); + } + @Post() addContacts( @Body() unfiedContactData: UnifiedContactInput[], diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 7d4cbd430..a1059d11d 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -69,67 +69,6 @@ export class ContactService { normalizedPhones, }; } - - async addContactToDb( - data: UnifiedContactInput, - job_id: string, - integrationId: string, - linkedUserId: string, - field_mappings?: Record[], - ) { - const { first_name, last_name, email_addresses, phone_numbers } = data; - const { normalizedEmails, normalizedPhones } = - this.normalizeEmailsAndNumbers(email_addresses, phone_numbers); - - const resp = await this.prisma.crm_contacts.create({ - data: { - id_crm_contact: uuidv4(), - created_at: new Date(), - modified_at: new Date(), - first_name: first_name, - last_name: last_name, - crm_email_addresses: { - create: normalizedEmails, - }, - crm_phone_numbers: { - create: normalizedPhones, - }, - id_job: job_id, - }, - }); - - //TODO: check why it doest iterate - if (field_mappings && field_mappings.length > 0) { - const entity = await this.prisma.entity.findFirst({ - where: { ressource_owner_id: 'contact' }, - }); - - for (const mapping of field_mappings) { - const attribute = await this.prisma.attribute.findFirst({ - where: { - slug: Object.keys(mapping)[0], - source: integrationId, - id_consumer: linkedUserId, - id_entity: entity.id_entity, - }, - }); - - if (attribute) { - await this.prisma.value.create({ - data: { - id_value: uuidv4(), - data: Object.values(mapping)[0], - id_attribute: attribute.id_attribute, - id_entity: entity.id_entity, - }, - }); - } - } - } - } - - /* */ - //TODO: do it for all providers async getCustomProperties(linkedUserId: string, providerId: string) { try { @@ -196,30 +135,17 @@ export class ContactService { linkedUserId: string, remote_data?: boolean, ): Promise> { - const job_resp_create = await this.prisma.jobs.create({ + const job_resp_create = await this.prisma.events.create({ data: { - id_job: uuidv4(), + id_event: uuidv4(), // Generate a new UUID for each job + status: 'initialized', // Use whatever status is appropriate + type: 'push', + direction: '0', + timestamp: new Date(), id_linked_user: linkedUserId, - status: 'initialized', - }, - }); - const job_id = job_resp_create.id_job; - //TODO: add field mappings data too - await this.addContactToDb( - unifiedContactData, - job_id, - integrationId, - linkedUserId, - unifiedContactData.field_mappings, - ); - const job_resp_update = await this.prisma.jobs.update({ - where: { - id_job: job_id, - }, - data: { - status: 'written', }, }); + const job_id = job_resp_create.id_event; // Retrieve custom field mappings // get potential fieldMappings and extract the original properties name @@ -298,9 +224,9 @@ export class ContactService { }; } const status_resp = resp.statusCode === HttpStatus.OK ? 'success' : 'fail'; - const job_resp = await this.prisma.jobs.update({ + const job_resp = await this.prisma.events.update({ where: { - id_job: job_id, + id_event: job_id, }, data: { status: status_resp, @@ -309,20 +235,98 @@ export class ContactService { return { ...resp, data: res }; } - //TODO: insert data in the db (would be used as data lake and webooks syncs later) async getContacts( integrationId: string, linkedUserId: string, remote_data?: boolean, ): Promise> { - const job_resp_create = await this.prisma.jobs.create({ + // handle case for remote data too + //TODO: handle case where data is not there (not synced) or old synced + const contacts = await this.prisma.crm_contacts.findMany({ + where: { + origin: integrationId, + events: { + id_linked_user: linkedUserId, + }, + }, + include: { + crm_email_addresses: true, + crm_phone_numbers: true, + }, + }); + + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( + contacts.map(async (contact) => { + // Fetch field mappings for the contact + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: contact.id_crm_contact, + }, + }, + include: { + attribute: true, + }, + }); + //console.log(values); + + const field_mappings = values.map((value) => ({ + [value.attribute.slug]: value.data, + })); + + // Transform to UnifiedContactInput format + return { + first_name: contact.first_name, + last_name: contact.last_name, + email_addresses: contact.crm_email_addresses.map((email) => ({ + email_address: email.email_address, + email_address_type: email.email_address_type, + })), + phone_numbers: contact.crm_phone_numbers.map((phone) => ({ + phone_number: phone.phone_number, + phone_type: phone.phone_type, + })), + field_mappings: field_mappings, + }; + }), + ); + + const res: ContactResponse = { + contacts: unifiedContacts, + }; + + /*if (remote_data) { + res = { + ...res, + remote_data: [resp.data], + }; + }*/ + + return { + data: res, + statusCode: 200, + }; + } + + //function used by sync worker which populate our crm_contacts table + //its role is to fetch all contacts from providers 3rd parties and save the info inside our db + //TODO: find a way to save all remote data for each contact somowhere in our db so our GET action know where to fetch it + async syncContacts( + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ) { + const job_resp_create = await this.prisma.events.create({ data: { - id_job: uuidv4(), + id_event: uuidv4(), // Generate a new UUID for each job + status: 'initialized', // Use whatever status is appropriate + type: 'pull', + direction: '0', + timestamp: new Date(), id_linked_user: linkedUserId, - status: 'written', }, }); - const job_id = job_resp_create.id_job; + const job_id = job_resp_create.id_event; // get potential fieldMappings and extract the original properties name const customFieldMappings = @@ -361,8 +365,6 @@ export class ContactService { break; } - //TODO: insert the data in the DB with the fieldMappings (value table) - const sourceObject: OriginalContactOutput[] = resp.data; //unify the data according to the target obj wanted const unifiedObject = (await unify({ @@ -372,30 +374,156 @@ export class ContactService { customFieldMappings, })) as UnifiedContactOutput[]; - let res: ContactResponse = { - contacts: unifiedObject, - }; + const contactIds = sourceObject.map((contact) => + 'id' in contact + ? (contact.id as string) + : 'contact_id' in contact + ? String(contact.contact_id) + : undefined, + ); - if (remote_data) { - res = { - ...res, - remote_data: sourceObject, - }; - } + //insert the data in the DB with the fieldMappings (value table) + await this.saveContactsInDb( + linkedUserId, + unifiedObject, + contactIds, + integrationId, + job_id, + ); + return 200; + } - const status_resp = resp.statusCode === HttpStatus.OK ? 'success' : 'fail'; + //TODO; field mappings check + async saveContactsInDb( + linkedUserId: string, + contacts: UnifiedContactOutput[], + originIds: string[], + originSource: string, + jobId: string, + ) { + for (let i = 0; i < contacts.length; i++) { + const contact = contacts[i]; + const originId = originIds[i]; //TODO: check that originId is defined - const job_resp = await this.prisma.jobs.update({ - where: { - id_job: job_id, - }, - data: { - status: status_resp, - }, - }); - return { ...resp, data: res }; + const existingContact = await this.prisma.crm_contacts.findFirst({ + where: { + origin_id: originId, + origin: originSource, + events: { + id_linked_user: linkedUserId, + }, + }, + include: { crm_email_addresses: true, crm_phone_numbers: true }, + }); + + const { normalizedEmails, normalizedPhones } = + this.normalizeEmailsAndNumbers( + contact.email_addresses, + contact.phone_numbers, + ); + + let unique_crm_contact_id: string; + + if (existingContact) { + // Update the existing contact + const res = await this.prisma.crm_contacts.update({ + where: { + id_crm_contact: existingContact.id_crm_contact, + }, + data: { + first_name: contact.first_name, + last_name: contact.last_name, + modified_at: new Date(), + crm_email_addresses: { + update: normalizedEmails.map((email, index) => ({ + where: { + id_crm_email: + existingContact.crm_email_addresses[index].id_crm_email, + }, + data: email, + })), + }, + crm_phone_numbers: { + update: normalizedPhones.map((phone, index) => ({ + where: { + id_crm_phone_number: + existingContact.crm_phone_numbers[index] + .id_crm_phone_number, + }, + data: phone, + })), + }, + }, + }); + unique_crm_contact_id = res.id_crm_contact; + } else { + // Create a new contact + const res = await this.prisma.crm_contacts.create({ + data: { + id_crm_contact: uuidv4(), + first_name: contact.first_name, + last_name: contact.last_name, + created_at: new Date(), + modified_at: new Date(), + id_event: jobId, + origin_id: originId, + origin: originSource, + crm_email_addresses: { + create: normalizedEmails, + }, + crm_phone_numbers: { + create: normalizedPhones, + }, + }, + }); + unique_crm_contact_id = res.id_crm_contact; + } + + //TODO: map the fields mapping to the contact + // check duplicate or existing values + if (contact.field_mappings && contact.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_contact_id, + }, + }); + + for (const mapping of contact.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + } } + //TODO async updateContact( id: string, updateContactData: Partial, diff --git a/packages/shared-types/package.json b/packages/shared-types/package.json index ab502d0e9..6b9f40bbd 100644 --- a/packages/shared-types/package.json +++ b/packages/shared-types/package.json @@ -8,5 +8,6 @@ "@typescript-eslint/parser": "^6.13.2", "eslint": "^8.55.0", "typescript": "^5.3.3" - } + }, + "type": "module" } \ No newline at end of file