diff --git a/.env.example b/.env.example index ae0d5d435..284837894 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ # ================================================ ENV=dev DISTRIBUTION=selfhosted # selfhosted or managed -OAUTH_REDIRECT_BASE=http://localhost:3000 +PANORA_BASE_API_URL=http://localhost:3000 JWT_SECRET=secret_jwt ENCRYPT_CRYPTO_SECRET_KEY="0123456789abcdef0123456789abcdef" #Managed only @@ -16,6 +16,10 @@ REDIS_HOST=redis REDIS_PORT=6379 REDIS_USER=default REDIS_PASS=A3vniod98Zbuvn9u5 + +#REDIS_TLS= + + # ================================================ # ================================================ @@ -64,7 +68,8 @@ GORGIAS_TICKETING_CLOUD_CLIENT_SECRET= GORGIAS_TICKETING_CLOUD_SUBDOMAIN= FRONT_TICKETING_CLOUD_CLIENT_ID= FRONT_TICKETING_CLOUD_CLIENT_SECRET= - +GITLAB_TICKETING_CLOUD_CLIENT_ID= +GITLAB_TICKETING_CLOUD_CLIENT_SECRET= # ================================================ # Webapp settings # Must be set in the perspective of the end user browser diff --git a/.github/workflows/docker.check-build.backend.selfhosted.yml b/.github/workflows/docker.check-build.backend.selfhosted.yml index aaa0177a9..b332f12de 100644 --- a/.github/workflows/docker.check-build.backend.selfhosted.yml +++ b/.github/workflows/docker.check-build.backend.selfhosted.yml @@ -27,7 +27,7 @@ jobs: push: false tags: panoradotdev/backend-api:selfhosted build-args: | - OAUTH_REDIRECT_BASE=${{ env.OAUTH_REDIRECT_BASE }} + PANORA_BASE_API_URL=${{ env.PANORA_BASE_API_URL }} DISTRIBUTION=${{ env.DISTRIBUTION }} ENV=${{ ENV }} DATABASE_URL=postgresql://${{env.POSTGRES_USER}}:${{secrets.POSTGRES_PASSWORD}}@${{env.POSTGRES_HOST}}:5432/${{env.POSTGRES_DB}}?ssl=false diff --git a/.github/workflows/docker.export.backend.selfhosted.yml b/.github/workflows/docker.export.backend.selfhosted.yml index e3e866a0b..14bb2abcf 100644 --- a/.github/workflows/docker.export.backend.selfhosted.yml +++ b/.github/workflows/docker.export.backend.selfhosted.yml @@ -33,7 +33,7 @@ jobs: push: true tags: panoradotdev/backend-api:selfhosted build-args: | - OAUTH_REDIRECT_BASE=${{ env.OAUTH_REDIRECT_BASE }} + PANORA_BASE_API_URL=${{ env.PANORA_BASE_API_URL }} DISTRIBUTION=${{ env.DISTRIBUTION }} ENV=${{ ENV }} DATABASE_URL=postgresql://${{env.POSTGRES_USER}}:${{secrets.POSTGRES_PASSWORD}}@${{env.POSTGRES_HOST}}:5432/${{env.POSTGRES_DB}}?ssl=false diff --git a/.github/workflows/porter_stack_panora-backend.yml b/.github/workflows/porter_stack_panora-backend.yml deleted file mode 100644 index 7df2b672f..000000000 --- a/.github/workflows/porter_stack_panora-backend.yml +++ /dev/null @@ -1,28 +0,0 @@ -"on": - push: - branches: - - main -name: Deploy to panora-backend -jobs: - porter-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Set Github tag - id: vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Setup porter - uses: porter-dev/setup-porter@v0.1.0 - - name: Deploy stack - timeout-minutes: 30 - run: exec porter apply - env: - PORTER_CLUSTER: "3838" - PORTER_DEPLOYMENT_TARGET_ID: d70e8c80-aa13-48c7-b549-94422f0c31f0 - PORTER_HOST: https://dashboard.getporter.dev - PORTER_PR_NUMBER: ${{ github.event.number }} - PORTER_PROJECT: "12245" - PORTER_STACK_NAME: panora-backend - PORTER_TAG: ${{ steps.vars.outputs.sha_short }} - PORTER_TOKEN: ${{ secrets.PORTER_STACK_12245_3838 }} diff --git a/apps/client-ts/Dockerfile b/apps/client-ts/Dockerfile index 940f4217a..9557e5761 100644 --- a/apps/client-ts/Dockerfile +++ b/apps/client-ts/Dockerfile @@ -13,7 +13,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 COPY . . RUN turbo prune client-ts --docker diff --git a/apps/client-ts/Dockerfile.dev b/apps/client-ts/Dockerfile.dev index 547a70508..3b940a38f 100644 --- a/apps/client-ts/Dockerfile.dev +++ b/apps/client-ts/Dockerfile.dev @@ -21,7 +21,7 @@ ENV VITE_STYTCH_TOKEN="$VITE_STYTCH_TOKEN" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 # Start the Webapp CMD cd apps/client-ts && pnpm install && pnpm run dev diff --git a/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx b/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx index edad14f74..03e71fa52 100644 --- a/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx +++ b/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx @@ -32,7 +32,7 @@ export function CatalogWidget() { useEffect(() => { if (data) { const filteredData = Object.entries(data).reduce((acc, [key, value]) => { - if (key !== 'id_project' && key !== 'id_project_connector') { + if (key !== 'id_connector_set') { acc[key] = Boolean(value); } return acc; diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingModal.tsx b/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingModal.tsx deleted file mode 100644 index 3b3ed433b..000000000 --- a/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingModal.tsx +++ /dev/null @@ -1,473 +0,0 @@ -/* eslint-disable react/no-unescaped-entities */ -'use client' - -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import useMapField from "@/hooks/create/useMapField" -import { useEffect, useState } from "react" -import useFieldMappings from "@/hooks/get/useFieldMappings" -import useProviderProperties from "@/hooks/get/useProviderProperties" -import { standardObjects } from "@panora/shared" -import useProjectStore from "@/state/projectStore" -import useLinkedUsers from "@/hooks/get/useLinkedUsers" -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import * as z from "zod" -import { usePostHog } from 'posthog-js/react' -import config from "@/lib/config" -import { CRM_PROVIDERS } from "@panora/shared" -import useDefineField from "@/hooks/create/useDefineField" -import { toast } from "sonner" -import { Badge } from "@/components/ui/badge" -import { useQueryClient } from "@tanstack/react-query" - - -const defineFormSchema = z.object({ - standardModel: z.string().min(2, { - message: "standardModel must be at least 2 characters.", - }), - fieldName: z.string().min(2, { - message: "fieldName must be at least 2 characters.", - }), - fieldDescription: z.string().min(2, { - message: "fieldDescription must be at least 2 characters.", - }), - fieldType: z.string().min(2, { - message: "fieldType must be at least 2 characters.", - }), -}) - -const mapFormSchema = z.object({ - attributeId: z.string().min(2, { - message: "attributeId must be at least 2 characters.", - }), - sourceCustomFieldId: z.string().min(2, { - message: "sourceCustomFieldId must be at least 2 characters.", - }), - sourceProvider: z.string().min(2, { - message: "sourceProvider must be at least 2 characters.", - }), - linkedUserId: z.string().min(2, { - message: "linkedUserId must be at least 2 characters.", - }), -}) - -export function FModal({ onClose }: {onClose: () => void}) { - - const defineForm = useForm>({ - resolver: zodResolver(defineFormSchema), - defaultValues: { - standardModel: "", - fieldName: "", - fieldDescription: "", - fieldType: "", - }, - }) - - const mapForm = useForm>({ - resolver: zodResolver(mapFormSchema), - defaultValues: { - attributeId: "", - sourceCustomFieldId: "", - sourceProvider: "", - linkedUserId: "" - }, - }) - - const [sourceCustomFieldsData, setSourceCustomFieldsData] = useState[]>([]); - const [ linkedUserId, sourceProvider ] = mapForm.watch(['linkedUserId', 'sourceProvider']); - - const {idProject} = useProjectStore(); - const queryClient = useQueryClient(); - - const { data: mappings } = useFieldMappings(); - const { defineMappingPromise } = useDefineField(); - const { mapMappingPromise } = useMapField(); - const { data: linkedUsers } = useLinkedUsers(); - // TODO: HANDLE VERTICAL AND PROVIDERS FOR CUSTOM MAPPINGS - const { data: sourceCustomFields, error, isLoading } = useProviderProperties(linkedUserId,sourceProvider, "crm"); - - const posthog = usePostHog() - - useEffect(() => { - if (sourceCustomFields && sourceCustomFields.data.length > 0 && !isLoading && !error) { - console.log("inside custom fields properties "); - setSourceCustomFieldsData(sourceCustomFields.data); - } - }, [sourceCustomFields, isLoading, error]); - - - function onDefineSubmit(values: z.infer) { - toast.promise( - defineMappingPromise({ - object_type_owner: values.standardModel, - name: values.fieldName, - description: values.fieldDescription, - data_type: values.fieldType, - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['mappings'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Custom field - {`${ values.fieldName }`} - has been defined -
-
- ) - ; - }, - error: 'Error', - }); - posthog?.capture("field_defined", { - id_project: idProject, - mode: config.DISTRIBUTION - }) - onClose(); - } - - function onMapSubmit(values: z.infer) { - toast.promise( - mapMappingPromise({ - attributeId: values.attributeId.trim(), - source_custom_field_id: values.sourceCustomFieldId, - source_provider: values.sourceProvider, - linked_user_id: values.linkedUserId, - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['mappings'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Custom field - {`${values.sourceCustomFieldId}`} - has been mapped -
-
- ) - ; - }, - error: 'Error', - }); - posthog?.capture("field_mapped", { - id_project: idProject, - mode: config.DISTRIBUTION - }) - onClose(); - } - - return ( - - - Create Field - Map Field - - - -
- - - Create a custom field - - Create a custom field in Panora to extend our unified objects. Once done, you can map this field to existing fields in your end-user's software. Find details in - documentation. - - - -
- ( - - What object to you want to extend? - - - - - - )} - /> -
-
- ( - - Give your Custom Field an identifier - - - - - - )} - /> -
-
- ( - - Short Description - - - - - - )} - /> -
-
- ( - - Data Type - - - - - - )} - /> -
-
- - - -
- -
-
- - -
- - - - Map Field - - Field Mapping allows you to map data from your users' platforms to custom fields on your Panora Unified Models. - - - -
- ( - - Panora Custom Field - - - - - - )} - /> -
-
- ( - - Provider - - - - - This is the source provider where the field exists. - - - - )} - /> -
-
- ( - - Linked User Id - - - - - This is the id of the user in your system. - - - - )} - /> -
-
- ( - - Origin Source Field - - - - - These are all the fields we found in your customer's software. - - - - )} - /> -
-
- - - -
- -
-
-
- ) -} diff --git a/apps/client-ts/src/components/Events/EventsTable.tsx b/apps/client-ts/src/components/Events/EventsTable.tsx index 920897cdd..88befe029 100644 --- a/apps/client-ts/src/components/Events/EventsTable.tsx +++ b/apps/client-ts/src/components/Events/EventsTable.tsx @@ -5,7 +5,6 @@ import { DataTableLoading } from "../shared/data-table-loading"; import { events as Event } from "api"; import { useEventsCount } from '@/hooks/get/useEventsCount'; import { useQueryPagination } from '@/hooks/get/useQueryPagination'; -import useProjectStore from "@/state/projectStore"; export default function EventsTable() { const { data: eventsCount } = useEventsCount(); @@ -22,14 +21,14 @@ export default function EventsTable() { pageSize: pagination.pageSize, }); - //TODO const transformedEvents = events?.map((event: Event) => ({ - method: event.method, // replace with actual value - url: event.url, // replace with actual value + method: event.method, + url: event.url, status: event.status, - direction: event.type, // replace with actual value - integration: event.provider, // replace with actual value + logo - date: event.timestamp.toLocaleString(), // convert Date to string + direction: event.type, + integration: event.provider, + id_linked_user: event.id_linked_user, + date: event.timestamp.toLocaleString(), })); if(isLoading){ diff --git a/apps/client-ts/src/components/Events/columns.tsx b/apps/client-ts/src/components/Events/columns.tsx index cd8ef32ce..70f9b920e 100644 --- a/apps/client-ts/src/components/Events/columns.tsx +++ b/apps/client-ts/src/components/Events/columns.tsx @@ -90,6 +90,22 @@ export const columns: ColumnDef[] = [ return value.includes(row.getValue(id)) }, }, + { + accessorKey: "id_linked_user", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {row.getValue("id_linked_user")} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, { accessorKey: "date", header: ({ column }) => ( diff --git a/apps/client-ts/src/components/Events/schema.ts b/apps/client-ts/src/components/Events/schema.ts index 81897f91b..175c4f7a5 100644 --- a/apps/client-ts/src/components/Events/schema.ts +++ b/apps/client-ts/src/components/Events/schema.ts @@ -8,6 +8,7 @@ export const eventSchema = z.object({ status: z.string(), direction: z.string(), integration: z.string(), + id_linked_user: z.string(), date: z.string() }) diff --git a/apps/client-ts/src/hooks/get/useProjectConnectors.tsx b/apps/client-ts/src/hooks/get/useProjectConnectors.tsx index 540c9c8c5..e0f330704 100644 --- a/apps/client-ts/src/hooks/get/useProjectConnectors.tsx +++ b/apps/client-ts/src/hooks/get/useProjectConnectors.tsx @@ -1,16 +1,11 @@ import config from '@/lib/config'; import { useQuery } from '@tanstack/react-query'; import Cookies from 'js-cookie'; - -interface ProjectConnectorBase { - id_project: string; - id_project_connector: string; -} - -interface ProjectConnector extends ProjectConnectorBase { +interface ProjectConnector { + id_connector_set: string; [key: string]: boolean | string; } - + const useProjectConnectors = (id: string) => { return useQuery({ queryKey: ['project-connectors'], diff --git a/apps/magic-link/Dockerfile b/apps/magic-link/Dockerfile index 5c308bfd0..b8efb57e2 100644 --- a/apps/magic-link/Dockerfile +++ b/apps/magic-link/Dockerfile @@ -12,7 +12,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 COPY . . RUN turbo prune magic-link --docker RUN ls -la ./out/full/apps/magic-link diff --git a/apps/magic-link/Dockerfile.dev b/apps/magic-link/Dockerfile.dev index ddedf2945..f6f74b25d 100644 --- a/apps/magic-link/Dockerfile.dev +++ b/apps/magic-link/Dockerfile.dev @@ -19,7 +19,7 @@ ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 # run the ML CMD cd apps/magic-link && pnpm install && pnpm run dev --host \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2a94d471e..6e340f247 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -37,6 +37,7 @@ services: REDIS_PASS: ${REDIS_PASS} REDIS_USER: ${REDIS_USER} REDIS_PORT: ${REDIS_PORT} + #REDIS_TLS: 1 # set this variable to 1 when Redis is AWS hosted REDIS_DB: ${REDIS_DB} ENCRYPT_CRYPTO_SECRET_KEY: ${ENCRYPT_CRYPTO_SECRET_KEY} HUBSPOT_CRM_CLOUD_CLIENT_ID: ${HUBSPOT_CRM_CLOUD_CLIENT_ID} @@ -49,7 +50,7 @@ services: PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET: ${PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET} ZENDESK_CRM_CLOUD_CLIENT_ID: ${ZENDESK_CRM_CLOUD_CLIENT_ID} ZENDESK_CRM_CLOUD_CLIENT_SECRET: ${ZENDESK_CRM_CLOUD_CLIENT_SECRET} - OAUTH_REDIRECT_BASE: ${OAUTH_REDIRECT_BASE} + PANORA_BASE_API_URL: ${PANORA_BASE_API_URL} ZENDESK_TICKETING_CLOUD_CLIENT_ID: ${ZENDESK_TICKETING_CLOUD_CLIENT_ID} ZENDESK_TICKETING_CLOUD_CLIENT_SECRET: ${ZENDESK_TICKETING_CLOUD_CLIENT_SECRET} ZENDESK_TICKETING_CLOUD_SUBDOMAIN: ${ZENDESK_TICKETING_CLOUD_SUBDOMAIN} @@ -186,6 +187,19 @@ services: - frontend volumes: - .:/app + + ngrok: + image: ngrok/ngrok:latest + restart: always + command: + - "start" + - "--all" + - "--config" + - "/etc/ngrok.yml" + volumes: + - ./ngrok.yml:/etc/ngrok.yml + ports: + - 4040:4040 docs: build: diff --git a/docker-compose.source.yml b/docker-compose.source.yml index 53820373b..13cdbe89f 100644 --- a/docker-compose.source.yml +++ b/docker-compose.source.yml @@ -50,7 +50,7 @@ services: PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET: ${PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET} ZENDESK_CRM_CLOUD_CLIENT_ID: ${ZENDESK_CRM_CLOUD_CLIENT_ID} ZENDESK_CRM_CLOUD_CLIENT_SECRET: ${ZENDESK_CRM_CLOUD_CLIENT_SECRET} - OAUTH_REDIRECT_BASE: ${OAUTH_REDIRECT_BASE} + PANORA_BASE_API_URL: ${PANORA_BASE_API_URL} ZENDESK_TICKETING_CLOUD_CLIENT_ID: ${ZENDESK_TICKETING_CLOUD_CLIENT_ID} ZENDESK_TICKETING_CLOUD_CLIENT_SECRET: ${ZENDESK_TICKETING_CLOUD_CLIENT_SECRET} ZENDESK_TICKETING_CLOUD_SUBDOMAIN: ${ZENDESK_TICKETING_CLOUD_SUBDOMAIN} diff --git a/docker-compose.yml b/docker-compose.yml index ed9f2a62a..135130f27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET: ${PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET} ZENDESK_CRM_CLOUD_CLIENT_ID: ${ZENDESK_CRM_CLOUD_CLIENT_ID} ZENDESK_CRM_CLOUD_CLIENT_SECRET: ${ZENDESK_CRM_CLOUD_CLIENT_SECRET} - OAUTH_REDIRECT_BASE: ${OAUTH_REDIRECT_BASE} + PANORA_BASE_API_URL: ${PANORA_BASE_API_URL} ZENDESK_TICKETING_CLOUD_CLIENT_ID: ${ZENDESK_TICKETING_CLOUD_CLIENT_ID} ZENDESK_TICKETING_CLOUD_CLIENT_SECRET: ${ZENDESK_TICKETING_CLOUD_CLIENT_SECRET} ZENDESK_TICKETING_CLOUD_SUBDOMAIN: ${ZENDESK_TICKETING_CLOUD_SUBDOMAIN} diff --git a/docs/open-source/contributors.mdx b/docs/open-source/contributors.mdx index 4766d8826..04e47a1ef 100644 --- a/docs/open-source/contributors.mdx +++ b/docs/open-source/contributors.mdx @@ -25,10 +25,10 @@ We made a docker file that builds Panora from sources, specifically to help you You have the option to manage a custom 3rd party OAuth App. - **Each custom 3rd party environment variable must be of the form `PROVIDER_VERTICAL_SOFTWAREMODE_ATTRIBUTE` where** + **Each custom 3rd party environment variable must be of the form `PROVIDER_category_SOFTWAREMODE_ATTRIBUTE` where** - `PROVIDER` is any 3rd party name - - `VERTICAL` is for example [CRM, TICKETING, MARKETINGAUTOMATION, ...] + - `category` is for example [CRM, TICKETING, MARKETINGAUTOMATION, ...] - `SOFTWAREMODE` is [ CLOUD, ONPREMISE ] - `ATTRIBUTE` is for example [ CLIENT_ID, CLIENT_SECRET, SUBDOMAIN, ... ] @@ -64,7 +64,7 @@ Make sure you are inside `packages/api/src` where the server lives ! _Ie: Slack, Hubspot, Jira, Shopify ..._ -First choose wisely which vertical the 3rd party belongs to among these: +First choose wisely which category the 3rd party belongs to among these: - `crm` - `ticketing` @@ -74,14 +74,16 @@ First choose wisely which vertical the 3rd party belongs to among these: - `hris` - `marketingautomation` +You can find all categories inside [`packages/shared/src/categories.ts`](https://github.com/panoratech/Panora/blob/main/packages/shared/src/categories.ts). + For the sake of the guide, now on we'll consider adding a 3rd party belonging - to the `crm` vertical. + to the `crm` category. # Step 1: Ensure 3rd party metadata is set -Look into the `packages/shared/src/utils.ts` file and check if the provider you want to build has its metadata set inside the `CONNECTORS_METADATA` object. +Look into the `packages/shared/src/connectors/metadata.ts` file and check if the provider you want to build has its metadata set inside the `CONNECTORS_METADATA` object. It should be available (if not [contact us](https://app.cal.com/rflih/30)) with `active` field set to `false` meaning the integration has not been built. @@ -96,7 +98,7 @@ Actually an integration is built in 2 parts : _Ie: Contact, Ticket, Deal, Company ..._ -For the sake of this guide, let's map the common object `contact` under `crm` vertical to _my3rdParty_ (in reality it would be a real 3rd party name). +For the sake of this guide, let's map the common object `contact` under `crm` category to _my3rdParty_ (in reality it would be a real 3rd party name). **An integration is considered valid when all common objects have been mapped. @@ -118,6 +120,8 @@ For the sake of this guide, let's map the common object `contact` under `crm` ve - `types.ts` where the 3rd party specific API types are defined - `mappers.ts` where the mapping between our unified common model and the 3rd party one is handled + After copying the following code you'll end up with linting/deps errors. It is fixed by our script at Step 2. + It must implement the `IContactService` interface. @@ -224,12 +228,12 @@ For the sake of this guide, let's map the common object `contact` under `crm` ve - After these 3 files are successfully created and filled, you are ready to to fix all dependencies/linting issues that you may have.
+ After these 3 files are successfully created and filled, you are ready to fix all dependencies/linting issues that you may have.
To make sure the service is enabled, dependencies and imports must be added.
We built a script that does it in seconds. You can execute the given command from the root directory of Panora. ```bash - cd packages/api && pnpm install && pnpm run validate-connectors --vertical="crm" --objectType="contact" + cd packages/api && pnpm install && pnpm run validate-connectors --category="crm" --objectType="contact" ``` The script will automatically scan the `/crm/contact/services` folder and detect any new service folder so all dependencies and imports are updated across the codebase. diff --git a/ngrok.yml b/ngrok.yml new file mode 100644 index 000000000..c53b55532 --- /dev/null +++ b/ngrok.yml @@ -0,0 +1,10 @@ +version: 2 +authtoken: YOUR_NGROK_KEY_HERE +log_level: debug +log: stdout + +tunnels: + api-tunnel: + proto: http + addr: 3000 + domain: your_ngrok_domain_here \ No newline at end of file diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile index 5207ae56d..3deae4f1c 100644 --- a/packages/api/Dockerfile +++ b/packages/api/Dockerfile @@ -12,7 +12,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 COPY . . RUN turbo prune api --docker diff --git a/packages/api/Dockerfile.dev b/packages/api/Dockerfile.dev index c90275b21..86a74bf0d 100644 --- a/packages/api/Dockerfile.dev +++ b/packages/api/Dockerfile.dev @@ -12,7 +12,7 @@ ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app -RUN pnpm add -g turbo +RUN pnpm add -g turbo@1.13.4 # Start API CMD pnpm install && cd packages/api && npx prisma db pull && npx prisma generate && pnpm run start:dev \ No newline at end of file diff --git a/packages/api/Dockerfile.validate-connectors b/packages/api/Dockerfile.validate-connectors new file mode 100644 index 000000000..018b74ab7 --- /dev/null +++ b/packages/api/Dockerfile.validate-connectors @@ -0,0 +1,30 @@ +################################################ +# 1/ move to the repo root directory +# 2/ build the image : docker build -t validate_connectors -f ./packages/api/Dockerfile.validate-connectors . +# 3/ run with: docker run -v $(pwd):/app/ -e VERTICAL=vertical -e OBJECT_TYPE=object validate_connectors +# note: use lowercase for object and vertical values +################################################ + +FROM node:20-alpine AS base +# ======================================================================= +FROM base AS builder +RUN apk add --no-cache libc6-compat netcat-openbsd curl +RUN apk update + +# Set pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /app +RUN pnpm add -g turbo@1.13.4 + +# Start Script + +# Set environment variable +ENV VERTICAL="$VERTICAL" +ENV OBJECT_TYPE="$OBJECT_TYPE" + +WORKDIR /app/packages/api + +CMD pnpm install && pnpm run validate-connectors --vertical="${VERTICAL}" --objectType="${OBJECT_TYPE}" \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 36d8ceb2f..7fd3757d8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -22,7 +22,6 @@ "test:e2e": "jest --config ./test/jest-e2e.json", "ci": "pnpm run lint && pnpm run build", "copy-types-to-shared": "cp ./exports.ts ../shared/index.ts", - "prisma:seed": "ts-node ./scripts/seed.webapp.ts", "validate-connectors": "node --experimental-detect-module ./scripts/connectorUpdate.js", "prebuild-oauth-connector": "node --experimental-detect-module ./scripts/oauthConnector.js" }, diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index ded59f6b5..b65b22a22 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -141,7 +141,7 @@ model crm_companies { name String? industry String? number_of_employees BigInt? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_companies") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String? remote_platform String? @@ -164,7 +164,7 @@ model crm_contacts { id_crm_contact String @id(map: "pk_crm_contacts") @db.Uuid first_name String last_name String - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_contacts") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String remote_platform String @@ -184,8 +184,8 @@ model crm_deals { id_crm_deal String @id(map: "pk_crm_deal") @db.Uuid name String description String - amount BigInt - created_at DateTime @db.Timestamp(6) + amount BigInt? + created_at DateTime @unique(map: "force_createdat_unique_crm_deals") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) remote_id String? remote_platform String? @@ -207,7 +207,7 @@ model crm_deals { model crm_deals_stages { id_crm_deals_stage String @id(map: "pk_crm_deal_stages") @db.Uuid stage_name String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_deals_stages") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid remote_id String? @@ -251,7 +251,7 @@ model crm_engagements { subject String? start_at DateTime? @db.Timestamp(6) end_time DateTime? @db.Timestamp(6) - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_engagements") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) remote_id String? id_linked_user String? @db.Uuid @@ -269,7 +269,7 @@ model crm_engagements { model crm_notes { id_crm_note String @id(map: "pk_crm_notes") @db.Uuid content String - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_notes") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_crm_company String? @db.Uuid id_crm_contact String? @db.Uuid @@ -312,7 +312,7 @@ model crm_tasks { status String? due_date DateTime? @db.Timestamp(6) finished_date DateTime? @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_tasks") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_crm_user String? @db.Uuid id_crm_company String? @db.Uuid @@ -333,7 +333,7 @@ model crm_users { id_crm_user String @id(map: "pk_crm_users") @db.Uuid name String? email String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_crm_users") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid remote_id String? @@ -425,35 +425,20 @@ model linked_users { @@index([id_project], map: "fk_proectid_linked_users") } -model project_connectors { - id_project_connector String @id(map: "pk_project_connectors") @db.Uuid - id_project String @db.Uuid - crm_hubspot Boolean - crm_zoho Boolean - crm_zendesk Boolean - crm_pipedrive Boolean - crm_attio Boolean - tcg_zendesk Boolean - tcg_gorgias Boolean - tcg_front Boolean - tcg_jira Boolean - tcg_gitlab Boolean - projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_connectors") -} - /// 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 projects { - id_project String @id(map: "pk_projects") @db.Uuid - name String - sync_mode String - pull_frequency BigInt? - redirect_url String? - id_user String @db.Uuid - api_keys api_keys[] - connections connections[] - linked_users linked_users[] - project_connectors project_connectors[] - users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_46_1") + id_project String @id(map: "pk_projects") @db.Uuid + name String + sync_mode String + pull_frequency BigInt? + redirect_url String? + id_user String @db.Uuid + id_connector_set String @db.Uuid + api_keys api_keys[] + connections connections[] + linked_users linked_users[] + users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_46_1") + connector_sets connector_sets @relation(fields: [id_connector_set], references: [id_connector_set], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_connectorsetid") } /// 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 @@ -472,7 +457,7 @@ model tcg_accounts { name String? domains String[] remote_platform String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_accounts") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid tcg_contacts tcg_contacts[] @@ -486,7 +471,7 @@ model tcg_attachments { file_name String? file_url String? uploader String @db.Uuid - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_attachments") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid id_tcg_ticket String? @db.Uuid @@ -506,7 +491,7 @@ model tcg_collections { remote_platform String? collection_type String? parent_collection String? @db.Uuid - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_collections") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String @db.Uuid } @@ -519,7 +504,7 @@ model tcg_comments { is_private Boolean? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_comments") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) creator_type String? id_tcg_attachment String[] @@ -545,7 +530,7 @@ model tcg_contacts { details String? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_contacts") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) id_tcg_account String? @db.Uuid id_linked_user String? @db.Uuid @@ -560,7 +545,7 @@ model tcg_tags { name String? remote_id String? remote_platform String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_tags") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_tcg_ticket String? @db.Uuid id_linked_user String? @db.Uuid @@ -575,7 +560,7 @@ model tcg_teams { remote_platform String? name String? description String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_teams") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) id_linked_user String? @db.Uuid } @@ -593,7 +578,7 @@ model tcg_tickets { collections String[] completed_at DateTime? @db.Timestamp(6) priority String? - created_at DateTime @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_tickets") @default(now()) @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) assigned_to String[] remote_id String? @@ -616,7 +601,7 @@ model tcg_users { remote_id String? remote_platform String? teams String[] - created_at DateTime? @db.Timestamp(6) + created_at DateTime @unique(map: "force_createdat_unique_tcg_users") @default(now()) @db.Timestamp(6) modified_at DateTime? @db.Timestamp(6) id_linked_user String? @db.Uuid tcg_comments tcg_comments[] @@ -656,3 +641,30 @@ model webhook_delivery_attempts { @@index([id_event], map: "fk_webhook_delivery_attempt_eventid") @@index([id_webhooks_reponse], map: "fk_webhook_delivery_attempt_webhook_responseid") } + +model connector_sets { + id_connector_set String @id(map: "pk_project_connector") @db.Uuid + crm_hubspot Boolean + crm_zoho Boolean + crm_attio Boolean + crm_pipedrive Boolean + tcg_zendesk Boolean + tcg_jira Boolean + tcg_gorgias Boolean + tcg_gitlab Boolean + tcg_front Boolean + projects projects[] +} + +/// 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 managed_webhooks { + id_managed_webhook String @id(map: "pk_managed_webhooks") @db.Uuid + active Boolean + id_connection String @db.Uuid + endpoint String @db.Uuid + api_version String? + active_events String[] + remote_signing_secret String? + modified_at DateTime @db.Timestamp(6) + created_at DateTime @db.Timestamp(6) +} diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 3ea0bac32..023530d1e 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -1,3 +1,5 @@ +-- ************************* SqlDBM: PostgreSQL ************************* +-- *********** Generated by SqlDBM: Panora_DB by rf@panora.dev ********** @@ -17,8 +19,6 @@ COMMENT ON COLUMN webhooks_reponses.http_status_code IS 'anything that is not 2x - - -- ************************************** webhooks_payloads CREATE TABLE webhooks_payloads @@ -93,6 +93,7 @@ COMMENT ON CONSTRAINT force_stytch_id_unique ON users IS 'force unique on stytch + -- ************************************** tcg_users CREATE TABLE tcg_users @@ -103,9 +104,10 @@ CREATE TABLE tcg_users remote_id text NULL, remote_platform text NULL, teams text[] NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_users UNIQUE ( created_at ), CONSTRAINT PK_tcg_users PRIMARY KEY ( id_tcg_user ) ); @@ -126,9 +128,10 @@ CREATE TABLE tcg_teams remote_platform text NULL, name text NULL, description text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_teams UNIQUE ( created_at ), CONSTRAINT PK_tcg_teams PRIMARY KEY ( id_tcg_team ) ); @@ -150,9 +153,10 @@ CREATE TABLE tcg_collections remote_platform text NULL, collection_type text NULL, parent_collection uuid NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NOT NULL, + CONSTRAINT force_createdAt_unique_tcg_collections UNIQUE ( created_at ), CONSTRAINT PK_tcg_collections PRIMARY KEY ( id_tcg_collection ) ); @@ -172,9 +176,10 @@ CREATE TABLE tcg_accounts name text NULL, domains text[] NULL, remote_platform text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, + CONSTRAINT force_createdAt_unique_tcg_accounts UNIQUE ( created_at ), CONSTRAINT PK_tcg_account PRIMARY KEY ( id_tcg_account ) ); @@ -208,6 +213,77 @@ COMMENT ON COLUMN remote_data."format" IS 'can be json, xml'; +-- ************************************** managed_webhooks + +CREATE TABLE managed_webhooks +( + id_managed_webhook uuid NOT NULL, + active boolean NOT NULL, + id_connection uuid NOT NULL, + endpoint uuid NOT NULL, + api_version text NULL, + active_events text[] NULL, + remote_signing_secret text NULL, + modified_at timestamp NOT NULL, + created_at timestamp NOT NULL, + CONSTRAINT PK_managed_webhooks PRIMARY KEY ( id_managed_webhook ) +); + + + +COMMENT ON COLUMN managed_webhooks.endpoint IS 'UUID that will be used in the final URL to help identify where to route data + ex: api.panora.dev/mw/{managed_webhooks.endpoint}'; + + + +-- ************************************** fs_shared_links + +CREATE TABLE fs_shared_links +( + id_fs_shared_link uuid NOT NULL, + created_at timestamp NOT NULL, + modified_at timestamp NOT NULL, + CONSTRAINT PK_fs_shared_links PRIMARY KEY ( id_fs_shared_link ) +); + + +-- ************************************** fs_permissions + +CREATE TABLE fs_permissions +( + id_fs_permission uuid NOT NULL, + remote_id text NULL, + created_at timestamp NOT NULL, + modified_at timestamp NOT NULL, + "user" uuid NOT NULL, + "group" uuid NOT NULL, + type text[] NOT NULL, + roles text[] NOT NULL, + CONSTRAINT PK_fs_permissions PRIMARY KEY ( id_fs_permission ) +); + + + +COMMENT ON COLUMN fs_permissions.roles IS 'read, write, owner'; + + + + + +-- ************************************** fs_drives + +CREATE TABLE fs_drives +( + id_fs_drive uuid NOT NULL, + remote_created_at timestamp NULL, + drive_url text NULL, + created_at timestamp NOT NULL, + modified_at timestamp NOT NULL, + remote_id text NULL, + CONSTRAINT PK_fs_drives PRIMARY KEY ( id_fs_drive ) +); + + -- ************************************** entity CREATE TABLE entity @@ -283,32 +359,28 @@ CREATE TABLE crm_users id_crm_user uuid NOT NULL, name text NULL, email text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, + CONSTRAINT force_createdAt_unique_crm_users UNIQUE ( created_at ), CONSTRAINT PK_crm_users PRIMARY KEY ( id_crm_user ) ); - - - - - - -- ************************************** crm_deals_stages CREATE TABLE crm_deals_stages ( id_crm_deals_stage uuid NOT NULL, stage_name text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, + CONSTRAINT force_createdAt_unique_crm_deals_stages UNIQUE ( created_at ), CONSTRAINT PK_crm_deal_stages PRIMARY KEY ( id_crm_deals_stage ) ); @@ -319,6 +391,25 @@ CREATE TABLE crm_deals_stages +-- ************************************** connector_sets + +CREATE TABLE connector_sets +( + id_connector_set uuid NOT NULL, + crm_hubspot boolean NOT NULL, + crm_zoho boolean NOT NULL, + crm_attio boolean NOT NULL, + crm_pipedrive boolean NOT NULL, + tcg_zendesk boolean NOT NULL, + tcg_jira boolean NOT NULL, + tcg_gorgias boolean NOT NULL, + tcg_gitlab boolean NOT NULL, + tcg_front boolean NOT NULL, + CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) +); + + + -- ************************************** connection_strategies CREATE TABLE connection_strategies @@ -354,7 +445,7 @@ CREATE TABLE tcg_tickets collections text[] NULL, completed_at timestamp NULL, priority text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, assigned_to text[] NULL, remote_id text NULL, @@ -362,6 +453,7 @@ CREATE TABLE tcg_tickets creator_type text NULL, id_tcg_user uuid NULL, id_linked_user uuid NOT NULL, + CONSTRAINT force_createdAt_unique_tcg_tickets UNIQUE ( created_at ), CONSTRAINT PK_tcg_tickets PRIMARY KEY ( id_tcg_ticket ) ); @@ -395,11 +487,12 @@ CREATE TABLE tcg_contacts details text NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, id_tcg_account uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_contact PRIMARY KEY ( id_tcg_contact ), + CONSTRAINT force_createdAt_unique_tcg_contacts UNIQUE ( created_at ), CONSTRAINT FK_49 FOREIGN KEY ( id_tcg_account ) REFERENCES tcg_accounts ( id_tcg_account ) ); @@ -419,16 +512,23 @@ CREATE INDEX FK_tcg_contact_tcg_account_id ON tcg_contacts CREATE TABLE projects ( - id_project uuid NOT NULL, - name text NOT NULL, - sync_mode text NOT NULL, - pull_frequency bigint NULL, - redirect_url text NULL, - id_user uuid NOT NULL, + id_project uuid NOT NULL, + name text NOT NULL, + sync_mode text NOT NULL, + pull_frequency bigint NULL, + redirect_url text NULL, + id_user uuid NOT NULL, + id_connector_set uuid NOT NULL, CONSTRAINT PK_projects PRIMARY KEY ( id_project ), + CONSTRAINT FK_project_connectorsetid FOREIGN KEY ( id_connector_set ) REFERENCES connector_sets ( id_connector_set ), CONSTRAINT FK_46_1 FOREIGN KEY ( id_user ) REFERENCES users ( id_user ) ); +CREATE INDEX FK_connectors_sets ON projects +( + id_connector_set +); + COMMENT ON COLUMN projects.sync_mode IS 'can be realtime or periodic_pull'; @@ -437,24 +537,32 @@ COMMENT ON COLUMN projects.pull_frequency IS 'frequency in seconds for pulls ex 3600 for one hour'; --- ************************************** project_connectors -CREATE TABLE project_connectors +-- ************************************** fs_folders + +CREATE TABLE fs_folders +( + id_fs_folder uuid NOT NULL, + folder_url text NULL, + "size" bigint NULL, + description text NULL, + parent_folder uuid NULL, + remote_id text NULL, + created_at timestamp NOT NULL, + modified_at timestamp NOT NULL, + id_fs_drive uuid NULL, + id_fs_permission uuid NOT NULL, + CONSTRAINT PK_fs_folders PRIMARY KEY ( id_fs_folder ) +); + +CREATE INDEX FK_fs_folder_driveID ON fs_folders +( + id_fs_drive +); + +CREATE INDEX FK_fs_folder_permissionID ON fs_folders ( - id_project_connector uuid NOT NULL, - id_project uuid NOT NULL, - crm_hubspot boolean NOT NULL, - crm_zoho boolean NOT NULL, - crm_zendesk boolean NOT NULL, - crm_pipedrive boolean NOT NULL, - crm_attio boolean NOT NULL, - tcg_zendesk boolean NOT NULL, - tcg_gorgias boolean NOT NULL, - tcg_front boolean NOT NULL, - tcg_jira boolean NOT NULL, - tcg_gitlab boolean NOT NULL, - CONSTRAINT PK_project_connectors PRIMARY KEY ( id_project_connector ), - CONSTRAINT FK_project_connectors FOREIGN KEY ( id_project ) REFERENCES projects ( id_project ) + id_fs_permission ); @@ -465,13 +573,14 @@ CREATE TABLE crm_contacts id_crm_contact uuid NOT NULL, first_name text NOT NULL, last_name text NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NOT NULL, remote_platform text NOT NULL, id_crm_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_crm_contacts PRIMARY KEY ( id_crm_contact ), + CONSTRAINT force_createdAt_unique_crm_contacts UNIQUE ( created_at ), CONSTRAINT FK_23 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); @@ -496,13 +605,14 @@ CREATE TABLE crm_companies name text NULL, industry text NULL, number_of_employees bigint NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NULL, remote_platform text NULL, id_crm_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_crm_companies PRIMARY KEY ( id_crm_company ), + CONSTRAINT force_createdAt_unique_crm_companies UNIQUE ( created_at ), CONSTRAINT FK_24 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); @@ -604,11 +714,12 @@ CREATE TABLE tcg_tags name text NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_tcg_ticket uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_tags PRIMARY KEY ( id_tcg_tag ), + CONSTRAINT force_createdAt_unique_tcg_tags UNIQUE ( created_at ), CONSTRAINT FK_48 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); @@ -634,7 +745,7 @@ CREATE TABLE tcg_comments is_private boolean NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, creator_type text NULL, id_tcg_attachment text[] NULL, @@ -643,6 +754,7 @@ CREATE TABLE tcg_comments id_tcg_user uuid NULL, id_linked_user uuid NULL, CONSTRAINT PK_tcg_comments PRIMARY KEY ( id_tcg_comment ), + CONSTRAINT force_createdAt_unique_tcg_comments UNIQUE ( created_at ), CONSTRAINT FK_41 FOREIGN KEY ( id_tcg_contact ) REFERENCES tcg_contacts ( id_tcg_contact ), CONSTRAINT FK_40_1 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ), CONSTRAINT FK_42 FOREIGN KEY ( id_tcg_user ) REFERENCES tcg_users ( id_tcg_user ) @@ -697,6 +809,41 @@ COMMENT ON COLUMN linked_users.alias IS 'human-readable alias, for UI (ex ACME c +-- ************************************** fs_files + +CREATE TABLE fs_files +( + id_fs_file uuid NOT NULL, + name text NULL, + type text NULL, + "path" text NULL, + mime_type text NULL, + "size" bigint NULL, + remote_id text NULL, + id_fs_folder uuid NULL, + created_at timestamp NOT NULL, + modified_at timestamp NOT NULL, + id_fs_permission uuid NOT NULL, + CONSTRAINT PK_fs_files PRIMARY KEY ( id_fs_file ) +); + +CREATE INDEX FK_fs_file_FolderID ON fs_files +( + id_fs_folder +); + +CREATE INDEX FK_fs_file_permissionID ON fs_files +( + id_fs_permission +); + + + + + + + + -- ************************************** crm_phone_numbers CREATE TABLE crm_phone_numbers @@ -743,7 +890,7 @@ CREATE TABLE crm_engagements subject text NULL, start_at timestamp NULL, end_time timestamp NULL, - created_at timestamp NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NULL, remote_id text NULL, id_linked_user uuid NULL, @@ -751,6 +898,7 @@ CREATE TABLE crm_engagements id_crm_company uuid NULL, id_crm_user uuid NULL, CONSTRAINT PK_crm_engagement PRIMARY KEY ( id_crm_engagement ), + CONSTRAINT force_createdAt_unique_crm_engagements UNIQUE ( created_at ), CONSTRAINT FK_crm_engagement_crm_user FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_29 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); @@ -817,8 +965,8 @@ CREATE TABLE crm_deals id_crm_deal uuid NOT NULL, name text NOT NULL, description text NOT NULL, - amount bigint NOT NULL, - created_at timestamp NOT NULL, + amount bigint NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, remote_id text NULL, remote_platform text NULL, @@ -827,6 +975,7 @@ CREATE TABLE crm_deals id_linked_user uuid NULL, id_crm_company uuid NULL, CONSTRAINT PK_crm_deal PRIMARY KEY ( id_crm_deal ), + CONSTRAINT force_createdAt_unique_crm_deals UNIQUE ( created_at ), CONSTRAINT FK_22 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_21 FOREIGN KEY ( id_crm_deals_stage ) REFERENCES crm_deals_stages ( id_crm_deals_stage ), CONSTRAINT FK_47_1 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) @@ -940,12 +1089,13 @@ CREATE TABLE tcg_attachments file_name text NULL, file_url text NULL, uploader uuid NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_linked_user uuid NULL, id_tcg_ticket uuid NULL, id_tcg_comment uuid NULL, CONSTRAINT PK_tcg_attachments PRIMARY KEY ( id_tcg_attachment ), + CONSTRAINT force_createdAt_unique_tcg_attachments UNIQUE ( created_at ), CONSTRAINT FK_51 FOREIGN KEY ( id_tcg_comment ) REFERENCES tcg_comments ( id_tcg_comment ), CONSTRAINT FK_50 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); @@ -1035,7 +1185,7 @@ CREATE TABLE crm_tasks status text NULL, due_date timestamp NULL, finished_date timestamp NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_crm_user uuid NULL, id_crm_company uuid NULL, @@ -1044,6 +1194,7 @@ CREATE TABLE crm_tasks remote_id text NULL, remote_platform text NULL, CONSTRAINT PK_crm_task PRIMARY KEY ( id_crm_task ), + CONSTRAINT force_createdAt_unique_crm_tasks UNIQUE ( created_at ), CONSTRAINT FK_26 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ), CONSTRAINT FK_25 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_27 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) @@ -1077,7 +1228,7 @@ CREATE TABLE crm_notes ( id_crm_note uuid NOT NULL, content text NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), modified_at timestamp NOT NULL, id_crm_company uuid NULL, id_crm_contact uuid NULL, @@ -1087,6 +1238,7 @@ CREATE TABLE crm_notes remote_platform text NULL, id_crm_user uuid NULL, CONSTRAINT PK_crm_notes PRIMARY KEY ( id_crm_note ), + CONSTRAINT force_createdAt_unique_crm_notes UNIQUE ( created_at ), CONSTRAINT FK_19 FOREIGN KEY ( id_crm_contact ) REFERENCES crm_contacts ( id_crm_contact ), CONSTRAINT FK_18 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ), CONSTRAINT FK_20 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) diff --git a/packages/api/scripts/oauthConnector.js b/packages/api/scripts/oauthConnector.js index 6eb16a701..fee1bd590 100755 --- a/packages/api/scripts/oauthConnector.js +++ b/packages/api/scripts/oauthConnector.js @@ -89,7 +89,7 @@ export class ${providerUpper}ConnectionService implements I${verticalUpper}Conne }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = \`\${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback\`; + const REDIRECT_URI = \`\${this.env.getPanoraBaseUrl()}/connections/oauth/callback\`; const CREDENTIALS = (await this.cService.getCredentials(projectId, this.type)) as OAuth2AuthData; const formData = new URLSearchParams({ diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index 8d2cc1472..bc95a73fc 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -1,22 +1,12 @@ ---INSERT INTO organizations (id_organization, name, stripe_customer_id) VALUES --- ('55222419-795d-4183-8478-361626363e58', 'Acme Inc', 'cust_stripe_acme_56604f75-7bf8-4541-9ab4-5928aade4bb8' ); - 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, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab) VALUES + ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); -INSERT INTO projects (id_project, name, sync_mode, id_user) VALUES - ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b'), - ('4c641a21-a7f8-4ffe-b7e8-e7d32db87557', 'Project 2', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b'), - ('2b198012-c79c-4bb6-971e-9635830e8c15', 'Project 3', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b'); - --- DO $$ --- DECLARE - --org_id UUID; ---BEGIN - --SELECT id_organization INTO org_id FROM organizations WHERE name = 'Acme Inc'; - --INSERT INTO projects (id_project, name, id_organization, sync_mode) VALUES - --('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', org_id, 'pool'), - --('4c641a21-a7f8-4ffe-b7e8-e7d32db87557', 'Project 2', org_id, 'pool'), - --('2b198012-c79c-4bb6-971e-9635830e8c15', 'Project 3', org_id, 'pool'); ---END $$; \ No newline at end of file +INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES + ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'), + ('4c641a21-a7f8-4ffe-b7e8-e7d32db87557', 'Project 2', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b', '852dfff8-ab63-4530-ae49-e4b2924407f8'), + ('2b198012-c79c-4bb6-971e-9635830e8c15', 'Project 3', 'pool', '0ce39030-2901-4c56-8db0-5e326182ec6b', 'aed0f856-f802-4a79-8640-66d441581a99'); diff --git a/packages/api/scripts/seed.testing.ts b/packages/api/scripts/seed.testing.ts deleted file mode 100644 index afb22baa4..000000000 --- a/packages/api/scripts/seed.testing.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { v4 as uuidv4 } from 'uuid'; - -const prisma = new PrismaClient(); - -async function main() { - /*const org = await prisma.organizations.create({ - data: { - id_organization: uuidv4(), - name: `Acme Inc`, - stripe_customer_id: `cust_stripe_acme_${uuidv4()}`, - }, - }); - */ - const user = await prisma.users.create({ - data: { - id_user: uuidv4(), - email: 'audrey@aubry.io', - password_hash: 'password_hashed_her', - first_name: 'audrey', - last_name: 'aubry', - identification_strategy: 'b2c', - }, - }); - - // Seed the `projects` table with 10 projects - const projectsData = Array.from({ length: 3 }).map((_, index) => ({ - id_project: uuidv4(), - name: `Project ${index + 1}`, - id_user: user.id_user, - //id_organization: org.id_organization, - sync_mode: 'pool', - })); - - await prisma.projects.createMany({ - data: projectsData, - }); - - // Seed the `linked_users` table with 10 linked users - /*const linkedUsersData = Array.from({ length: 10 }).map((_, index) => ({ - id_linked_user: uuidv4(), - linked_user_origin_id: `acme_origin_id_${uuidv4()}`, - alias: `Acme Inc`, - id_project: projectsData[index % projectsData.length].id_project, // Circularly assign projects to linked users - })); - - const linked_users = await prisma.linked_users.createMany({ - data: linkedUsersData, - }); - - const providers = [ - 'hubspot', - 'zoho', - 'zendesk', - 'slack', - 'asana', - 'shopify', - 'pipedrive', - 'freshsales', - 'freshbooks', - 'sage', - ]; - - // Seed the `connections` table with 10 connections - const connectionsData = linkedUsersData.map((user, index) => ({ - id_connection: uuidv4(), - status: '2', // 2: RELINK NEEDED - provider_slug: providers[index], - id_linked_user: user.id_linked_user, - id_project: user.id_project, - token_type: 'oauth', - created_at: new Date(), - })); - - const connections = await prisma.connections.createMany({ - data: connectionsData, - }); - - const entitiesData = [ - { - id_entity: uuidv4(), - ressource_owner_id: 'contact', - }, - { - id_entity: uuidv4(), - ressource_owner_id: 'task', - }, - { - id_entity: uuidv4(), - ressource_owner_id: 'company', - }, - { - id_entity: uuidv4(), - ressource_owner_id: 'note', - }, - ]; - - await prisma.entity.createMany({ - data: entitiesData, - }); - - const slugs = [ - { - slug: 'fav_color', - desc: 'favorite color', - origin_field: 'favorite_color', - providerSource: 'hubspot', - }, - { - slug: 'fav_coat', - desc: 'favorite coat', - origin_field: 'best_coat', - providerSource: 'zoho', - }, - { - slug: 'sales_number', - desc: 'number of sales', - origin_field: 'sales_amount', - providerSource: 'shopify', - }, - { - slug: 'pet_number', - desc: 'number of pets', - origin_field: 'pets_amount', - providerSource: 'sage', - }, - ]; - - const attributesData = entitiesData.map((entity, index) => ({ - id_attribute: uuidv4(), - status: 'mapped', - ressource_owner_type: entity.ressource_owner_id, - slug: slugs[index].slug, - description: slugs[index].desc, - data_type: `string`, - remote_id: slugs[index].origin_field, - source: slugs[index].providerSource, - id_entity: entity.id_entity, - scope: `user`, - })); - - await prisma.attribute.createMany({ - data: attributesData, - }); - - const slugs_ = [ - { - slug: 'fav_cake', - desc: 'favorite cake', - origin_field: 'favorite_cake', - providerSource: 'hubspot', - }, - { - slug: 'fav_beanie', - desc: 'favorite beanie', - origin_field: 'best_beanie', - providerSource: 'zoho', - }, - { - slug: 'clicks_number', - desc: 'number of cliks', - origin_field: 'clicks_amount', - providerSource: 'shopify', - }, - { - slug: 'tv_number', - desc: 'number of tv', - origin_field: 'tv_amount', - providerSource: 'sage', - }, - ]; - - const attributesData_ = entitiesData.map((entity, index) => ({ - id_attribute: uuidv4(), - status: 'defined', - ressource_owner_type: entity.ressource_owner_id, - slug: slugs_[index].slug, - description: slugs_[index].desc, - data_type: `string`, - remote_id: slugs_[index].origin_field, - source: slugs_[index].providerSource, - id_entity: entity.id_entity, - scope: `user`, - })); - - await prisma.attribute.createMany({ - data: attributesData_, - }); - - // Seed the `jobs` table with 20 jobs - const jobsData = Array.from({ length: 10 }).map((_, index) => ({ - 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.events.createMany({ - data: jobsData, - skipDuplicates: true, // Set to true to ignore conflicts (optional) - }); - - /*const apiKeysData = Array.from({ length: 4 }).map((_, index) => ({ - id_api_key: uuidv4(), - api_key_hash: `api_key_hashed_${index}`, - id_project: 'e7a741ef-6b5c-46f8-b9a1-55667f3a6c61', - id_user: 'd287cdda-28af-43a3-8d84-b5e22b584826', - })); - await prisma.api_keys.createMany({ - data: apiKeysData, - });*/ -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/packages/api/scripts/seed.webapp.ts b/packages/api/scripts/seed.webapp.ts deleted file mode 100644 index 851e0168c..000000000 --- a/packages/api/scripts/seed.webapp.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { v4 as uuidv4 } from 'uuid'; - -const prisma = new PrismaClient(); - -async function main() { - /*onst org = await prisma.organizations.create({ - data: { - id_organization: `55222419-795d-4183-8478-361626363e58`, - name: `Acme Inc`, - stripe_customer_id: `cust_stripe_acme_56604f75-7bf8-4541-9ab4-5928aade4bb8`, - }, - });*/ - - const user = await prisma.users.create({ - data: { - id_user: `0ce39030-2901-4c56-8db0-5e326182ec6b`, - email: 'audrey@aubry.io', - password_hash: - '$2b$10$Nxcp3x0yDaCrMrhZQ6IiNeqk0BxxDTnfn9iGG2UK5nWMh/UB6LgZu', - first_name: 'audrey', - last_name: 'aubry', - identification_strategy: 'b2c', - //id_organization: '55222419-795d-4183-8478-361626363e58', - }, - }); - - // Seed the `projects` table with 10 projects - const projectsData = Array.from({ length: 3 }).map((_, index) => ({ - id_project: uuidv4(), - name: `Project ${index + 1}`, - //id_organization: org.id_organization, - id_user: user.id_user, - sync_mode: 'pool', - })); - - await prisma.projects.createMany({ - data: projectsData, - }); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/packages/api/src/@core/auth/auth.module.ts b/packages/api/src/@core/auth/auth.module.ts index 655b6ffbc..5fc4de5ee 100644 --- a/packages/api/src/@core/auth/auth.module.ts +++ b/packages/api/src/@core/auth/auth.module.ts @@ -9,6 +9,7 @@ import { ConfigService } from '@nestjs/config'; import { LoggerService } from '@@core/logger/logger.service'; import { AuthController } from './auth.controller'; import { ValidateUserService } from '@@core/utils/services/validateUser.service'; +import { ProjectsService } from '@@core/projects/projects.service'; @Module({ controllers: [AuthController], @@ -19,6 +20,7 @@ import { ValidateUserService } from '@@core/utils/services/validateUser.service' ApiKeyStrategy, PrismaService, ConfigService, + ProjectsService, LoggerService, ValidateUserService, ], diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index 47906ea6a..bfb90b677 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -13,12 +13,13 @@ import { LoggerService } from '@@core/logger/logger.service'; import { handleServiceError } from '@@core/utils/errors'; import { LoginDto } from './dto/login.dto'; import { VerifyUserDto } from './dto/verify-user.dto'; +import { ProjectsService } from '@@core/projects/projects.service'; -//TODO: Ensure the JWT is used for user session authentication and that it's short-lived. @Injectable() export class AuthService { constructor( private prisma: PrismaService, + private projectService: ProjectsService, private jwtService: JwtService, private logger: LoggerService, ) { @@ -103,14 +104,11 @@ export class AuthService { created_at: new Date(), }, }); - const proj = await this.prisma.projects.create({ - data: { - id_project: uuidv4(), - name: 'Project 1', - sync_mode: '', - id_user: user_.id_user, - }, + const pro = await this.projectService.createProject({ + name: 'Project 1', + id_user: user_.id_user, }); + this.logger.log('proj is ' + JSON.stringify(pro)); return user_; } catch (error) { console.log(error); diff --git a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts index c31ab961b..b221efdfb 100644 --- a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts +++ b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts @@ -30,7 +30,7 @@ export class ConnectionsStrategiesService { private prisma: PrismaService, private crypto: EncryptionService, private configService: ConfigService, - ) { } + ) {} async isCustomCredentials(projectId: string, type: string) { const res = await this.prisma.connection_strategies.findFirst({ @@ -82,7 +82,7 @@ export class ConnectionsStrategiesService { id_cs_attribute: uuidv4(), id_cs_entity: entity.id_cs_entity, attribute_slug: attribute_slug, - data_type: 'string', //TODO + data_type: 'string', }, }); const value_ = await this.prisma.cs_values.create({ @@ -230,8 +230,9 @@ export class ConnectionsStrategiesService { data = { ...data, SCOPE: - CONNECTORS_METADATA[vertical.toLowerCase()][provider.toLowerCase()] - .scopes, + CONNECTORS_METADATA[vertical.toLowerCase()][ + provider.toLowerCase() + ].scopes, }; } /*const isSubdomain = needsSubdomain( @@ -337,7 +338,7 @@ export class ConnectionsStrategiesService { values: string[], ) { try { - console.log("In updateAPI xzx") + console.log('In updateAPI xzx'); const cs = await this.prisma.connection_strategies.findFirst({ where: { id_connection_strategy: id_cs, @@ -368,7 +369,7 @@ export class ConnectionsStrategiesService { where: { id_cs_entity: id_cs_entity, attribute_slug: attribute_slug, - data_type: 'string', //TODO + data_type: 'string', }, }); const value_ = await this.prisma.cs_values.updateMany({ @@ -382,8 +383,8 @@ export class ConnectionsStrategiesService { } return cs; } catch (error) { - console.log("Error xzx") - console.log(error) + console.log('Error xzx'); + console.log(error); throw new Error('Update Failed'); } } diff --git a/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts b/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts index dd32819c0..62357bd51 100644 --- a/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts +++ b/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts @@ -57,7 +57,7 @@ export class FreeagentConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts b/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts index 541aa7f14..b07e6ab46 100644 --- a/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts +++ b/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts @@ -57,7 +57,7 @@ export class FreshbooksConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts b/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts index 5923400a9..44048e4fd 100644 --- a/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts +++ b/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts @@ -58,7 +58,7 @@ export class MoneybirdConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts b/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts index 0f9d8da77..91d22333a 100644 --- a/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts +++ b/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts @@ -56,7 +56,7 @@ export class PennylaneConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts b/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts index b36efddb4..cd4647093 100644 --- a/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts +++ b/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts @@ -57,7 +57,7 @@ export class QuickbooksConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts b/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts index 45d1a5318..6fac99ed5 100644 --- a/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts +++ b/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts @@ -57,7 +57,7 @@ export class SageConnectionService implements IAccountingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts b/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts index 29fac39fe..7e695e705 100644 --- a/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts +++ b/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts @@ -63,7 +63,7 @@ export class WaveFinancialConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, @@ -161,7 +161,7 @@ export class WaveFinancialConnectionService this.type, )) as OAuth2AuthData; - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const formData = new URLSearchParams({ grant_type: 'refresh_token', diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 452eb6aba..7260a3aab 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -113,7 +113,12 @@ export class ConnectionsController { break; } // Performing Core Sync Service - this.coreSyncService.initialSync(vertical.toLowerCase(), providerName, linkedUserId, projectId); + this.coreSyncService.initialSync( + vertical.toLowerCase(), + providerName, + linkedUserId, + projectId, + ); res.redirect(returnUrl); } catch (error) { handleServiceError(error, this.logger); diff --git a/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts b/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts index 72a33ff81..223825385 100644 --- a/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts +++ b/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts @@ -61,7 +61,7 @@ export class AcceloConnectionService implements ICrmConnectionService { }); if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts index 21a2bf720..65523aaae 100644 --- a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts +++ b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts @@ -59,7 +59,7 @@ export class AttioConnectionService implements ICrmConnectionService { }); if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/close/close.service.ts b/packages/api/src/@core/connections/crm/services/close/close.service.ts index 0461f0482..89469aebf 100644 --- a/packages/api/src/@core/connections/crm/services/close/close.service.ts +++ b/packages/api/src/@core/connections/crm/services/close/close.service.ts @@ -61,7 +61,7 @@ export class CloseConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/copper/copper.service.ts b/packages/api/src/@core/connections/crm/services/copper/copper.service.ts index d2253e051..88b6d39d0 100644 --- a/packages/api/src/@core/connections/crm/services/copper/copper.service.ts +++ b/packages/api/src/@core/connections/crm/services/copper/copper.service.ts @@ -57,7 +57,7 @@ export class CopperConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, 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 062c5e7e0..0d83c3705 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 @@ -57,7 +57,7 @@ export class HubspotConnectionService implements ICrmConnectionService { }); if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, @@ -142,7 +142,7 @@ export class HubspotConnectionService implements ICrmConnectionService { async handleTokenRefresh(opts: RefreshParams) { try { const { connectionId, refreshToken, projectId } = opts; - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; //tocheck + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; //tocheck const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/crm/services/keap/keap.service.ts b/packages/api/src/@core/connections/crm/services/keap/keap.service.ts index 663d9bee2..e9ccd25ef 100644 --- a/packages/api/src/@core/connections/crm/services/keap/keap.service.ts +++ b/packages/api/src/@core/connections/crm/services/keap/keap.service.ts @@ -57,7 +57,7 @@ export class KeapConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts index 71e7c7901..9c90ffdd1 100644 --- a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts +++ b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts @@ -59,7 +59,7 @@ export class PipedriveConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, @@ -141,7 +141,7 @@ export class PipedriveConnectionService implements ICrmConnectionService { async handleTokenRefresh(opts: RefreshParams) { try { const { connectionId, refreshToken, projectId } = opts; - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts b/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts index 9e343e269..34286d993 100644 --- a/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts +++ b/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts @@ -58,7 +58,7 @@ export class TeamleaderConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts b/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts index 1fe98720e..551f0bb50 100644 --- a/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts +++ b/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts @@ -55,7 +55,7 @@ export class TeamworkConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts index 0d5f20d6c..e5c1729fa 100644 --- a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts @@ -58,7 +58,7 @@ export class ZendeskConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the frontend it must be the same - //const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + //const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; //TODO const REDIRECT_URI = `http://localhost:3000/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( diff --git a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts index b578d5d53..231663d89 100644 --- a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts +++ b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts @@ -91,7 +91,7 @@ export class ZohoConnectionService implements ICrmConnectionService { }); //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, @@ -182,7 +182,7 @@ export class ZohoConnectionService implements ICrmConnectionService { async handleTokenRefresh(opts: RefreshParams) { try { const { connectionId, refreshToken, account_url, projectId } = opts; - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts b/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts index 33eb77b20..6a94cd504 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts @@ -61,7 +61,7 @@ export class GetresponseConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/marketingautomation/services/mailchimp/mailchimp.service.ts b/packages/api/src/@core/connections/marketingautomation/services/mailchimp/mailchimp.service.ts index 355cba371..5b4657df3 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/mailchimp/mailchimp.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/mailchimp/mailchimp.service.ts @@ -60,7 +60,7 @@ export class MailchimpConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts b/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts index 607d0fa60..b835f0059 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts @@ -57,7 +57,7 @@ export class PodiumConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts b/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts index 0b338e910..c57090984 100644 --- a/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts @@ -52,7 +52,7 @@ export class AhaConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts b/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts index d642d3b7a..c08aa68ff 100644 --- a/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts @@ -51,7 +51,7 @@ export class ClickupConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - //const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + //const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/front/front.service.ts b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts index 997c8c05f..ecf33b70f 100644 --- a/packages/api/src/@core/connections/ticketing/services/front/front.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts @@ -54,7 +54,7 @@ export class FrontConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/github/github.service.ts b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts index 53626a9ae..1d8a6c3fc 100644 --- a/packages/api/src/@core/connections/ticketing/services/github/github.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts @@ -21,7 +21,7 @@ export type GithubOAuthResponse = { access_token: string; refresh_token: string; expires_in: string; - refresh_token_expires_in: string; //TODO + refresh_token_expires_in: string; token_type: string; scope: string; }; @@ -56,7 +56,7 @@ export class GithubConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts index 2663f2031..2a30ebdbf 100644 --- a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts @@ -55,7 +55,7 @@ export class GitlabConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts b/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts index 120ce873b..48c9a47e4 100644 --- a/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts @@ -60,7 +60,7 @@ export class GorgiasConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/ticketing/services/jira/jira.service.ts b/packages/api/src/@core/connections/ticketing/services/jira/jira.service.ts index 8f966d934..455a97526 100644 --- a/packages/api/src/@core/connections/ticketing/services/jira/jira.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/jira/jira.service.ts @@ -62,7 +62,7 @@ export class JiraConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/jira_service_mgmt/jira.service.ts b/packages/api/src/@core/connections/ticketing/services/jira_service_mgmt/jira.service.ts index 42b727091..fef87be92 100644 --- a/packages/api/src/@core/connections/ticketing/services/jira_service_mgmt/jira.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/jira_service_mgmt/jira.service.ts @@ -71,7 +71,7 @@ export class JiraServiceMgmtConnectionService }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts b/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts index f8985fe7f..4674a04d2 100644 --- a/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts @@ -54,7 +54,7 @@ export class LinearConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, diff --git a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts index dc4deb376..772abf7a0 100644 --- a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts @@ -16,6 +16,7 @@ import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { ManagedWebhooksService } from '@@core/managed-webhooks/managed-webhooks.service'; export interface ZendeskOAuthResponse { access_token: string; @@ -34,6 +35,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { private cryptoService: EncryptionService, private registry: ServiceRegistry, private cService: ConnectionsStrategiesService, + private mwService: ManagedWebhooksService, ) { this.logger.setContext(ZendeskConnectionService.name); this.registry.registerService('zendesk', this); @@ -52,7 +54,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { }); //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, @@ -67,6 +69,8 @@ export class ZendeskConnectionService implements ITicketingConnectionService { scope: 'read', }); + this.logger.log('Data Form is ' + JSON.stringify(formData)); + const res = await axios.post( `${CREDENTIALS.SUBDOMAIN}/oauth/tokens`, formData.toString(), @@ -129,6 +133,43 @@ export class ZendeskConnectionService implements ITicketingConnectionService { }, }); } + // upsert the creation of a managed webhook + 3rdpartywebhook + if ( + CONNECTORS_METADATA['ticketing']['zendesk'].realTimeWebhookMetadata + .method == 'API' + ) { + const scopes = + CONNECTORS_METADATA['ticketing']['zendesk'].realTimeWebhookMetadata + .events; + const exclude: string[] = [ + 'ticketing.tickets.events', + 'ticketing.comments.events', + 'ticketing.tags.events', + 'ticketing.attachments.events', + ]; + + // Filter the array to exclude specified elements + const filteredEvents = scopes.filter( + (event) => !exclude.includes(event), + ); + + const basic_mw = await this.mwService.createManagedWebhook({ + id_connection: db_res.id_connection, + scopes: filteredEvents, + }); + const trigger_mw = await this.mwService.createManagedWebhook({ + id_connection: db_res.id_connection, + scopes: exclude, + }); + await this.mwService.createRemoteThirdPartyWebhook({ + id_connection: db_res.id_connection, + mw_ids: [basic_mw.id_managed_webhook, trigger_mw.id_managed_webhook], + data: { + name_basic: 'Panora Webhook Events', + name_trigger: 'Panora Tickets Related Events Webhook', + }, + }); + } return db_res; } catch (error) { handleServiceError(error, this.logger, 'zendesk', Action.oauthCallback); diff --git a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts index 70cb5822b..3163b0777 100644 --- a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts +++ b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts @@ -17,9 +17,10 @@ import { GitlabConnectionService } from './services/gitlab/gitlab.service'; import { ClickupConnectionService } from './services/clickup/clickup.service'; import { GorgiasConnectionService } from './services/gorgias/gorgias.service'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ManagedWebhooksModule } from '@@core/managed-webhooks/managed-webhooks.module'; @Module({ - imports: [WebhookModule], + imports: [WebhookModule, ManagedWebhooksModule], providers: [ TicketingConnectionsService, PrismaService, diff --git a/packages/api/src/@core/environment/environment.service.ts b/packages/api/src/@core/environment/environment.service.ts index 391f5ef80..690e75a89 100644 --- a/packages/api/src/@core/environment/environment.service.ts +++ b/packages/api/src/@core/environment/environment.service.ts @@ -29,8 +29,8 @@ export class EnvironmentService { getDatabaseURL(): string { return this.configService.get('DATABASE_URL'); } - getOAuthRredirectBaseUrl(): string { - return this.configService.get('OAUTH_REDIRECT_BASE'); + getPanoraBaseUrl(): string { + return this.configService.get('PANORA_BASE_API_URL'); } getRedisHost(): string { return this.configService.get('REDIS_HOST'); diff --git a/packages/api/src/@core/events/events.controller.ts b/packages/api/src/@core/events/events.controller.ts index c8704c6bb..7bad9b52e 100644 --- a/packages/api/src/@core/events/events.controller.ts +++ b/packages/api/src/@core/events/events.controller.ts @@ -41,7 +41,6 @@ export class EventsController { return await this.eventsService.findEvents(dto, id_project); } - // todo @ApiOperation({ operationId: 'getEventsCount', summary: 'Retrieve Events Count', diff --git a/packages/api/src/@core/managed-webhooks/dto/managed-webhooks.dto.ts b/packages/api/src/@core/managed-webhooks/dto/managed-webhooks.dto.ts new file mode 100644 index 000000000..8381ddb16 --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/dto/managed-webhooks.dto.ts @@ -0,0 +1,18 @@ +export class ManagedWebhooksDto { + id_connection: string; + scopes: string[]; + api_version?: string; + remote_signature_secret?: string; +} + +export class SignatureVerificationDto { + payload: { [key: string]: any }; + signature: string; + secret: string; +} + +export class RemoteThirdPartyCreationDto { + data: { [key: string]: any }; + id_connection: string; + mw_ids: string[]; +} diff --git a/packages/api/src/@core/managed-webhooks/handler/mw-handler.controller.ts b/packages/api/src/@core/managed-webhooks/handler/mw-handler.controller.ts new file mode 100644 index 000000000..26f22549d --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/handler/mw-handler.controller.ts @@ -0,0 +1,33 @@ +import { Body, Controller, Post, Param, Headers } from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { ApiResponse, ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Queue } from 'bull'; +import { InjectQueue } from '@nestjs/bull'; + +@ApiTags('mw') +@Controller('mw') +export class MWHandlerController { + constructor( + @InjectQueue('realTimeWebhookQueue') private queue: Queue, + private loggerService: LoggerService, + ) { + this.loggerService.setContext(MWHandlerController.name); + } + + @ApiOperation({ + operationId: 'handleThirdPartyWebhook', + summary: 'Handle Third Party Webhook', + }) + @ApiResponse({ status: 201 }) + @Post(':endpoint_uuid') + async handleThirdPartyWebhook( + @Body() data: any, + @Headers() headers: any, + @Param('uuid') uuid: string, + ) { + this.loggerService.log( + 'Realtime Webhook Received with Payload ---- ' + JSON.stringify(data), + ); + await this.queue.add({ uuid, data, headers }); + } +} diff --git a/packages/api/src/@core/managed-webhooks/handler/mw-handler.processor.ts b/packages/api/src/@core/managed-webhooks/handler/mw-handler.processor.ts new file mode 100644 index 000000000..97a559afc --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/handler/mw-handler.processor.ts @@ -0,0 +1,66 @@ +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { CrmWebhookHandlerService } from '@crm/@webhook/handler.service'; +import { OnQueueActive, Process, Processor } from '@nestjs/bull'; +import { ConnectorCategory } from '@panora/shared'; +import { TicketingWebhookHandlerService } from '@ticketing/@webhook/handler.service'; +import { Job } from 'bull'; + +@Processor('realTimeWebhookQueue') +export class MwHandlerProcessor { + constructor( + private logger: LoggerService, + private prisma: PrismaService, + private ticketingHandler: TicketingWebhookHandlerService, + private crmHandler: CrmWebhookHandlerService, + ) { + this.logger.setContext(MwHandlerProcessor.name); + } + + @OnQueueActive() + onActive(job: Job) { + this.logger.log(`[Realtime Webhook Queue] Processing job ${job.id} ...`); + } + + @Process({ concurrency: 5 }) + async processReceivedRealtimeWebhooks(job: Job) { + const res = await this.prisma.managed_webhooks.findFirst({ + where: { + endpoint: job.data.uuid, + }, + }); + this.logger.log( + `Start processing incoming realtime-webhook id ${res.id_managed_webhook}...`, + ); + + const connection = await this.prisma.connections.findFirst({ + where: { + id_connection: res.id_connection, + }, + }); + const metadata = { + connector_name: connection.provider_slug, + id_managed_webhook: res.id_managed_webhook, + payload: job.data.data, + headers: job.data.headers, + }; + switch (connection.vertical) { + case ConnectorCategory.Ticketing: + return await this.ticketingHandler.handleExternalIncomingWebhook( + metadata, + ); + case ConnectorCategory.Crm: + return await this.crmHandler.handleExternalIncomingWebhook(metadata); + case ConnectorCategory.Accounting: + return; + case ConnectorCategory.Ats: + return; + case ConnectorCategory.FileStorage: + return; + case ConnectorCategory.Hris: + return; + case ConnectorCategory.MarketingAutomation: + return; + } + } +} diff --git a/packages/api/src/@core/managed-webhooks/managed-webhooks.controller.ts b/packages/api/src/@core/managed-webhooks/managed-webhooks.controller.ts new file mode 100644 index 000000000..aa21185e6 --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/managed-webhooks.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Post, + Put, + Param, + UseGuards, +} from '@nestjs/common'; +import { LoggerService } from '@@core/logger/logger.service'; +import { ApiBody, ApiResponse, ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ManagedWebhooksService } from './managed-webhooks.service'; +import { + ManagedWebhooksDto, + RemoteThirdPartyCreationDto, +} from './dto/managed-webhooks.dto'; +import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; +@ApiTags('managed-webhooks') +@Controller('managed-webhooks') +export class ManagedWebhooksController { + constructor( + private managedWebhookService: ManagedWebhooksService, + private loggerService: LoggerService, + ) { + this.loggerService.setContext(ManagedWebhooksController.name); + } + + @ApiOperation({ + operationId: 'getManagedWebhooks', + summary: 'Retrieve managed webhooks', + }) + @ApiResponse({ status: 200 }) + @UseGuards(JwtAuthGuard) + @Get(':id_connection') + getManagedWebhook(@Param('id_connection') id_connection: string) { + return this.managedWebhookService.getManagedWebhook(id_connection); + } + + @ApiOperation({ + operationId: 'updateManagedWebhooksStatus', + summary: 'Update managed webhook status', + }) + @UseGuards(JwtAuthGuard) + @Put(':id') + async updateManagedWebhooksStatus( + @Param('id') id: string, + @Body('active') active: boolean, + ) { + return this.managedWebhookService.updateStatusManagedWebhookEndpoint( + id, + active, + ); + } + + @ApiOperation({ + operationId: 'createManagedWebhook', + summary: 'Create managed webhook', + }) + @ApiBody({ type: ManagedWebhooksDto }) + @ApiResponse({ status: 201 }) + @UseGuards(JwtAuthGuard) + @Post() + async addManagedWebhook(@Body() data: ManagedWebhooksDto) { + return this.managedWebhookService.createManagedWebhook(data); + } + + @ApiOperation({ + operationId: 'createRemoteThirdPartyWebhook', + summary: 'Create Remote Third Party Webhook', + }) + @ApiBody({ type: RemoteThirdPartyCreationDto }) + @ApiResponse({ status: 201 }) + @UseGuards(JwtAuthGuard) + @Post('remoteThirdPartyCreation') + async createRemoteThirdPartyWebhook( + @Body() data: RemoteThirdPartyCreationDto, + ) { + return this.managedWebhookService.createRemoteThirdPartyWebhook(data); + } +} diff --git a/packages/api/src/@core/managed-webhooks/managed-webhooks.module.ts b/packages/api/src/@core/managed-webhooks/managed-webhooks.module.ts new file mode 100644 index 000000000..876568cef --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/managed-webhooks.module.ts @@ -0,0 +1,34 @@ +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; +import { ManagedWebhooksService } from './managed-webhooks.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { ManagedWebhooksController } from './managed-webhooks.controller'; +import { ValidateUserService } from '@@core/utils/services/validateUser.service'; +import { MWHandlerController } from './handler/mw-handler.controller'; +import { TicketingWebhookHandlerModule } from '@ticketing/@webhook/handler.module'; +import { CrmWebhookHandlerModule } from '@crm/@webhook/handler.module'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'realTimeWebhookQueue', + }), + TicketingWebhookHandlerModule, + CrmWebhookHandlerModule, + ], + controllers: [ManagedWebhooksController, MWHandlerController], + exports: [ + BullModule.registerQueue({ + name: 'realTimeWebhookQueue', + }), + ManagedWebhooksService, + ], + providers: [ + ManagedWebhooksService, + PrismaService, + LoggerService, + ValidateUserService, + ], +}) +export class ManagedWebhooksModule {} diff --git a/packages/api/src/@core/managed-webhooks/managed-webhooks.service.ts b/packages/api/src/@core/managed-webhooks/managed-webhooks.service.ts new file mode 100644 index 000000000..f0366bb6d --- /dev/null +++ b/packages/api/src/@core/managed-webhooks/managed-webhooks.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@nestjs/common'; +import { Queue } from 'bull'; +import { InjectQueue } from '@nestjs/bull'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { v4 as uuidv4 } from 'uuid'; +import { LoggerService } from '@@core/logger/logger.service'; +import { handleServiceError } from '@@core/utils/errors'; +import { + ManagedWebhooksDto, + RemoteThirdPartyCreationDto, +} from './dto/managed-webhooks.dto'; +import crypto from 'crypto'; +import { ConnectorCategory } from '@panora/shared'; +import { TicketingWebhookHandlerService } from '@ticketing/@webhook/handler.service'; + +@Injectable() +export class ManagedWebhooksService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private ticketingHandler: TicketingWebhookHandlerService, + ) { + this.logger.setContext(ManagedWebhooksService.name); + } + + async getManagedWebhook(conn_id: string) { + try { + return await this.prisma.managed_webhooks.findFirst({ + where: { + id_connection: conn_id, + }, + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async updateStatusManagedWebhookEndpoint(id: string, active: boolean) { + try { + return await this.prisma.managed_webhooks.update({ + where: { id_managed_webhook: id }, + data: { active: active }, + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async createManagedWebhook(data: ManagedWebhooksDto) { + try { + return await this.prisma.managed_webhooks.create({ + data: { + id_managed_webhook: uuidv4(), + active: true, + id_connection: data.id_connection, + endpoint: uuidv4(), + api_version: data.api_version || '', + created_at: new Date(), + modified_at: new Date(), + active_events: data.scopes, + remote_signing_secret: data.remote_signature_secret || '', + }, + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async createRemoteThirdPartyWebhook(data: RemoteThirdPartyCreationDto) { + try { + const conn = await this.prisma.connections.findFirst({ + where: { + id_connection: data.id_connection, + }, + }); + switch (conn.vertical) { + case ConnectorCategory.Ticketing: + return await this.ticketingHandler.createExternalWebhook( + data.id_connection, + data.data, + data.mw_ids, + ); + } + } catch (error) { + throw new Error(error); + } + } +} diff --git a/packages/api/src/@core/project-connectors/project-connectors.controller.ts b/packages/api/src/@core/project-connectors/project-connectors.controller.ts index a7cb29f6e..033b48441 100644 --- a/packages/api/src/@core/project-connectors/project-connectors.controller.ts +++ b/packages/api/src/@core/project-connectors/project-connectors.controller.ts @@ -67,8 +67,6 @@ export class ProjectConnectorsController { return await this.projectConnectorsService.createProjectConnectors(data); } - // It should be public API and don't have to add AuthGuard - // TODO: add admin control @ApiOperation({ operationId: 'getConnectorsFromProject', summary: 'Retrieve connectors by Project Id', diff --git a/packages/api/src/@core/project-connectors/project-connectors.service.ts b/packages/api/src/@core/project-connectors/project-connectors.service.ts index ebcc82a2b..067d9a892 100644 --- a/packages/api/src/@core/project-connectors/project-connectors.service.ts +++ b/packages/api/src/@core/project-connectors/project-connectors.service.ts @@ -19,12 +19,17 @@ export class ProjectConnectorsService { id_project: string, ) { try { - const existingPConnectors = - await this.prisma.project_connectors.findFirst({ - where: { - id_project: id_project, - }, - }); + const project = await this.prisma.projects.findFirst({ + where: { + id_project: id_project, + }, + }); + + const existingPConnectors = await this.prisma.connector_sets.findFirst({ + where: { + id_connector_set: project.id_connector_set, + }, + }); if (!existingPConnectors) { throw new Error( @@ -36,9 +41,9 @@ export class ProjectConnectorsService { [column]: status, // Use computed property names to set the column dynamically }; - const res = await this.prisma.project_connectors.update({ + const res = await this.prisma.connector_sets.update({ where: { - id_project_connector: existingPConnectors.id_project_connector, + id_connector_set: existingPConnectors.id_connector_set, }, data: updateData, }); @@ -51,8 +56,7 @@ export class ProjectConnectorsService { async createProjectConnectors(data: TypeCustom) { try { const updateData: any = { - id_project_connector: uuidv4(), - id_project: data.id_project, + id_connector_set: uuidv4(), crm_hubspot: data.crm_hubspot, crm_zoho: data.crm_zoho, crm_zendesk: data.crm_zendesk, @@ -65,7 +69,7 @@ export class ProjectConnectorsService { tcg_gitlab: data.tcg_gitlab, }; - const res = await this.prisma.project_connectors.create({ + const res = await this.prisma.connector_sets.create({ data: updateData, }); return res; @@ -86,9 +90,9 @@ export class ProjectConnectorsService { throw new NotFoundException('Project does not exist!'); } - const res = await this.prisma.project_connectors.findFirst({ + const res = await this.prisma.connector_sets.findFirst({ where: { - id_project: id_project, + id_connector_set: project.id_connector_set, }, }); if (!res) { diff --git a/packages/api/src/@core/projects/projects.service.ts b/packages/api/src/@core/projects/projects.service.ts index 86900fe23..5d0f54362 100644 --- a/packages/api/src/@core/projects/projects.service.ts +++ b/packages/api/src/@core/projects/projects.service.ts @@ -4,6 +4,11 @@ import { LoggerService } from '../logger/logger.service'; import { CreateProjectDto } from './dto/create-project.dto'; import { v4 as uuidv4 } from 'uuid'; import { handleServiceError } from '@@core/utils/errors'; +import { + ConnectorCategory, + providersArray, + slugFromCategory, +} from '@panora/shared'; @Injectable() export class ProjectsService { @@ -33,14 +38,33 @@ export class ProjectsService { async createProject(data: CreateProjectDto) { try { - // const { id_organization, ...rest } = data; + const ACTIVE_CONNECTORS = providersArray(); + // update project-connectors table for the project + const updateData: any = { + id_connector_set: uuidv4(), + }; + + ACTIVE_CONNECTORS.forEach((connector) => { + if (connector.vertical) { + // Construct the property name using the vertical name + const propertyName = `${slugFromCategory( + connector.vertical as ConnectorCategory, + )}_`; + // Add the property to updateData with a value of true + updateData[propertyName + connector.name] = true; + } + }); + const cSet = await this.prisma.connector_sets.create({ + data: updateData, + }); + const res = await this.prisma.projects.create({ data: { name: data.name, sync_mode: 'pool', id_project: uuidv4(), id_user: data.id_user, - //id_organization: id_organization, + id_connector_set: cSet.id_connector_set, }, }); return res; diff --git a/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts b/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts new file mode 100644 index 000000000..7da1509fc --- /dev/null +++ b/packages/api/src/@core/utils/dtos/fetch-objects-query.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty, ApiQuery } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator' + + +// To provide a default pageSize +const DEFAULT_PAGE_SIZE = 50; + +export class FetchObjectsQueryDto { + + @ApiProperty({ + name: 'remote_data', + description: 'Set to true to include data from the original software.', + required: false + }) + @IsOptional() + @Transform(({ value }) => value === 'true' ? true : value === 'false' ? false : value) + @IsBoolean() + remote_data: boolean; + + @ApiProperty({ + name: 'pageSize', + required: false, + description: 'Set to get the number of records.' + }) + @IsOptional() + @IsNumber() + @Transform(p => Number(p.value)) + pageSize: number = DEFAULT_PAGE_SIZE; + + @ApiProperty({ + name: 'cursor', + required: false, + description: 'Set to get the number of records after this cursor.' + }) + @IsOptional() + @Transform(p => Buffer.from(p.value, 'base64').toString()) + @IsUUID() + cursor: string; + +} + + + diff --git a/packages/api/src/@core/utils/types/index.ts b/packages/api/src/@core/utils/types/index.ts index 3c28069c2..5f53b99c6 100644 --- a/packages/api/src/@core/utils/types/index.ts +++ b/packages/api/src/@core/utils/types/index.ts @@ -1,6 +1,6 @@ import { CrmObject, UnifiedCrm } from '@crm/@lib/@types'; import { HrisObject } from '@hris/@lib/@types'; -import { AtsObject, UnifiedAts } from '@ats/@lib/@types'; +import { AtsObject } from '@ats/@lib/@types'; import { AccountingObject } from '@accounting/@lib/@types'; import { TicketingObject, UnifiedTicketing } from '@ticketing/@lib/@types'; import { @@ -14,7 +14,7 @@ import { Type, applyDecorators } from '@nestjs/common'; import { FileStorageObject } from '@filestorage/@lib/@types'; import { MarketingAutomationObject } from '@marketingautomation/@lib/@types'; -export type Unified = UnifiedCrm | UnifiedAts | UnifiedTicketing; +export type Unified = UnifiedCrm | UnifiedTicketing; export type UnifyReturnType = Unified | Unified[]; export type TargetObject = | CrmObject diff --git a/packages/api/src/@core/webhook/webhook.processor.ts b/packages/api/src/@core/webhook/webhook.processor.ts index d287cec77..f4444662c 100644 --- a/packages/api/src/@core/webhook/webhook.processor.ts +++ b/packages/api/src/@core/webhook/webhook.processor.ts @@ -18,7 +18,7 @@ export class WebhookProcessor { @OnQueueActive() onActive(job: Job) { - this.logger.log(`Processing job ${job.id} ...`); + this.logger.log(`[Panora Webhook Queue] Processing job ${job.id} ...`); } @Process({ concurrency: 5 }) diff --git a/packages/api/src/@core/webhook/webhook.service.ts b/packages/api/src/@core/webhook/webhook.service.ts index 3404157f8..83d4725a8 100644 --- a/packages/api/src/@core/webhook/webhook.service.ts +++ b/packages/api/src/@core/webhook/webhook.service.ts @@ -118,7 +118,7 @@ export class WebhookService { id_webhook_endpoint: webhook.id_webhook_endpoint, status: 'queued', // queued | processed | failed | success id_webhooks_payload: w_payload.id_webhooks_payload, - attempt_count: 0, //TODO + attempt_count: 0, }, }); this.logger.log('adding webhook to the queue '); @@ -174,7 +174,7 @@ export class WebhookService { id_webhook_endpoint: webhook.id_webhook_endpoint, status: 'processed', // queued | processed | failed | success id_webhooks_payload: w_payload.id_webhooks_payload, - attempt_count: 0, //TODO + attempt_count: 0, }, }); this.logger.log('sending the webhook to the client '); diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index 1792a80ba..24bc7d123 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -19,6 +19,7 @@ import { BullModule } from '@nestjs/bull'; import { TicketingModule } from '@ticketing/ticketing.module'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; + @Module({ imports: [ CoreModule, @@ -68,9 +69,9 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; password: process.env.REDIS_PASS, username: process.env.REDIS_USER || 'default', db: Number(process.env.REDIS_DB) || 0, - enableReadyCheck: false, - maxRetriesPerRequest: null, - tls: process.env.REDIS_TLS ? { rejectUnauthorized: false } : undefined, + //enableReadyCheck: false, + //maxRetriesPerRequest: null, + tls: process.env.REDIS_TLS ? { rejectUnauthorized: false } : undefined }, }), ], diff --git a/packages/api/src/ats/@lib/@types/index.ts b/packages/api/src/ats/@lib/@types/index.ts index 61153b770..c52257e7e 100644 --- a/packages/api/src/ats/@lib/@types/index.ts +++ b/packages/api/src/ats/@lib/@types/index.ts @@ -1,3 +1 @@ export enum AtsObject {} - -export type UnifiedAts = ''; //TODO; diff --git a/packages/api/src/crm/@lib/@utils/index.ts b/packages/api/src/crm/@lib/@utils/index.ts index c82972e32..921fa64c1 100644 --- a/packages/api/src/crm/@lib/@utils/index.ts +++ b/packages/api/src/crm/@lib/@utils/index.ts @@ -84,7 +84,8 @@ export class Utils { id_crm_user: uuid, }, }); - if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -98,7 +99,8 @@ export class Utils { id_crm_user: uuid, }, }); - if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_user not found for uuid ${uuid}`); + if (!res) return; return res; } catch (error) { throw new Error(error); @@ -113,10 +115,12 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) return; - /*TODO: throw new Error( - `crm_user not found for remote_id ${remote_id} and integration ${remote_platform}`, - );*/ + if (!res) { + // throw new Error( + // `crm_user not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + return; + } return res.id_crm_user; } catch (error) { throw new Error(error); @@ -164,7 +168,10 @@ export class Utils { id_crm_company: uuid, }, }); - if (!res) return; //throw new Error(`crm_companies not found for uuid ${uuid}`); + if (!res) { + // throw new Error(`crm_companies not found for uuid ${uuid}`); + return; + } return res.remote_id; } catch (error) { throw new Error(error); @@ -179,10 +186,12 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) return; - /*throw new Error( - `crm_companies not found for remote_id ${remote_id} and integration ${remote_platform}`, - );*/ + if (!res) { + return; + // throw new Error( + // `crm_companies not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + } return res.id_crm_company; } catch (error) { throw new Error(error); @@ -196,7 +205,8 @@ export class Utils { id_crm_deals_stage: uuid, }, }); - if (!res) throw new Error(`crm_deals_stages not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_deals_stages not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -211,10 +221,12 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) return; - /*TODO: throw new Error( - `crm_deals_stages not found for remote_id ${remote_id} and integration ${remote_platform}`, - );*/ + if (!res) { + return; + // throw new Error( + // `crm_deals_stages not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + } return res.id_crm_deals_stage; } catch (error) { throw new Error(error); @@ -228,7 +240,8 @@ export class Utils { id_crm_contact: uuid, }, }); - if (!res) throw new Error(`crm_contacts not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_contacts not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -243,10 +256,11 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) - throw new Error( - `crm_contacts not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + // if (!res) + // throw new Error( + // `crm_contacts not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + if (!res) return; return res.id_crm_contact; } catch (error) { throw new Error(error); @@ -260,7 +274,8 @@ export class Utils { id_crm_deal: uuid, }, }); - if (!res) throw new Error(`crm_deals not found for uuid ${uuid}`); + // if (!res) throw new Error(`crm_deals not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -275,10 +290,11 @@ export class Utils { remote_platform: remote_platform, }, }); - if (!res) - throw new Error( - `crm_deals not found for remote_id ${remote_id} and integration ${remote_platform}`, - ); + // if (!res) + // throw new Error( + // `crm_deals not found for remote_id ${remote_id} and integration ${remote_platform}`, + // ); + if (!res) return; return res.id_crm_deal; } catch (error) { throw new Error(error); @@ -303,7 +319,7 @@ export class Utils { return priority === 'High' ? 'HIGH' : priority === 'Medium' - ? 'MEDIUM' - : 'LOW'; + ? 'MEDIUM' + : 'LOW'; } } diff --git a/packages/api/src/crm/@webhook/handler.module.ts b/packages/api/src/crm/@webhook/handler.module.ts new file mode 100644 index 000000000..365862039 --- /dev/null +++ b/packages/api/src/crm/@webhook/handler.module.ts @@ -0,0 +1,20 @@ +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { Module } from '@nestjs/common'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { CrmWebhookHandlerService } from './handler.service'; + +@Module({ + imports: [], + providers: [ + PrismaService, + LoggerService, + EncryptionService, + EnvironmentService, + CrmWebhookHandlerService, + /* PROVIDERS SERVICES */ + ], + exports: [LoggerService, PrismaService, CrmWebhookHandlerService], +}) +export class CrmWebhookHandlerModule {} diff --git a/packages/api/src/crm/@webhook/handler.service.ts b/packages/api/src/crm/@webhook/handler.service.ts new file mode 100644 index 000000000..9a66b849c --- /dev/null +++ b/packages/api/src/crm/@webhook/handler.service.ts @@ -0,0 +1,38 @@ +import { LoggerService } from '@@core/logger/logger.service'; +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; + +@Injectable() +export class CrmWebhookHandlerService { + constructor(private logger: LoggerService, private prisma: PrismaService) { + this.logger.setContext(CrmWebhookHandlerService.name); + } + + async createExternalWebhook( + id_connection: string, + data: { [key: string]: any }, + mw_ids: string[], + ) { + const conn = await this.prisma.connections.findFirst({ + where: { + id_connection: id_connection, + }, + }); + switch (conn.provider_slug) { + default: + return; + } + } + + async handleExternalIncomingWebhook(metadata: { + connector_name: string; + id_managed_webhook: string; + payload: any; + headers: any; + }) { + switch (metadata.connector_name) { + default: + return; + } + } +} diff --git a/packages/api/src/crm/company/company.controller.ts b/packages/api/src/crm/company/company.controller.ts index c11446c7b..a04e3034d 100644 --- a/packages/api/src/crm/company/company.controller.ts +++ b/packages/api/src/crm/company/company.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -18,6 +20,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { CompanyService } from './services/company.service'; @@ -26,7 +29,9 @@ import { UnifiedCompanyOutput, } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/companies') @Controller('crm/companies') export class CompanyController { @@ -49,28 +54,26 @@ export class CompanyController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedCompanyOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getCompanies( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.companyService.getCompanies( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/company/services/company.service.ts b/packages/api/src/crm/company/services/company.service.ts index 404322087..156a31cda 100644 --- a/packages/api/src/crm/company/services/company.service.ts +++ b/packages/api/src/crm/company/services/company.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCompanyInput, @@ -113,12 +113,10 @@ export class CompanyService { // add the company inside our db const source_company = resp.data; const target_company = unifiedObject[0]; - const originId = - 'id' in source_company ? String(source_company.id) : undefined; //TODO const existingCompany = await this.prisma.crm_companies.findFirst({ where: { - remote_id: originId, + remote_id: target_company.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -253,7 +251,7 @@ export class CompanyService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_company.remote_id, remote_platform: integrationId, }; @@ -456,10 +454,36 @@ export class CompanyService { async getCompanies( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCompanyOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const companies = await this.prisma.crm_companies.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_companies.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_company: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let companies = await this.prisma.crm_companies.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_company: cursor + } : undefined, + orderBy: { + modified_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -471,6 +495,15 @@ export class CompanyService { }, }); + if (companies.length === (pageSize + 1)) { + next_cursor = Buffer.from(companies[companies.length - 1].id_crm_company).toString('base64'); + companies.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedCompanies: UnifiedCompanyOutput[] = await Promise.all( companies.map(async (company) => { const values = await this.prisma.value.findMany({ @@ -550,7 +583,11 @@ export class CompanyService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/company/services/hubspot/index.ts b/packages/api/src/crm/company/services/hubspot/index.ts index 8187a6c4e..3c2a0b667 100644 --- a/packages/api/src/crm/company/services/hubspot/index.ts +++ b/packages/api/src/crm/company/services/hubspot/index.ts @@ -102,6 +102,7 @@ export class HubspotService implements ICompanyService { }); this.logger.log(`Synced hubspot companies !`); + return { data: resp.data.results, message: 'Hubspot companies retrieved', diff --git a/packages/api/src/crm/company/sync/sync.service.ts b/packages/api/src/crm/company/sync/sync.service.ts index 93bbf8a15..48b59dadb 100644 --- a/packages/api/src/crm/company/sync/sync.service.ts +++ b/packages/api/src/crm/company/sync/sync.service.ts @@ -70,25 +70,14 @@ export class SyncService implements OnModuleInit { async syncCompanies(user_id?: string) { try { this.logger.log(`Syncing companies....`); - // TODO: insert inside sync_jobs table ? - /* - { - "common_object": "company", - "vertical": "crm", - "last_sync_start": "", - "next_sync_start": "", - "status": "SYNCING", - "is_initial_sync": true, - } - */ const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -364,7 +353,7 @@ export class SyncService implements OnModuleInit { const uuid = uuidv4(); let data: any = { id_crm_company: uuid, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index b3eaed11a..3518a35fd 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -8,6 +8,8 @@ import { Param, UseGuards, Headers, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ContactService } from './services/contact.service'; import { LoggerService } from '@@core/logger/logger.service'; @@ -22,11 +24,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiCustomResponse } from '@@core/utils/types'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; + +@ApiBearerAuth('JWT') @ApiTags('crm/contacts') @Controller('crm/contacts') export class ContactController { @@ -49,28 +55,26 @@ export class ContactController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original CRM software.', - }) @ApiCustomResponse(UnifiedContactOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getContacts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.contactService.getContacts( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 1467fd3da..e3f17000a 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -11,7 +11,7 @@ import { UnifiedContactOutput, } from '@crm/contact/types/model.unified'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; @@ -112,16 +112,10 @@ export class ContactService { // add the contact inside our db const source_contact = resp.data; const target_contact = unifiedObject[0]; - const originId = - 'id' in source_contact - ? String(source_contact.id) - : 'contact_id' in source_contact - ? String(source_contact.contact_id) - : undefined; const existingContact = await this.prisma.crm_contacts.findFirst({ where: { - remote_id: originId, + remote_id: target_contact.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -253,7 +247,7 @@ export class ContactService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_contact.remote_id, remote_platform: integrationId, }; @@ -492,12 +486,37 @@ export class ContactService { async getContacts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedContactOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const contacts = await this.prisma.crm_contacts.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_contacts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_contact: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let contacts = await this.prisma.crm_contacts.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_contact: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -509,6 +528,15 @@ export class ContactService { }, }); + if (contacts.length === (pageSize + 1)) { + next_cursor = Buffer.from(contacts[contacts.length - 1].id_crm_contact).toString('base64'); + contacts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( contacts.map(async (contact) => { // Fetch field mappings for the contact @@ -587,7 +615,11 @@ export class ContactService { id_linked_user: linkedUserId, }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/contact/sync/sync.service.ts b/packages/api/src/crm/contact/sync/sync.service.ts index b4485acf0..0adc59b52 100644 --- a/packages/api/src/crm/contact/sync/sync.service.ts +++ b/packages/api/src/crm/contact/sync/sync.service.ts @@ -68,7 +68,7 @@ export class SyncService implements OnModuleInit { }, ) .then(() => { - console.log('Job added successfully'); + console.log('Crm Sync Contact Job added successfully'); }) .catch((error) => { console.error('Failed to add job', error); @@ -85,12 +85,12 @@ export class SyncService implements OnModuleInit { const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -369,7 +369,7 @@ export class SyncService implements OnModuleInit { id_crm_contact: uuid, first_name: '', last_name: '', - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/crm.module.ts b/packages/api/src/crm/crm.module.ts index 3edf3c72a..a6ea7ffff 100644 --- a/packages/api/src/crm/crm.module.ts +++ b/packages/api/src/crm/crm.module.ts @@ -7,6 +7,7 @@ import { StageModule } from './stage/stage.module'; import { TaskModule } from './task/task.module'; import { UserModule } from './user/user.module'; import { CompanyModule } from './company/company.module'; +import { CrmWebhookHandlerModule } from './@webhook/handler.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { CompanyModule } from './company/company.module'; StageModule, TaskModule, UserModule, + CrmWebhookHandlerModule, ], providers: [], exports: [ @@ -29,6 +31,7 @@ import { CompanyModule } from './company/company.module'; StageModule, TaskModule, UserModule, + CrmWebhookHandlerModule, ], }) export class CrmModule {} diff --git a/packages/api/src/crm/deal/deal.controller.ts b/packages/api/src/crm/deal/deal.controller.ts index 2c464ac9b..5d5a6c2bf 100644 --- a/packages/api/src/crm/deal/deal.controller.ts +++ b/packages/api/src/crm/deal/deal.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -18,12 +20,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { DealService } from './services/deal.service'; import { UnifiedDealInput, UnifiedDealOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/deals') @Controller('crm/deals') export class DealController { @@ -46,25 +51,21 @@ export class DealController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedDealOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getDeals( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.dealService.getDeals(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.dealService.getDeals(remoteSource, linkedUserId, pageSize, remote_data, cursor); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/deal/services/deal.service.ts b/packages/api/src/crm/deal/services/deal.service.ts index c6f511049..64d3d30dc 100644 --- a/packages/api/src/crm/deal/services/deal.service.ts +++ b/packages/api/src/crm/deal/services/deal.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedDealInput, UnifiedDealOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -118,11 +118,10 @@ export class DealService { // add the deal inside our db const source_deal = resp.data; const target_deal = unifiedObject[0]; - const originId = 'id' in source_deal ? String(source_deal.id) : undefined; //TODO const existingDeal = await this.prisma.crm_deals.findFirst({ where: { - remote_id: originId, + remote_id: target_deal.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -171,7 +170,7 @@ export class DealService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_deal.remote_id, remote_platform: integrationId, description: '', }; @@ -319,16 +318,51 @@ export class DealService { async getDeals( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedDealOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const deals = await this.prisma.crm_deals.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_deals.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_deal: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let deals = await this.prisma.crm_deals.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_deal: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (deals.length === (pageSize + 1)) { + next_cursor = Buffer.from(deals[deals.length - 1].id_crm_deal).toString('base64'); + deals.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedDeals: UnifiedDealOutput[] = await Promise.all( deals.map(async (deal) => { // Fetch field mappings for the ticket @@ -399,7 +433,11 @@ export class DealService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/deal/services/hubspot/index.ts b/packages/api/src/crm/deal/services/hubspot/index.ts index 7e9672693..5c9ed96d3 100644 --- a/packages/api/src/crm/deal/services/hubspot/index.ts +++ b/packages/api/src/crm/deal/services/hubspot/index.ts @@ -53,8 +53,11 @@ export class HubspotService implements IDealService { }, }, ); + + this.logger.log(`Synced hubspot deals !`); + return { - data: resp.data, + data: resp.data.results, message: 'Hubspot deal created', statusCode: 201, }; diff --git a/packages/api/src/crm/deal/services/hubspot/mappers.ts b/packages/api/src/crm/deal/services/hubspot/mappers.ts index 2172e042c..ba2eae95b 100644 --- a/packages/api/src/crm/deal/services/hubspot/mappers.ts +++ b/packages/api/src/crm/deal/services/hubspot/mappers.ts @@ -101,11 +101,17 @@ export class HubspotDealMapper implements IDealMapper { } } + if (deal.properties.amount) { + opts = { + ...opts, + amount: parseFloat(deal.properties.amount) + } + } + return { remote_id: deal.id, name: deal.properties.dealname, description: deal.properties.dealname, // Placeholder if there's no direct mapping - amount: parseFloat(deal.properties.amount), //TODO; stage_id: deal.properties.dealstage, field_mappings, ...opts, diff --git a/packages/api/src/crm/deal/sync/sync.service.ts b/packages/api/src/crm/deal/sync/sync.service.ts index eae790bc4..ebd069aec 100644 --- a/packages/api/src/crm/deal/sync/sync.service.ts +++ b/packages/api/src/crm/deal/sync/sync.service.ts @@ -50,13 +50,20 @@ export class SyncService implements OnModuleInit { } } // Add new job to the queue with a CRON expression - await this.syncQueue.add( - jobName, - {}, - { - repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight - }, - ); + await this.syncQueue + .add( + jobName, + {}, + { + repeat: { cron: '*/2 * * * *' }, // Runs once a day at midnight + }, + ) + .then(() => { + console.log('Crm Sync Deals Job added successfully'); + }) + .catch((error) => { + console.error('Failed to add job', error); + }); } //function used by sync worker which populate our crm_deals table //its role is to fetch all deals from providers 3rd parties and save the info inside our db @@ -67,12 +74,12 @@ export class SyncService implements OnModuleInit { this.logger.log(`Syncing deals....`); const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -159,7 +166,7 @@ export class SyncService implements OnModuleInit { ); const sourceObject: OriginalDealOutput[] = resp.data; - //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + // this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); //unify the data according to the target obj wanted const unifiedObject = (await unify({ sourceObject, @@ -265,11 +272,10 @@ export class SyncService implements OnModuleInit { this.logger.log('deal not exists'); let data: any = { id_crm_deal: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, description: '', - amount: deal.amount, remote_id: originId, remote_platform: originSource, }; diff --git a/packages/api/src/crm/engagement/engagement.controller.ts b/packages/api/src/crm/engagement/engagement.controller.ts index 57daa5a10..dd869c283 100644 --- a/packages/api/src/crm/engagement/engagement.controller.ts +++ b/packages/api/src/crm/engagement/engagement.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; @@ -18,6 +20,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { EngagementService } from './services/engagement.service'; @@ -26,7 +29,9 @@ import { UnifiedEngagementOutput, } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/engagements') @Controller('crm/engagements') export class EngagementController { @@ -49,28 +54,27 @@ export class EngagementController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedEngagementOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getEngagements( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; + return this.engagementService.getEngagements( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/engagement/services/engagement.service.ts b/packages/api/src/crm/engagement/services/engagement.service.ts index e3293499a..83921540c 100644 --- a/packages/api/src/crm/engagement/services/engagement.service.ts +++ b/packages/api/src/crm/engagement/services/engagement.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedEngagementInput, @@ -124,8 +124,8 @@ export class EngagementService { type === 'CALL' ? CrmObject.engagement_call : type === 'MEETING' - ? CrmObject.engagement_meeting - : CrmObject.engagement_email; + ? CrmObject.engagement_meeting + : CrmObject.engagement_email; //unify the data according to the target obj wanted const unifiedObject = (await unify({ @@ -139,12 +139,10 @@ export class EngagementService { // add the engagement inside our db const source_engagement = resp.data; const target_engagement = unifiedObject[0]; - const originId = - 'id' in source_engagement ? String(source_engagement.id) : undefined; //TODO const existingEngagement = await this.prisma.crm_engagements.findFirst({ where: { - remote_id: originId, + remote_id: target_engagement.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -202,7 +200,7 @@ export class EngagementService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_engagement.remote_id, remote_platform: integrationId, }; @@ -372,16 +370,50 @@ export class EngagementService { async getEngagements( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedEngagementOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const engagements = await this.prisma.crm_engagements.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_engagements.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_engagement: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let engagements = await this.prisma.crm_engagements.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_engagement: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (engagements.length === (pageSize + 1)) { + next_cursor = Buffer.from(engagements[engagements.length - 1].id_crm_engagement).toString('base64'); + engagements.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedEngagements: UnifiedEngagementOutput[] = await Promise.all( engagements.map(async (engagement) => { // Fetch field mappings for the ticket @@ -460,7 +492,11 @@ export class EngagementService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/engagement/sync/sync.service.ts b/packages/api/src/crm/engagement/sync/sync.service.ts index bc535c672..034c7de98 100644 --- a/packages/api/src/crm/engagement/sync/sync.service.ts +++ b/packages/api/src/crm/engagement/sync/sync.service.ts @@ -280,7 +280,7 @@ export class SyncService implements OnModuleInit { this.logger.log('engagement not exists'); let data: any = { id_crm_engagement: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/note/note.controller.ts b/packages/api/src/crm/note/note.controller.ts index 76572964e..036e39aa8 100644 --- a/packages/api/src/crm/note/note.controller.ts +++ b/packages/api/src/crm/note/note.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { LoggerService } from '@@core/logger/logger.service'; @@ -17,12 +19,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { NoteService } from './services/note.service'; import { UnifiedNoteInput, UnifiedNoteOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/notes') @Controller('crm/notes') export class NoteController { @@ -45,25 +50,27 @@ export class NoteController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedNoteOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getNotes( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.noteService.getNotes(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.noteService.getNotes( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/note/services/note.service.ts b/packages/api/src/crm/note/services/note.service.ts index 96890e31f..3e23c8fc6 100644 --- a/packages/api/src/crm/note/services/note.service.ts +++ b/packages/api/src/crm/note/services/note.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedNoteInput, UnifiedNoteOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -141,11 +141,10 @@ export class NoteService { // add the note inside our db const source_note = resp.data; const target_note = unifiedObject[0]; - const originId = 'id' in source_note ? String(source_note.id) : undefined; //TODO const existingNote = await this.prisma.crm_notes.findFirst({ where: { - remote_id: originId, + remote_id: target_note.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -189,7 +188,7 @@ export class NoteService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_note.remote_id, remote_platform: integrationId, }; if (target_note.content) { @@ -334,16 +333,50 @@ export class NoteService { async getNotes( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedNoteOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const notes = await this.prisma.crm_notes.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_notes.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_note: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let notes = await this.prisma.crm_notes.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_note: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (notes.length === (pageSize + 1)) { + next_cursor = Buffer.from(notes[notes.length - 1].id_crm_note).toString('base64'); + notes.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedNotes: UnifiedNoteOutput[] = await Promise.all( notes.map(async (note) => { // Fetch field mappings for the ticket @@ -414,7 +447,11 @@ export class NoteService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/note/sync/sync.service.ts b/packages/api/src/crm/note/sync/sync.service.ts index 9ce345aa0..73ce8de09 100644 --- a/packages/api/src/crm/note/sync/sync.service.ts +++ b/packages/api/src/crm/note/sync/sync.service.ts @@ -260,7 +260,7 @@ export class SyncService implements OnModuleInit { this.logger.log('note not exists'); let data: any = { id_crm_note: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/stage/services/stage.service.ts b/packages/api/src/crm/stage/services/stage.service.ts index a7d4d4c48..f34d839cb 100644 --- a/packages/api/src/crm/stage/services/stage.service.ts +++ b/packages/api/src/crm/stage/services/stage.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedStageOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -82,16 +82,50 @@ export class StageService { async getStages( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedStageOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const stages = await this.prisma.crm_deals_stages.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_deals_stages.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_deals_stage: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let stages = await this.prisma.crm_deals_stages.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_deals_stage: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (stages.length === (pageSize + 1)) { + next_cursor = Buffer.from(stages[stages.length - 1].id_crm_deals_stage).toString('base64'); + stages.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedStages: UnifiedStageOutput[] = await Promise.all( stages.map(async (stage) => { // Fetch field mappings for the ticket @@ -158,7 +192,11 @@ export class StageService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/stage/stage.controller.ts b/packages/api/src/crm/stage/stage.controller.ts index 47bfdb12a..c75945ef8 100644 --- a/packages/api/src/crm/stage/stage.controller.ts +++ b/packages/api/src/crm/stage/stage.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; @@ -14,12 +16,15 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { StageService } from './services/stage.service'; import { UnifiedStageOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/stages') @Controller('crm/stages') export class StageController { @@ -42,28 +47,26 @@ export class StageController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedStageOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getStages( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.stageService.getStages( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/crm/stage/sync/sync.service.ts b/packages/api/src/crm/stage/sync/sync.service.ts index 1c3974ed1..a427405f1 100644 --- a/packages/api/src/crm/stage/sync/sync.service.ts +++ b/packages/api/src/crm/stage/sync/sync.service.ts @@ -290,7 +290,7 @@ export class SyncService implements OnModuleInit { this.logger.log('stage not exists'); let data: any = { id_crm_deals_stage: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId || '', diff --git a/packages/api/src/crm/task/services/task.service.ts b/packages/api/src/crm/task/services/task.service.ts index 26e7039e4..a28c05923 100644 --- a/packages/api/src/crm/task/services/task.service.ts +++ b/packages/api/src/crm/task/services/task.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedTaskInput, UnifiedTaskOutput } from '../types/model.unified'; import { desunify } from '@@core/utils/unification/desunify'; @@ -130,11 +130,10 @@ export class TaskService { // add the task inside our db const source_task = resp.data; const target_task = unifiedObject[0]; - const originId = 'id' in source_task ? String(source_task.id) : undefined; //TODO const existingTask = await this.prisma.crm_tasks.findFirst({ where: { - remote_id: originId, + remote_id: target_task.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -187,7 +186,7 @@ export class TaskService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_task.remote_id, remote_platform: integrationId, }; @@ -342,16 +341,50 @@ export class TaskService { async getTasks( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTaskOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const tasks = await this.prisma.crm_tasks.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_tasks.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_task: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tasks = await this.prisma.crm_tasks.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_task: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (tasks.length === (pageSize + 1)) { + next_cursor = Buffer.from(tasks[tasks.length - 1].id_crm_task).toString('base64'); + tasks.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTasks: UnifiedTaskOutput[] = await Promise.all( tasks.map(async (task) => { // Fetch field mappings for the ticket @@ -424,7 +457,11 @@ export class TaskService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/task/sync/sync.service.ts b/packages/api/src/crm/task/sync/sync.service.ts index e33555cf5..01d11fea9 100644 --- a/packages/api/src/crm/task/sync/sync.service.ts +++ b/packages/api/src/crm/task/sync/sync.service.ts @@ -269,7 +269,7 @@ export class SyncService implements OnModuleInit { this.logger.log('task not exists'); let data: any = { id_crm_task: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/task/task.controller.ts b/packages/api/src/crm/task/task.controller.ts index 52e20f9ce..2be474389 100644 --- a/packages/api/src/crm/task/task.controller.ts +++ b/packages/api/src/crm/task/task.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,13 +19,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TaskService } from './services/task.service'; import { UnifiedTaskInput, UnifiedTaskOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/tasks') @Controller('crm/tasks') export class TaskController { @@ -46,25 +51,28 @@ export class TaskController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedTaskOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTasks( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.taskService.getTasks(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + + return this.taskService.getTasks( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/crm/user/services/user.service.ts b/packages/api/src/crm/user/services/user.service.ts index 93b1fb52f..38f47183a 100644 --- a/packages/api/src/crm/user/services/user.service.ts +++ b/packages/api/src/crm/user/services/user.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedUserOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -89,16 +89,51 @@ export class UserService { async getUsers( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedUserOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const users = await this.prisma.crm_users.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.crm_users.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_crm_user: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let users = await this.prisma.crm_users.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_crm_user: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (users.length === (pageSize + 1)) { + next_cursor = Buffer.from(users[users.length - 1].id_crm_user).toString('base64'); + users.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedUsers: UnifiedUserOutput[] = await Promise.all( users.map(async (user) => { // Fetch field mappings for the ticket @@ -166,7 +201,11 @@ export class UserService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/crm/user/sync/sync.service.ts b/packages/api/src/crm/user/sync/sync.service.ts index f5832a08c..a4917c417 100644 --- a/packages/api/src/crm/user/sync/sync.service.ts +++ b/packages/api/src/crm/user/sync/sync.service.ts @@ -252,7 +252,7 @@ export class SyncService implements OnModuleInit { this.logger.log('user not exists'); let data: any = { id_crm_user: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/crm/user/user.controller.ts b/packages/api/src/crm/user/user.controller.ts index cd21c6cfe..e23c60371 100644 --- a/packages/api/src/crm/user/user.controller.ts +++ b/packages/api/src/crm/user/user.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { UserService } from './services/user.service'; import { UnifiedUserOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('crm/users') @Controller('crm/users') export class UserController { @@ -42,25 +47,27 @@ export class UserController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: 'Set to true to include data from the original Crm software.', - }) @ApiCustomResponse(UnifiedUserOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getUsers( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.userService.getUsers(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.userService.getUsers( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index 61b1ccaf2..2decb23af 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -13,6 +13,10 @@ async function bootstrap() { .setTitle('Unified Panora API') .setDescription('The Panora API description') .setVersion('1.0') + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'JWT' + ) .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/packages/api/src/ticketing/@lib/@utils/index.ts b/packages/api/src/ticketing/@lib/@utils/index.ts index a2fc9daa2..625e4f968 100644 --- a/packages/api/src/ticketing/@lib/@utils/index.ts +++ b/packages/api/src/ticketing/@lib/@utils/index.ts @@ -37,7 +37,8 @@ export class Utils { id_tcg_user: uuid, }, }); - if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -66,7 +67,8 @@ export class Utils { id_tcg_contact: uuid, }, }); - if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); @@ -97,14 +99,18 @@ export class Utils { id_tcg_user: uuid, }, }); - if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_user not found for uuid ${uuid}`); + if (!res) return; return res.email_address; } catch (error) { throw new Error(error); } } - async getCollectionUuidFromRemoteId(remote_id: string, remote_platform: string) { + async getCollectionUuidFromRemoteId( + remote_id: string, + remote_platform: string, + ) { try { const res = await this.prisma.tcg_collections.findFirst({ where: { @@ -155,7 +161,8 @@ export class Utils { id_tcg_ticket: uuid, }, }); - if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + // if (!res) throw new Error(`tcg_contact not found for uuid ${uuid}`); + if (!res) return; return res.remote_id; } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/@webhook/handler.module.ts b/packages/api/src/ticketing/@webhook/handler.module.ts new file mode 100644 index 000000000..8d7be7332 --- /dev/null +++ b/packages/api/src/ticketing/@webhook/handler.module.ts @@ -0,0 +1,27 @@ +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { Module } from '@nestjs/common'; +import { TicketingWebhookHandlerService } from './handler.service'; +import { ZendeskHandlerService } from './zendesk/handler'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; + +@Module({ + imports: [], + providers: [ + PrismaService, + LoggerService, + EncryptionService, + EnvironmentService, + TicketingWebhookHandlerService, + /* PROVIDERS SERVICES */ + ZendeskHandlerService, + ], + exports: [ + LoggerService, + PrismaService, + ZendeskHandlerService, + TicketingWebhookHandlerService, + ], +}) +export class TicketingWebhookHandlerModule {} diff --git a/packages/api/src/ticketing/@webhook/handler.service.ts b/packages/api/src/ticketing/@webhook/handler.service.ts new file mode 100644 index 000000000..65fae3d96 --- /dev/null +++ b/packages/api/src/ticketing/@webhook/handler.service.ts @@ -0,0 +1,51 @@ +import { LoggerService } from '@@core/logger/logger.service'; +import { Injectable } from '@nestjs/common'; +import { ZendeskHandlerService } from './zendesk/handler'; +import { PrismaService } from '@@core/prisma/prisma.service'; + +@Injectable() +export class TicketingWebhookHandlerService { + constructor( + private logger: LoggerService, + private prisma: PrismaService, + private zendesk: ZendeskHandlerService, + ) { + this.logger.setContext(TicketingWebhookHandlerService.name); + } + + async createExternalWebhook( + id_connection: string, + data: { [key: string]: any }, + mw_ids: string[], + ) { + const conn = await this.prisma.connections.findFirst({ + where: { + id_connection: id_connection, + }, + }); + switch (conn.provider_slug) { + case 'zendesk': + return await this.zendesk.createWebhook(data, mw_ids); + default: + return; + } + } + + async handleExternalIncomingWebhook(metadata: { + connector_name: string; + id_managed_webhook: string; + payload: any; + headers: any; + }) { + switch (metadata.connector_name) { + case 'zendesk': + return await this.zendesk.handler( + metadata.payload, + metadata.headers, + metadata.id_managed_webhook, + ); + default: + return; + } + } +} diff --git a/packages/api/src/ticketing/@webhook/zendesk/handler.ts b/packages/api/src/ticketing/@webhook/zendesk/handler.ts new file mode 100644 index 000000000..2b49f5bb0 --- /dev/null +++ b/packages/api/src/ticketing/@webhook/zendesk/handler.ts @@ -0,0 +1,281 @@ +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { Payload } from './types'; +import { mapToRemoteEvent } from './utils'; +import * as crypto from 'crypto'; + +@Injectable() +export class ZendeskHandlerService { + constructor( + private logger: LoggerService, + private prisma: PrismaService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + ) { + this.logger.setContext(ZendeskHandlerService.name); + } + + async createWebhook(data: { [key: string]: any }, mw_ids: string[]) { + if (mw_ids[0]) { + await this.createBasicWebhook(data.name_basic, mw_ids[0]); + } + if (mw_ids[1]) { + await this.createTriggerWebhook(data.name_trigger, mw_ids[1]); + } + } + + async createBasicWebhook(webhook_name: string, mw_id: string) { + const mw = await this.prisma.managed_webhooks.findUnique({ + where: { + id_managed_webhook: mw_id, + }, + }); + const conn = await this.prisma.connections.findUnique({ + where: { + id_connection: mw.id_connection, + }, + }); + const unified_events = mw.active_events; + + const events_ = unified_events + .flatMap((event) => mapToRemoteEvent(event)) + .filter((item) => item !== null && item !== undefined); + + const body_data = { + webhook: { + name: webhook_name, + status: 'active', + endpoint: `${this.env.getPanoraBaseUrl()}/mw/${mw.endpoint}`, + http_method: 'POST', + request_format: 'json', + subscriptions: events_, + }, + }; + + this.logger.log('Creating basic webhook... '); + + const resp = await axios.post( + `${conn.account_url}/webhooks`, + JSON.stringify(body_data), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + conn.access_token, + )}`, + }, + }, + ); + + this.logger.log( + 'Zendesk basic webhook created ' + JSON.stringify(resp.data), + ); + + this.logger.log('Fetching basic webhook secret... '); + + const webhook_result = await axios.get( + `${conn.account_url}/webhooks/${resp.data.webhook.id}/signing_secret`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + conn.access_token, + )}`, + }, + }, + ); + //update signing secret inside mw table + await this.prisma.managed_webhooks.update({ + where: { + id_managed_webhook: mw.id_managed_webhook, + }, + data: { + remote_signing_secret: webhook_result.data.signing_secret.secret, + }, + }); + } + + async createTriggerWebhook(webhook_name: string, mw_id: string) { + const mw = await this.prisma.managed_webhooks.findUnique({ + where: { + id_managed_webhook: mw_id, + }, + }); + const conn = await this.prisma.connections.findUnique({ + where: { + id_connection: mw.id_connection, + }, + }); + const body_data = { + webhook: { + name: webhook_name, + status: 'active', + endpoint: `${this.env.getPanoraBaseUrl()}/mw/${mw.endpoint}`, + http_method: 'POST', + request_format: 'json', + subscriptions: ['conditional_ticket_events'], + }, + }; + + this.logger.log('Creating trigger webhook... '); + const resp = await axios.post( + `${conn.account_url}/webhooks`, + JSON.stringify(body_data), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + conn.access_token, + )}`, + }, + }, + ); + + this.logger.log( + 'Zendesk trigger webhook created ' + JSON.stringify(resp.data), + ); + + // create trigger webhook + const b_ = { + trigger: { + actions: [ + { + field: 'notification_webhook', + value: [ + resp.data.webhook.id, + ` + { + "id_ticket": "{{ticket.id}}" + } + `, + ], + }, + ], + conditions: { + any: [ + { + field: 'assignee_id', + operator: 'changed', + }, + { + field: 'attachment', + operator: 'is', + value: 'present', + }, + { + field: 'comment_is_public', + value: 'true', + }, + { + field: 'priority', + operator: 'changed', + }, + { + field: 'status', + value: 'changed', + }, + { + field: 'update_type', + value: 'Create', + }, + { + field: 'update_type', + value: 'Change', + }, + { + field: 'cc', + operator: 'is', + value: 'present', + }, + { + field: 'type', + operator: 'changed', + }, + ], + }, + title: 'Trigger Webhooks', + }, + }; + const trigger_result = await axios.post( + `${conn.account_url}/triggers.json`, + JSON.stringify(b_), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + conn.access_token, + )}`, + }, + }, + ); + + this.logger.log('Fetching trigger webhook secret... '); + const webhook_result = await axios.get( + `${conn.account_url}/webhooks/${resp.data.webhook.id}/signing_secret`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + conn.access_token, + )}`, + }, + }, + ); + //update signing secret inside mw table + await this.prisma.managed_webhooks.update({ + where: { + id_managed_webhook: mw.id_managed_webhook, + }, + data: { + remote_signing_secret: webhook_result.data.signing_secret.secret, + }, + }); + } + + async handler(payload: Payload, headers: any, id_managed_webhook: string) { + try { + await this.verifyWebhookAuthenticity( + headers['x-zendesk-webhook-signature'], + headers['x-zendesk-webhook-signature-timestamp'], + payload, + id_managed_webhook, + ); + if ('ticketId' in payload) { + // ticket payload + // TODO:update the tickzt inside our db + } else { + //non-ticket payload + } + } catch (error) { + throw new Error(error); + } + } + + async verifyWebhookAuthenticity( + signature: string, + timestamp: string, + body: any, + id_managed_webhook: string, + ) { + try { + const res = await this.prisma.managed_webhooks.findFirst({ + where: { + id_managed_webhook: id_managed_webhook, + }, + }); + const SIGNING_SECRET_ALGORITHM = 'sha256'; + const hmac = crypto.createHmac( + SIGNING_SECRET_ALGORITHM, + res.remote_signing_secret, + ); + const sig = hmac.update(timestamp + body).digest('base64'); + return Buffer.compare(Buffer.from(signature), Buffer.from(sig)) === 0; + } catch (error) { + throw new Error(error); + } + } +} diff --git a/packages/api/src/ticketing/@webhook/zendesk/types.ts b/packages/api/src/ticketing/@webhook/zendesk/types.ts new file mode 100644 index 000000000..03575c36c --- /dev/null +++ b/packages/api/src/ticketing/@webhook/zendesk/types.ts @@ -0,0 +1,21 @@ +export type NonTicketPayload = { + type: string; + account_id: number; + id: string; + time: string; + zendesk_event_version: string; + subject: string; + detail: { + [key: string]: any; + id: string; + }; + event: { + [key: string]: any; + }; +}; + +export type TicketPayload = { + ticket_id: string; +}; + +export type Payload = NonTicketPayload | TicketPayload; diff --git a/packages/api/src/ticketing/@webhook/zendesk/utils.ts b/packages/api/src/ticketing/@webhook/zendesk/utils.ts new file mode 100644 index 000000000..aef06badc --- /dev/null +++ b/packages/api/src/ticketing/@webhook/zendesk/utils.ts @@ -0,0 +1,28 @@ +export const mapToRemoteEvent = (unified_event: string) => { + switch (unified_event) { + case 'ticketing.accounts.events': + return [ + 'zen:event-type:organization.created', + 'zen:event-type:organization.custom_field_changed', + 'zen:event-type:organization.deleted', + 'zen:event-type:organization.external_id_changed', + 'zen:event-type:organization.name_changed', + 'zen:event-type:organization.tags_changed', + ]; + case 'ticketing.users.events': + return [ + 'zen:event-type:user.alias_changed', + 'zen:event-type:user.created', + 'zen:event-type:user.custom_field_changed', + 'zen:event-type:user.deleted', + 'zen:event-type:user.role_changed', + 'zen:event-type:user.custom_role_changed', + 'zen:event-type:user.organization_membership_created', + 'zen:event-type:user.organization_membership_deleted', + 'zen:event-type:user.name_changed', + 'zen:event-type:user.details_changed', + ]; + case 'ticketing.contacts.events': + return ['zen:event-type:user.custom_role_changed']; + } +}; diff --git a/packages/api/src/ticketing/account/account.controller.ts b/packages/api/src/ticketing/account/account.controller.ts index 2bfa586a1..f7ce70933 100644 --- a/packages/api/src/ticketing/account/account.controller.ts +++ b/packages/api/src/ticketing/account/account.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { AccountService } from './services/account.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedAccountOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/accounts') @Controller('ticketing/accounts') export class AccountController { @@ -42,29 +47,26 @@ export class AccountController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedAccountOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getAccounts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.accountService.getAccounts( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/account/services/account.service.ts b/packages/api/src/ticketing/account/services/account.service.ts index 529a89239..b9cedab96 100644 --- a/packages/api/src/ticketing/account/services/account.service.ts +++ b/packages/api/src/ticketing/account/services/account.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedAccountOutput } from '../types/model.unified'; @Injectable() @@ -79,18 +79,52 @@ export class AccountService { async getAccounts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedAccountOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const accounts = await this.prisma.tcg_accounts.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_accounts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_account: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let accounts = await this.prisma.tcg_accounts.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_account: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (accounts.length === (pageSize + 1)) { + next_cursor = Buffer.from(accounts[accounts.length - 1].id_tcg_account).toString('base64'); + accounts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedAccounts: UnifiedAccountOutput[] = await Promise.all( accounts.map(async (account) => { // Fetch field mappings for the account @@ -157,7 +191,11 @@ export class AccountService { id_linked_user: linkedUserId, }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index ca6ac5e12..d97246851 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -245,7 +245,7 @@ export class SyncService implements OnModuleInit { id_tcg_account: uuidv4(), name: account.name, domains: account.domains, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/attachment/attachment.controller.ts b/packages/api/src/ticketing/attachment/attachment.controller.ts index 0d3d08c07..b92e3bb62 100644 --- a/packages/api/src/ticketing/attachment/attachment.controller.ts +++ b/packages/api/src/ticketing/attachment/attachment.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -16,6 +18,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { AttachmentService } from './services/attachment.service'; @@ -25,7 +28,9 @@ import { } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/attachments') @Controller('ticketing/attachments') export class AttachmentController { @@ -48,29 +53,27 @@ export class AttachmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedAttachmentOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getAttachments( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; + return this.attachmentService.getAttachments( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/attachment/services/attachment.service.ts b/packages/api/src/ticketing/attachment/services/attachment.service.ts index ea1649abf..8736e2fbc 100644 --- a/packages/api/src/ticketing/attachment/services/attachment.service.ts +++ b/packages/api/src/ticketing/attachment/services/attachment.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedAttachmentInput, @@ -205,17 +205,50 @@ export class AttachmentService { async getAttachments( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedAttachmentOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const attachments = await this.prisma.tcg_attachments.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_attachments.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_attachment: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + let attachments = await this.prisma.tcg_attachments.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_attachment: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (attachments.length === (pageSize + 1)) { + next_cursor = Buffer.from(attachments[attachments.length - 1].id_tcg_attachment).toString('base64'); + attachments.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedAttachments: UnifiedAttachmentOutput[] = await Promise.all( attachments.map(async (attachment) => { // Fetch field mappings for the attachment @@ -285,7 +318,11 @@ export class AttachmentService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/collection/collection.controller.ts b/packages/api/src/ticketing/collection/collection.controller.ts index f895182ce..4ff55e22c 100644 --- a/packages/api/src/ticketing/collection/collection.controller.ts +++ b/packages/api/src/ticketing/collection/collection.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,6 +19,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { CollectionService } from './services/collection.service'; @@ -26,7 +29,9 @@ import { } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/collections') @Controller('ticketing/collections') export class CollectionController { @@ -49,29 +54,26 @@ export class CollectionController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedCollectionOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getCollections( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.collectionService.getCollections( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/collection/services/collection.service.ts b/packages/api/src/ticketing/collection/services/collection.service.ts index 698785f1e..243ec0f97 100644 --- a/packages/api/src/ticketing/collection/services/collection.service.ts +++ b/packages/api/src/ticketing/collection/services/collection.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCollectionOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @@ -65,17 +65,51 @@ export class CollectionService { async getCollections( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCollectionOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - console.log("In collection service : ", integrationId) - const collections = await this.prisma.tcg_collections.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_collections.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_collection: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let collections = await this.prisma.tcg_collections.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_collection: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (collections.length === (pageSize + 1)) { + next_cursor = Buffer.from(collections[collections.length - 1].id_tcg_collection).toString('base64'); + collections.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedCollections: UnifiedCollectionOutput[] = await Promise.all( collections.map(async (collection) => { return { @@ -118,7 +152,11 @@ export class CollectionService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/collection/sync/sync.service.ts b/packages/api/src/ticketing/collection/sync/sync.service.ts index 84b70ddb8..c591cfd25 100644 --- a/packages/api/src/ticketing/collection/sync/sync.service.ts +++ b/packages/api/src/ticketing/collection/sync/sync.service.ts @@ -237,7 +237,7 @@ export class SyncService implements OnModuleInit { name: collection.name, description: collection.description, collection_type: collection.collection_type, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/comment/comment.controller.ts b/packages/api/src/ticketing/comment/comment.controller.ts index 3127d2ba1..cc8e0b760 100644 --- a/packages/api/src/ticketing/comment/comment.controller.ts +++ b/packages/api/src/ticketing/comment/comment.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -16,6 +18,7 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { CommentService } from './services/comment.service'; import { @@ -25,7 +28,9 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiCustomResponse } from '@@core/utils/types'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/comments') @Controller('ticketing/comments') export class CommentController { @@ -48,29 +53,26 @@ export class CommentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedCommentOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getComments( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.commentService.getComments( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/comment/services/comment.service.ts b/packages/api/src/ticketing/comment/services/comment.service.ts index bc6babd31..fd34b2f5e 100644 --- a/packages/api/src/ticketing/comment/services/comment.service.ts +++ b/packages/api/src/ticketing/comment/services/comment.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCommentInput, @@ -163,12 +163,10 @@ export class CommentService { // add the comment inside our db const source_comment = resp.data; const target_comment = unifiedObject[0]; - const originId = - 'id' in source_comment ? String(source_comment.id) : undefined; //TODO const existingComment = await this.prisma.tcg_comments.findFirst({ where: { - remote_id: originId, + remote_id: target_comment.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -178,13 +176,13 @@ export class CommentService { const opts = target_comment.creator_type === 'contact' ? { - id_tcg_contact: unifiedCommentData.contact_id, - } + id_tcg_contact: unifiedCommentData.contact_id, + } : target_comment.creator_type === 'user' - ? { + ? { id_tcg_user: unifiedCommentData.user_id, } - : {}; //case where nothing is passed for creator or a not authorized value; + : {}; //case where nothing is passed for creator or a not authorized value; if (existingComment) { // Update the existing comment @@ -222,7 +220,7 @@ export class CommentService { modified_at: new Date(), id_tcg_ticket: unifiedCommentData.ticket_id, id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_comment.remote_id, remote_platform: integrationId, }; @@ -375,16 +373,50 @@ export class CommentService { async getComments( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedCommentOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { - const comments = await this.prisma.tcg_comments.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_comments.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_comment: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let comments = await this.prisma.tcg_comments.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_comment: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (comments.length === (pageSize + 1)) { + next_cursor = Buffer.from(comments[comments.length - 1].id_tcg_comment).toString('base64'); + comments.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedComments: UnifiedCommentOutput[] = await Promise.all( comments.map(async (comment) => { //WE SHOULDNT HAVE FIELD MAPPINGS FOR COMMENT @@ -457,7 +489,11 @@ export class CommentService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 98f13c20a..a1ed3423c 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -350,7 +350,7 @@ export class SyncService implements OnModuleInit { file_name: attchmt.file_name, file_url: attchmt.file_url, id_tcg_comment: unique_ticketing_comment_id, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), uploader: linkedUserId, //TODO id_tcg_ticket: id_ticket, diff --git a/packages/api/src/ticketing/contact/contact.controller.ts b/packages/api/src/ticketing/contact/contact.controller.ts index 7de05fe31..a6ff6d586 100644 --- a/packages/api/src/ticketing/contact/contact.controller.ts +++ b/packages/api/src/ticketing/contact/contact.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiHeader, ApiTags, + ApiBearerAuth, } from '@nestjs/swagger'; import { ContactService } from './services/contact.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedContactOutput } from './types/model.unified'; import { ApiCustomResponse } from '@@core/utils/types'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/contacts') @Controller('ticketing/contacts') export class ContactController { @@ -42,29 +47,26 @@ export class ContactController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedContactOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getContacts( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.contactService.getContacts( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/contact/services/contact.service.ts b/packages/api/src/ticketing/contact/services/contact.service.ts index 345bd59ab..907b3d415 100644 --- a/packages/api/src/ticketing/contact/services/contact.service.ts +++ b/packages/api/src/ticketing/contact/services/contact.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedContactOutput } from '../types/model.unified'; @Injectable() @@ -81,17 +81,51 @@ export class ContactService { async getContacts( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedContactOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const contacts = await this.prisma.tcg_contacts.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_contacts.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_contact: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let contacts = await this.prisma.tcg_contacts.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_contact: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (contacts.length === (pageSize + 1)) { + next_cursor = Buffer.from(contacts[contacts.length - 1].id_tcg_contact).toString('base64'); + contacts.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedContacts: UnifiedContactOutput[] = await Promise.all( contacts.map(async (contact) => { // Fetch field mappings for the contact @@ -161,7 +195,11 @@ export class ContactService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 937cb48bf..c2a9fbe71 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -278,7 +278,7 @@ export class SyncService implements OnModuleInit { email_address: contact.email_address, phone_number: contact.phone_number, details: contact.details, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/tag/services/tag.service.ts b/packages/api/src/ticketing/tag/services/tag.service.ts index e30eea4e6..70af5063d 100644 --- a/packages/api/src/ticketing/tag/services/tag.service.ts +++ b/packages/api/src/ticketing/tag/services/tag.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedTagOutput } from '../types/model.unified'; @Injectable() @@ -78,17 +78,52 @@ export class TagService { async getTags( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTagOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const tags = await this.prisma.tcg_tags.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_tags.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_tag: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tags = await this.prisma.tcg_tags.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_tag: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (tags.length === (pageSize + 1)) { + next_cursor = Buffer.from(tags[tags.length - 1].id_tcg_tag).toString('base64'); + tags.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTags: UnifiedTagOutput[] = await Promise.all( tags.map(async (tag) => { // Fetch field mappings for the tag @@ -155,7 +190,11 @@ export class TagService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index be264f495..5fc9bc83a 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -263,7 +263,7 @@ export class SyncService implements OnModuleInit { const data = { id_tcg_tag: uuidv4(), name: tag.name, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_tcg_ticket: id_ticket, id_linked_user: linkedUserId, diff --git a/packages/api/src/ticketing/tag/tag.controller.ts b/packages/api/src/ticketing/tag/tag.controller.ts index 30a84d857..6e133146c 100644 --- a/packages/api/src/ticketing/tag/tag.controller.ts +++ b/packages/api/src/ticketing/tag/tag.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TagService } from './services/tag.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedTagOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/tags') @Controller('ticketing/tags') export class TagController { @@ -42,26 +47,27 @@ export class TagController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTagOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTags( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.tagService.getTags(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.tagService.getTags( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/ticketing/team/services/team.service.ts b/packages/api/src/ticketing/team/services/team.service.ts index ee1112b38..faefa11a5 100644 --- a/packages/api/src/ticketing/team/services/team.service.ts +++ b/packages/api/src/ticketing/team/services/team.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedTeamOutput } from '../types/model.unified'; @Injectable() @@ -79,18 +79,52 @@ export class TeamService { async getTeams( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTeamOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const teams = await this.prisma.tcg_teams.findMany({ + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_teams.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_team: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let teams = await this.prisma.tcg_teams.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_team: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (teams.length === (pageSize + 1)) { + next_cursor = Buffer.from(teams[teams.length - 1].id_tcg_team).toString('base64'); + teams.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTeams: UnifiedTeamOutput[] = await Promise.all( teams.map(async (team) => { // Fetch field mappings for the team @@ -158,7 +192,11 @@ export class TeamService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index 024da3505..cc82edac2 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -247,7 +247,7 @@ export class SyncService implements OnModuleInit { id_tcg_team: uuidv4(), name: team.name, description: team.description, - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, diff --git a/packages/api/src/ticketing/team/team.controller.ts b/packages/api/src/ticketing/team/team.controller.ts index 5131451ac..0d2eb2626 100644 --- a/packages/api/src/ticketing/team/team.controller.ts +++ b/packages/api/src/ticketing/team/team.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TeamService } from './services/team.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedTeamOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/teams') @Controller('ticketing/teams') export class TeamController { @@ -42,26 +47,27 @@ export class TeamController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTeamOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTeams( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.teamService.getTeams(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.teamService.getTeams( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/src/ticketing/ticket/services/front/index.ts b/packages/api/src/ticketing/ticket/services/front/index.ts index d59bf7bb8..5e05808a8 100644 --- a/packages/api/src/ticketing/ticket/services/front/index.ts +++ b/packages/api/src/ticketing/ticket/services/front/index.ts @@ -9,7 +9,7 @@ import axios from 'axios'; import { ActionType, handleServiceError } from '@@core/utils/errors'; import { ServiceRegistry } from '../registry.service'; import { FrontTicketInput, FrontTicketOutput } from './types'; -import { Utils } from '@ticketing/@lib/@utils';; +import { Utils } from '@ticketing/@lib/@utils'; @Injectable() export class FrontService implements ITicketService { diff --git a/packages/api/src/ticketing/ticket/services/ticket.service.ts b/packages/api/src/ticketing/ticket/services/ticket.service.ts index ceeb62afc..9c90f862e 100644 --- a/packages/api/src/ticketing/ticket/services/ticket.service.ts +++ b/packages/api/src/ticketing/ticket/services/ticket.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedTicketInput, @@ -144,12 +144,10 @@ export class TicketService { // add the ticket inside our db const source_ticket = resp.data; const target_ticket = unifiedObject[0]; - const originId = - 'id' in source_ticket ? String(source_ticket.id) : undefined; //TODO const existingTicket = await this.prisma.tcg_tickets.findFirst({ where: { - remote_id: originId, + remote_id: target_ticket.remote_id, remote_platform: integrationId, id_linked_user: linkedUserId, }, @@ -206,7 +204,7 @@ export class TicketService { created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, - remote_id: originId, + remote_id: target_ticket.remote_id, remote_platform: integrationId, }; if (target_ticket.name) { @@ -414,11 +412,37 @@ export class TicketService { async getTickets( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedTicketOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const tickets = await this.prisma.tcg_tickets.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_tickets.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_ticket: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let tickets = await this.prisma.tcg_tickets.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_ticket: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, @@ -429,6 +453,15 @@ export class TicketService { },*/ }); + if (tickets.length === (pageSize + 1)) { + next_cursor = Buffer.from(tickets[tickets.length - 1].id_tcg_ticket).toString('base64'); + tickets.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedTickets: UnifiedTicketOutput[] = await Promise.all( tickets.map(async (ticket) => { // Fetch field mappings for the ticket @@ -508,7 +541,11 @@ export class TicketService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/ticket/services/zendesk/index.ts b/packages/api/src/ticketing/ticket/services/zendesk/index.ts index dc315ea30..4e1d93f04 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/index.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/index.ts @@ -125,7 +125,7 @@ export class ZendeskService implements ITicketService { }, }); - const resp = await axios.get(`${connection.account_url}/tickets.json`, { + const resp = await axios.get(`${connection.account_url}/tickets/.json`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cryptoService.decrypt( @@ -150,4 +150,6 @@ export class ZendeskService implements ITicketService { ); } } + + //todo: create a syncTicket(remote_ticket_id) } diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 6cac7cf2a..d63dd428f 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -67,12 +67,12 @@ export class SyncService implements OnModuleInit { this.logger.log(`Syncing tickets....`); const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: user_id, - }, - }), - ] + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { @@ -165,7 +165,6 @@ export class SyncService implements OnModuleInit { customFieldMappings, })) as UnifiedTicketOutput[]; - //insert the data in the DB with the fieldMappings (value table) const tickets_data = await this.saveTicketsInDb( linkedUserId, @@ -271,7 +270,7 @@ export class SyncService implements OnModuleInit { let data: any = { id_tcg_ticket: uuidv4(), - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, remote_id: originId, @@ -302,7 +301,7 @@ export class SyncService implements OnModuleInit { data = { ...data, assigned_to: ticket.assigned_to }; } if (ticket.project_id) { - data = { ...data, collections: [ticket.project_id] } + data = { ...data, collections: [ticket.project_id] }; } /* parent_ticket: ticket.parent_ticket || 'd', diff --git a/packages/api/src/ticketing/ticket/ticket.controller.ts b/packages/api/src/ticketing/ticket/ticket.controller.ts index aff73ebcb..a47199c9c 100644 --- a/packages/api/src/ticketing/ticket/ticket.controller.ts +++ b/packages/api/src/ticketing/ticket/ticket.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -17,13 +19,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { TicketService } from './services/ticket.service'; import { UnifiedTicketInput, UnifiedTicketOutput } from './types/model.unified'; import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/tickets') @Controller('ticketing/tickets') export class TicketController { @@ -46,29 +51,26 @@ export class TicketController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedTicketOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getTickets( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); + const { remote_data, pageSize, cursor } = query; return this.ticketService.getTickets( remoteSource, linkedUserId, + pageSize, remote_data, + cursor ); } catch (error) { throw new Error(error); diff --git a/packages/api/src/ticketing/ticketing.module.ts b/packages/api/src/ticketing/ticketing.module.ts index cedaa0e3f..588df0496 100644 --- a/packages/api/src/ticketing/ticketing.module.ts +++ b/packages/api/src/ticketing/ticketing.module.ts @@ -8,6 +8,7 @@ import { AccountModule } from './account/account.module'; import { TagModule } from './tag/tag.module'; import { TeamModule } from './team/team.module'; import { CollectionModule } from './collection/collection.module'; +import { TicketingWebhookHandlerModule } from './@webhook/handler.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { CollectionModule } from './collection/collection.module'; TagModule, TeamModule, CollectionModule, + TicketingWebhookHandlerModule, ], providers: [], controllers: [], @@ -33,6 +35,7 @@ import { CollectionModule } from './collection/collection.module'; TagModule, TeamModule, CollectionModule, + TicketingWebhookHandlerModule, ], }) export class TicketingModule {} diff --git a/packages/api/src/ticketing/user/services/user.service.ts b/packages/api/src/ticketing/user/services/user.service.ts index 329643462..8a4493c93 100644 --- a/packages/api/src/ticketing/user/services/user.service.ts +++ b/packages/api/src/ticketing/user/services/user.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { handleServiceError } from '@@core/utils/errors'; +import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { UnifiedUserOutput } from '../types/model.unified'; @Injectable() @@ -80,17 +80,52 @@ export class UserService { async getUsers( integrationId: string, linkedUserId: string, + pageSize: number, remote_data?: boolean, - ): Promise { + cursor?: string + ): Promise<{ data: UnifiedUserOutput[], prev_cursor: null | string, next_cursor: null | string }> { try { //TODO: handle case where data is not there (not synced) or old synced - const users = await this.prisma.tcg_users.findMany({ + + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = await this.prisma.tcg_users.findFirst({ + where: { + remote_platform: integrationId.toLowerCase(), + id_linked_user: linkedUserId, + id_tcg_user: cursor + } + }); + if (!isCursorPresent) { + throw new NotFoundError(`The provided cursor does not exist!`); + } + } + + let users = await this.prisma.tcg_users.findMany({ + take: pageSize + 1, + cursor: cursor ? { + id_tcg_user: cursor + } : undefined, + orderBy: { + created_at: 'asc' + }, where: { remote_platform: integrationId.toLowerCase(), id_linked_user: linkedUserId, }, }); + if (users.length === (pageSize + 1)) { + next_cursor = Buffer.from(users[users.length - 1].id_tcg_user).toString('base64'); + users.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + const unifiedUsers: UnifiedUserOutput[] = await Promise.all( users.map(async (user) => { // Fetch field mappings for the user @@ -160,7 +195,11 @@ export class UserService { }, }); - return res; + return { + data: res, + prev_cursor, + next_cursor + }; } catch (error) { handleServiceError(error, this.logger); } diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index 06129515d..c017a83ba 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -251,7 +251,7 @@ export class SyncService implements OnModuleInit { name: user.name, email_address: user.email_address, teams: user.teams || [], - created_at: new Date(), + // created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, // id_tcg_account: user.account_id || '', diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index a4f2447cd..f571bd1eb 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -23,7 +23,6 @@ export class UnifiedUserInput { @IsOptional() teams?: string[]; - //TODO @ApiPropertyOptional({ type: String, description: 'The account or organization the user is part of', diff --git a/packages/api/src/ticketing/user/user.controller.ts b/packages/api/src/ticketing/user/user.controller.ts index ea4c4132c..089b28dd9 100644 --- a/packages/api/src/ticketing/user/user.controller.ts +++ b/packages/api/src/ticketing/user/user.controller.ts @@ -5,6 +5,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { @@ -13,13 +15,16 @@ import { ApiQuery, ApiTags, ApiHeader, + ApiBearerAuth, } from '@nestjs/swagger'; import { ApiCustomResponse } from '@@core/utils/types'; import { UserService } from './services/user.service'; import { ConnectionUtils } from '@@core/connections/@utils'; import { UnifiedUserOutput } from './types/model.unified'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +@ApiBearerAuth('JWT') @ApiTags('ticketing/users') @Controller('ticketing/users') export class UserController { @@ -42,26 +47,27 @@ export class UserController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Ticketing software.', - }) @ApiCustomResponse(UnifiedUserOutput) @UseGuards(ApiKeyAuthGuard) @Get() + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) async getUsers( @Headers('x-connection-token') connection_token: string, - @Query('remote_data') remote_data?: boolean, + @Query() query: FetchObjectsQueryDto, ) { try { const { linkedUserId, remoteSource } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.userService.getUsers(remoteSource, linkedUserId, remote_data); + const { remote_data, pageSize, cursor } = query; + return this.userService.getUsers( + remoteSource, + linkedUserId, + pageSize, + remote_data, + cursor + ); } catch (error) { throw new Error(error); } diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 93f667a72..5e42f2956 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -477,6 +477,128 @@ ] } }, + "/managed-webhooks/{id_connection}": { + "get": { + "operationId": "getManagedWebhooks", + "summary": "Retrieve managed webhooks", + "parameters": [ + { + "name": "id_connection", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "managed-webhooks" + ] + } + }, + "/managed-webhooks/{id}": { + "put": { + "operationId": "updateManagedWebhooksStatus", + "summary": "Update managed webhook status", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "managed-webhooks" + ] + } + }, + "/managed-webhooks": { + "post": { + "operationId": "createManagedWebhook", + "summary": "Create managed webhook", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedWebhooksDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "managed-webhooks" + ] + } + }, + "/managed-webhooks/remoteThirdPartyCreation": { + "post": { + "operationId": "createRemoteThirdPartyWebhook", + "summary": "Create Remote Third Party Webhook", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteThirdPartyCreationDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "managed-webhooks" + ] + } + }, + "/mw/{endpoint_uuid}": { + "post": { + "operationId": "handleThirdPartyWebhook", + "summary": "Handle Third Party Webhook", + "parameters": [ + { + "name": "uuid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "mw" + ] + } + }, "/crm/companies": { "get": { "operationId": "getCompanies", @@ -495,10 +617,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -526,6 +667,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -597,6 +743,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -637,6 +788,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -690,6 +846,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -768,6 +929,11 @@ }, "tags": [ "crm/companies" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -789,10 +955,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -820,6 +1005,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -891,6 +1081,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -920,6 +1115,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -973,6 +1173,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1051,6 +1256,11 @@ }, "tags": [ "crm/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1072,10 +1282,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1103,6 +1332,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1174,6 +1408,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1227,6 +1466,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -1267,6 +1511,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1345,6 +1594,11 @@ }, "tags": [ "crm/deals" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1366,10 +1620,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1397,6 +1670,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1468,6 +1746,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -1508,6 +1791,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1561,6 +1849,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1639,6 +1932,11 @@ }, "tags": [ "crm/engagements" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1660,10 +1958,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1691,6 +2008,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -1762,6 +2084,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1815,6 +2142,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1893,6 +2225,11 @@ }, "tags": [ "crm/notes" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1914,10 +2251,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -1945,6 +2301,11 @@ }, "tags": [ "crm/stages" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -1998,6 +2359,11 @@ }, "tags": [ "crm/stages" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2019,10 +2385,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2050,6 +2435,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -2121,6 +2511,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -2161,6 +2556,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2214,6 +2614,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2292,6 +2697,11 @@ }, "tags": [ "crm/tasks" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2313,10 +2723,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2344,6 +2773,11 @@ }, "tags": [ "crm/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2397,6 +2831,11 @@ }, "tags": [ "crm/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2418,10 +2857,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2449,6 +2907,11 @@ }, "tags": [ "ticketing/accounts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2502,6 +2965,11 @@ }, "tags": [ "ticketing/accounts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2523,10 +2991,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2554,6 +3041,11 @@ }, "tags": [ "ticketing/collections" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2607,6 +3099,11 @@ }, "tags": [ "ticketing/collections" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2628,10 +3125,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2659,6 +3175,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -2730,6 +3251,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2783,6 +3309,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2861,6 +3392,11 @@ }, "tags": [ "ticketing/comments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2882,10 +3418,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -2913,6 +3468,11 @@ }, "tags": [ "ticketing/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2966,6 +3526,11 @@ }, "tags": [ "ticketing/contacts" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -2987,10 +3552,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3018,6 +3602,11 @@ }, "tags": [ "ticketing/tags" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3071,6 +3660,11 @@ }, "tags": [ "ticketing/tags" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3092,10 +3686,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3123,6 +3736,11 @@ }, "tags": [ "ticketing/teams" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3176,6 +3794,11 @@ }, "tags": [ "ticketing/teams" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3197,10 +3820,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3228,6 +3870,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -3299,6 +3946,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] }, "patch": { @@ -3328,6 +3980,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3381,6 +4038,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3459,6 +4121,11 @@ }, "tags": [ "ticketing/tickets" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3480,10 +4147,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3511,6 +4197,11 @@ }, "tags": [ "ticketing/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -3564,6 +4255,11 @@ }, "tags": [ "ticketing/users" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4410,10 +5106,29 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", + "description": "Set to true to include data from the original software.", "schema": { "type": "boolean" } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } } ], "responses": { @@ -4441,6 +5156,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] }, "post": { @@ -4512,6 +5232,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4565,6 +5290,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4618,6 +5348,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } }, @@ -4696,6 +5431,11 @@ }, "tags": [ "ticketing/attachments" + ], + "security": [ + { + "JWT": [] + } ] } } @@ -4709,6 +5449,13 @@ "tags": [], "servers": [], "components": { + "securitySchemes": { + "JWT": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + } + }, "schemas": { "CreateUserDto": { "type": "object", @@ -4850,6 +5597,48 @@ "secret" ] }, + "ManagedWebhooksDto": { + "type": "object", + "properties": { + "id_connection": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "api_version": { + "type": "string" + }, + "remote_signature_secret": { + "type": "string" + } + }, + "required": [ + "id_connection", + "scopes" + ] + }, + "RemoteThirdPartyCreationDto": { + "type": "object", + "properties": { + "id_connection": { + "type": "string" + }, + "mw_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id_connection", + "mw_ids" + ] + }, "ApiResponse": { "type": "object", "properties": { diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 05b74af78..a9ee5c056 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -309,7 +309,19 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKVceZGVM7PbARp_2bjdOICUxlpS5B29UYlurvh6Z2Q&s', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', - authStrategy: AuthStrategy.oauth2 + authStrategy: AuthStrategy.oauth2, + realTimeWebhookMetadata: { + method: 'API', + events: [ + 'ticketing.tickets.events', + 'ticketing.comments.events', + 'ticketing.tags.events', + 'ticketing.attachments.events', + 'ticketing.accounts.events', + 'ticketing.users.events', + 'ticketing.contacts.events', + ] + }, }, 'gorgias': { scopes: 'write:all openid email profile offline', diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 1e830f7e2..fdceeed16 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -20,6 +20,10 @@ export type ProviderConfig = { apiUrl: string; authBaseUrl?: string; // url used to authorize an application on behalf of the user (only when authStrategy is oauth2) customPropertiesUrl?: string; + }; + realTimeWebhookMetadata?: { + method?: 'API' | 'MANUAL'; + events?: string[]; } };