From ef506b96d4f941df602aa176fa2525b70bc8d812 Mon Sep 17 00:00:00 2001 From: nael Date: Tue, 9 Apr 2024 07:09:01 +0200 Subject: [PATCH 1/3] :memo: Up^date doc --- .npmrc | 2 + docs/core-concepts/embedded-catalog.mdx | 4 +- docs/open-source/contributors.mdx | 215 ++++-------------------- packages/api/prisma/schema.prisma | 2 +- packages/api/tsconfig.json | 4 +- packages/shared/.eslintrc.cjs | 7 - packages/shared/package.json | 2 - 7 files changed, 44 insertions(+), 192 deletions(-) create mode 100644 .npmrc delete mode 100644 packages/shared/.eslintrc.cjs diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..a914207cd --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +node-linker=hoisted +package-import-method=clone-or-copy diff --git a/docs/core-concepts/embedded-catalog.mdx b/docs/core-concepts/embedded-catalog.mdx index 183eb42c4..2b0502dc1 100644 --- a/docs/core-concepts/embedded-catalog.mdx +++ b/docs/core-concepts/embedded-catalog.mdx @@ -13,7 +13,8 @@ icon: "wand-magic-sparkles" ## Import our pre-built React Component - You can find the component on NPM [here](https://www.npmjs.com/package/@panora/embedded-card-react) + You can find the component on NPM + [here](https://www.npmjs.com/package/@panora/embedded-card-react) @@ -49,6 +50,7 @@ icon: "wand-magic-sparkles" ```javascript React - - ```bash - cp .env.example .env - ``` - +{" "} - - ```bash - rm -rf node_modules .pnpm-store ./packages/api/dist ./packages/api/node_modules ./apps/webapp/node_modules ./apps/frontend_snippet/node_modules - ``` - +```bash cp .env.example .env ``` - - ```bash - echo -e "node-linker=hoisted\npackage-import-method=clone-or-copy" > .npmrc - ``` - +{" "} + + + ```bash rm -rf node_modules .pnpm-store ./packages/api/dist + ./packages/api/node_modules ./apps/webapp/node_modules + ./apps/frontend_snippet/node_modules ``` + + +{" "} + + + ```bash echo -e "node-linker=hoisted\npackage-import-method=clone-or-copy" > + .npmrc ``` + ```bash docker compose -f docker-compose.dev.yml up ``` - That's all! You can find the backend and other services running at their usual location. Editing code locally will immediately reflect. +That's all! You can find the backend and other services running at their usual location. Editing code locally will immediately reflect. @@ -56,109 +57,30 @@ First choose wisely which vertical the 3rd party belongs to among these: - ticketing - accounting - ats -- file-storage +- file_storage - hris -- marketing-automation +- marketing_automation For the sake of the guide, now on we'll consider adding a 3rd party belonging to the `crm` vertical. -## 1. Adding a new connection service for your 3rd Party +## 1. Look into the `packages/shared/src/utils.ts` file and check if the provider you want to build has its metadata set inside the `providersConfig` object. -Create a new folder with the name of your 3rd party. Let's call it _my3rdParty_. +It should be available (if not contact Panora team) with active field set to false meaning the integration has not been built. -`cd @core/connections/crm/services/my3rdParty` +Actually an integration is built in 2 parts : -Create a new file containing the core logic of your service. +- the authentication part (oauth, api key, basic etc) which is built by the Panora team +- the service integration where the mapping is created with our unified model which is what you'll build -`cd @core/connections/crm/services/my3rdParty/my3rdParty.service.ts` - -It must implement the `ICrmConnectionService` interface. - -```ts -export interface ICrmConnectionService { - handleCallback(opts: CallbackParams): Promise; - handleTokenRefresh(opts: RefreshParams): Promise; -} - -export type CallbackParams = { - linkedUserId: string; - projectId: string; - code: string; - location?: string; //for zoho -}; - -export type RefreshParams = { - connectionId: string; - refreshToken: string; - account_url?: string; -}; -``` - -```ts -import { Injectable } from "@nestjs/common"; -import { - CallbackParams, - ICrmConnectionService, - RefreshParams, -} from "../../types"; - -@Injectable() -export class My3rdPartyConnectionService implements ICrmConnectionService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private env: EnvironmentService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry - ) { - this.logger.setContext(My3rdPartyConnectionService.name); - this.registry.registerService("insert_the_name_of_your_3rd_party", this); - } - - async handleCallback(opts: CallbackParams) { - return; - } - async handleTokenRefresh(opts: RefreshParams) { - return; - } -} -``` - -Now that you have the structure, check other 3rd parties implementations under `/@core/connections/crm/services` to build your functions. - -## 2. Enable your connection service to handle oAuth granting access - -Add your service to the `CrmConnectionModule` under @core/connections/crm/crm.connection.module.ts module ! - -```ts -@Module({ - imports: [WebhookModule], - providers: [ - CrmConnectionsService, - PrismaService, - ServiceConnectionRegistry, - LoggerService, - WebhookService, - EnvironmentService, - EncryptionService, - FreshsalesConnectionService, - HubspotConnectionService, - ZohoConnectionService, - ZendeskConnectionService, - PipedriveConnectionService, - //INSERT YOUR SERVICE HERE - My3rdPartyConnectionService, - ], - exports: [CrmConnectionsService], -}) -export class CrmConnectionModule {} -``` +## 2. Build your provider service # You want to map a common object to your new 3rd Party ? πŸ‘©β€πŸŽ€ _Ie: Contact, Ticket, Deal, Company ..._ -For the sake of this guide, let's map the common object `contact` under `crm` vertical to _my3rdParty_ just defined just before. +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). + +### DISCLAIMER: an integration is considered valid when all common objects have been mapped. Then, after the PR is accepted we'll be able to set `active` field to `true` inside `providersConfig`. ## 1. Add a new service to map your common object to your 3rd party @@ -168,7 +90,7 @@ Create a new service folder with the name of your 3rd party. Let's call it _my3r You'll now create 3 files. -`index.ts` _where your service is created and direct interaction with your 3rd party API is handled_ +`index.ts` \_where your service is created and direct interaction with your 3rd party API is handled It must implement the `IContactService` interface. @@ -197,8 +119,7 @@ export class My3rdPartyService implements IContactService { this.logger.setContext( CrmObject.contact.toUpperCase() + ':' + My3rdPartyService.name, ); - this.registry.registerService('my3rdPartyService', this); - + this.registry.registerService('my3rdParty', this); } async addContact( contactData: 3rdPartyContactInput, @@ -273,80 +194,14 @@ export class My3rdPartyMapper implements IContactMapper { Check other implementations under `/crm/contacts/services` to fill the core functions. -## 2. Enable your service - -`cd crm/contact/types/mappingsTypes.ts` +## 2. Enable your new service -Add your new 3rd party service to the `contactUnificationMapping` object. +To make sure the service is enabled, dependencies and imports must be added. +W ebuilt a script that does it in seconds. -```ts -// ADD YOUR IMPORT HERE -import { My3rdPartyContactMapper } from '@contact/services/my3rdParty/mappers'; - -const hubspotContactMapper = new HubspotContactMapper(); -const zendeskContactMapper = new ZendeskContactMapper(); -const zohoContactMapper = new ZohoContactMapper(); -const pipedriveContactMapper = new PipedriveContactMapper(); -const freshSalesContactMapper = new FreshsalesContactMapper(); -// INSERT BELOW YOUR 3rd PARTY HERE -const my3rdPartyContactMapper = new My3rdPartyContactMapper(); - - -export const contactUnificationMapping = { - hubspot: { - unify: hubspotContactMapper.unify, - desunify: hubspotContactMapper.desunify, - }, - pipedrive: { - unify: pipedriveContactMapper.unify, - desunify: pipedriveContactMapper.desunify, - }, - zoho: { - unify: zohoContactMapper.unify, - desunify: zohoContactMapper.desunify, - }, - ...., - ...., - // INSERT BELOW YOUR 3rd PARTY HERE - my3rdParty: { - unify: my3rdPartyContactMapper.unify, - desunify: my3rdPartyContactMapper.desunify, - }, -}; -``` - -Don't forget to add your service you've defined at step 1 inside the module under `/crm/contacts/contact.module.ts`. +`pnpm run validate-connectors --vertical="crm" --objectType="contact"` -```ts -@Module({ - imports: [ - BullModule.registerQueue({ - name: "webhookDelivery", - }), - ], - controllers: [ContactController], - providers: [ - ContactService, - PrismaService, - LoggerService, - FieldMappingService, - SyncService, - WebhookService, - EncryptionService, - ServiceRegistry, - /* PROVIDERS SERVICES */ - FreshSalesService, - ZendeskService, - ZohoService, - PipedriveService, - HubspotService, - //INSERT YOUR SERVICE HERE - My3rdPartyService, - ], - exports: [SyncService], -}) -export class ContactModule {} -``` +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. ### Congrats Hero ! πŸ¦Έβ€β™€οΈ diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index df5e2acff..0ab263682 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -51,9 +51,9 @@ model connections { expiration_timestamp DateTime? @db.Timestamp(6) created_at DateTime @db.Timestamp(6) connection_token String? + vertical String id_project String @db.Uuid id_linked_user String @db.Uuid - vertical String? linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_11") projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_9") diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 4cc369848..13ab06529 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "moduleResolution": "node", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, @@ -26,6 +27,7 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + //"moduleResolution": "Bundler" } } diff --git a/packages/shared/.eslintrc.cjs b/packages/shared/.eslintrc.cjs deleted file mode 100644 index cc9c00124..000000000 --- a/packages/shared/.eslintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-env node */ -module.exports = { - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - root: true, -}; \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index db5695360..16c5486da 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -10,9 +10,7 @@ "eslint": "^8.55.0", "typescript": "^5.3.3" }, - "type": "module", "dependencies": { - "axios": "^1.5.1", "dotenv": "^16.3.1" } } \ No newline at end of file From 78bd12ec639de632020bf23701791a13c0c1eb09 Mon Sep 17 00:00:00 2001 From: nael Date: Tue, 9 Apr 2024 07:57:17 +0200 Subject: [PATCH 2/3] :memo: Update docs --- docs/open-source/contributors.mdx | 2 +- docs/open-source/glossary.mdx | 140 +++++++++++++++++++++++++ packages/api/scripts/oauthConnector.js | 14 +-- 3 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 docs/open-source/glossary.mdx diff --git a/docs/open-source/contributors.mdx b/docs/open-source/contributors.mdx index 23f8f7b36..24cb13795 100644 --- a/docs/open-source/contributors.mdx +++ b/docs/open-source/contributors.mdx @@ -197,7 +197,7 @@ Check other implementations under `/crm/contacts/services` to fill the core func ## 2. Enable your new service To make sure the service is enabled, dependencies and imports must be added. -W ebuilt a script that does it in seconds. +We built a script that does it in seconds. `pnpm run validate-connectors --vertical="crm" --objectType="contact"` diff --git a/docs/open-source/glossary.mdx b/docs/open-source/glossary.mdx new file mode 100644 index 000000000..866abaf5e --- /dev/null +++ b/docs/open-source/glossary.mdx @@ -0,0 +1,140 @@ +--- +title: "Glossary" +description: "We welcome all contributions to Panora; this glossary is meant for people aiming to build core features" +icon: "star" +--- + +## Introduction + +### The source of truth + +The `/packages/shared/src/utils.ts` file contains the most important object of the project which is `providersConfig`. + +```ts +export const providersConfig: ProvidersConfig = { + 'crm': { + 'hubspot': { + scopes: 'crm.objects.contacts.read crm.objects.contacts.write crm.schemas.deals.read crm.schemas.deals.write crm.objects.deals.read crm.objects.deals.write crm.objects.companies.read crm.objects.companies.write crm.objects.owners.read settings.users.read settings.users.write settings.users.teams.read settings.users.teams.write', + authBaseUrl: 'https://app-eu1.hubspot.com/oauth/authorize', + logoPath: "https://assets-global.website-files.com/6421a177cdeeaf3c6791b745/64d61202dd99e63d40d446f6_hubspot%20logo.png", + description: "Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users", + apiUrl: 'https://api.hubapi.com', + customPropertiesUrl: '/properties/v1/contacts/properties', + authStrategy: AuthStrategy.oauth2 + }, + .... + }, + 'ticketing': { + 'zendesk': { + scopes: 'read write', + authBaseUrl: '/oauth/authorizations/new', + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKVceZGVM7PbARp_2bjdOICUxlpS5B29UYlurvh6Z2Q&s', + description: "Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users", + apiUrl: '/api/v2', + authStrategy: AuthStrategy.oauth2 + }, + .... + }, + .... +} +``` + +where + +- `authBaseUrl` is the api endpoint to authorize oauth connections if the provider has `authStrategy` == `AuthStrategy.oauth2` +- `logoPath` is the logo address displayed across Panora +- `apiUrl`is the base api endpoint used +- `authStrategy` is the authentication strategy used, it is of type authStrategy + +```ts +export enum AuthStrategy { + oauth2, + api_key, + basic, +} +``` + +DISCLAIMER: it may be updated in the future if other auth methods are figured out. + +### The authentication flow + +By default, we focused the core on building the oAuth2 authentication strategy. +But other strategies exist as well. + +All utils data could be found inside `/packages/shared/src/envConfig.ts` + +The important thing to note is the `type` string variable. + +We handle all connections strategies logic in the `packages/api/src/@core/connections-strategies`. + +One may want to call either + +- `https://${PANORA_API_URL}/connections-strategies/getCredentials` (used to get either custom credentials or default managed panora environment variables as a fallback) + or +- `https://${PANORA_API_URL}/connections-strategies/createConnectionStrategy` (used in the webapp to let clients add custom connections credentials) + +Both endpoints ask for a `type` variable argument which is a `string`. + +## IT IS VERY IMPORTANT TO MAKE SURE THE FORMAT CONVENTION IS AS FOLLOWS IN UPPERCASE: + +`{PROVIDERNAME}_{VERTICALNAME}_{SOFTWAREMODE}_{AUTHMODE}` where +`PROVIDERNAME` is the name of the provider +`VERTICALNAME` is the vertical the provider belongs to +`SOFTWAREMODE` is the software mode the client wants to run the provider on (onpremise, cloud). By default, set it to `CLOUD`. +`AUTHMODE` is the authentication strategy of the provider (For now we have => OAUTH, API, BASIC) + +// i.e HUBSPOT_CRM_CLOUD_OAUTH +// i.e ZENDESK_TICKETING_CLOUD_OAUTH + +You'll find this type in .env files, and in some db tables. + +### The authentication contributor guide + +If you don't see your provider inside the `providersConfig` object within the file `/packages/shared/src/utils.ts` then you may get in touch with Panora team members or build it yourself. + +Here are the steps to do so: + +# 1. Add the provider metadata to `providersConfig` object + +Choose the right vertical and add it given that it has to contain these potential fields: + +```ts +export type ProviderConfig = { + scopes: string; + authBaseUrl: string; + logoPath: string; + description: string; + active?: boolean; + apiUrl: string; + customPropertiesUrl?: string; + authStrategy?: AuthStrategy; +}; +``` + +If `authStrategy` is not `AuthStrategy.oauth2`, set `authBaseUrl` to blank. + +NB: Some providers may use subdomains instead of plain auth api endpoints (i.e https://my_panora_domain/auth) +In this case just write `authBaseUrl: /auth` with a starting `/`. +It is important to start with a `/` as our code use that to detect if subomain is needed across the project. + +# 2. Update the construction of the authentication url (used in the frontend) + +Modify the `/packages/shared/src/authUrl.ts` file. + +Right now we only coded the oAuth2 side so we'll focus on it here. + +Update `handleOAuth2Url` function and switch/case found inside if specific params are needed for your provider. + +# 3. Generate boilerplate and code the service to build the connection + +Now you have to build the connection code inside `/packages/api/@core/src/connections/your_vertical/services` + +We have a script that automates all the code + the import across the app. + +Run `pnpm run prebuild-oauth-connector --vertical="crm" --provider="hubspot"` for instance. + +All imports, dependencies, environment variables are handled. + +All you have to do is to check similar services and finish the code. + +NB: If your provider needs a subdomain, you have to manually add the environment variable inside the docker-compose and .env .env.example files. diff --git a/packages/api/scripts/oauthConnector.js b/packages/api/scripts/oauthConnector.js index f7a656cbe..017b46c95 100755 --- a/packages/api/scripts/oauthConnector.js +++ b/packages/api/scripts/oauthConnector.js @@ -220,7 +220,7 @@ export class ${providerUpper}ConnectionService implements I${verticalUpper}Conne } // Function to add provider to EnvironmentService.ts -function addProviderToEnvironmentService(provider, envServicePath) { +/*function addProviderToEnvironmentService(provider, envServicePath) { const providerMethodName_ = provider .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) @@ -250,23 +250,25 @@ function addProviderToEnvironmentService(provider, envServicePath) { fs.writeFileSync(envServicePath, content); console.log(`Added ${providerMethodName}() to EnvironmentService.`); -} +}*/ // Function to add provider to docker-compose.dev.yml function addProviderToDockerCompose(provider, vertical, dockerComposePath) { const providerEnvPrefix = provider.toUpperCase(); const newEnvVariables = ` - ${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_ID: $\{${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_ID} - ${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_SECRET: $\{${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_SECRET} + ${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_ID: $\{${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_ID} + ${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_SECRET: $\{${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_SECRET} `; let content = fs.readFileSync(dockerComposePath, { encoding: 'utf8' }); if ( - content.includes(`${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_ID`) + content.includes( + `${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_ID`, + ) ) { console.log( - `${providerEnvPrefix}_${vertical.toUpperCase()}_CLIENT_ID already exists in docker-compose.dev.yml.`, + `${providerEnvPrefix}_${vertical.toUpperCase()}_CLOUD_CLIENT_ID already exists in docker-compose.dev.yml.`, ); return; } From efa5651e9f2396ae5dea70de52aeb9f359927b53 Mon Sep 17 00:00:00 2001 From: Rachid F <109089247+rflihxyz@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:40:35 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Fix=20on=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmrc | 2 -- docs/open-source/contributors.mdx | 10 +++------- 2 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index a914207cd..000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -node-linker=hoisted -package-import-method=clone-or-copy diff --git a/docs/open-source/contributors.mdx b/docs/open-source/contributors.mdx index 24cb13795..27a73afb3 100644 --- a/docs/open-source/contributors.mdx +++ b/docs/open-source/contributors.mdx @@ -1,7 +1,7 @@ --- -title: "Build a connector" -description: "We welcome all contributions to Panora; from small UI enhancements to brand new integrations. We love seeing community members level up and give people power-ups!" -icon: "star" +title: 'Build a connector' +description: 'We welcome all contributions to Panora; from small UI enhancements to brand new integrations. We love seeing community members level up and give people power-ups!' +icon: 'star' --- ## Introduction @@ -14,11 +14,9 @@ We made a docker file that builds Panora from sources, specifically to help you -{" "} ```bash cp .env.example .env ``` -{" "} ```bash rm -rf node_modules .pnpm-store ./packages/api/dist @@ -26,8 +24,6 @@ We made a docker file that builds Panora from sources, specifically to help you ./apps/frontend_snippet/node_modules ``` -{" "} - ```bash echo -e "node-linker=hoisted\npackage-import-method=clone-or-copy" > .npmrc ```