diff --git a/.gitignore b/.gitignore index 2163ca8..845cb0d 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ generated/ *Ballerina.lock .ballerina **/Config.toml +**/target/ +**/generated/ # Ignore Gradle project-specific cache directory .gradle @@ -56,3 +58,11 @@ build # Ignore Docker env file docker.env + +# Test config file +ballerina/tests/Config.toml + +# Data files +*.csv + + diff --git a/README.md b/README.md index eabb67b..021c13d 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,173 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.crm.obj.contacts` package offers APIs to connect and interact with the [HubSpot Contact API](https://api.hubapi.com/crm/v3/objects/contacts) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/crm/objects/contacts). + +Using this API, users can develop applications easily that enables you to manage contacts easily. ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot connector, you must have access to the HubSpot API through a [HubSpot developer account](https://developers.hubspot.com/get-started) and a project under it. If you do not have a HubSpot Developer account, you can sign up for one [here](https://app.hubspot.com/signup-hubspot/developers?_ga=2.207749649.2047916093.1734412948-232493525.1734412948&step=landing_page). + +### Step 1: Create a HubSpot Developer Project + +1. Open the [HubSpot Developer Portal](https://app.hubspot.com/login/?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fsignup-hubspot%2Fdevelopers%3F_ga%3D2.269102326.624948025.1734413225-1764281074.1734413225%26step%3Dlanding_page) + +2. Click on the 'App' tab and select an existing project or create a new project for which you want API keys and Authentication Access. + +![hubspot-app-home](https://github.com/user-attachments/assets/8f14b6d5-f537-41fe-a860-1f5fe9489b1c) + + +In order to create a new poject, you must provide a public app name and a redirect url/s. Optionally you can add a app logo and a description for the app. + +![hubspot-new-app-info](https://github.com/user-attachments/assets/9985a3b4-b540-4f8e-9607-04f384f229b0) + + +To add redirect url/s for the app, click the 'Auth' tab on top of the page, and navigate to 'Redirect URLs' section. + +![hubspot-new-app-auth](https://github.com/user-attachments/assets/85bff298-0c54-4edb-8620-2f244b8eeb5b) + +![hubspot-new-app-redirect-url](https://github.com/user-attachments/assets/fff04140-2445-411d-9dca-f8b6596f678b) + +### Setp 2. Obtain Client ID and Client Secret. + +1. After completing the project setup, you will be provided with your client id and client secret. Make sure to save the provided client id and client secret. + +![hubspot-new-app-client-id-secret](https://github.com/user-attachments/assets/bc2f76ad-91a9-452f-b35f-0344f130beb8) + + +### 2. Setup OAuth 2.0 Flow + +Before proceeding with Quick start, ensure you ave obtained the Access Token and refresh Token using the following steps: + +1. Add necessaryscopes for your app based on API your using. Go to the relevate [API reference](https://developers.hubspot.com/beta-docs/reference/api), select the API you have and go through the operation. + +You will see the scope has defined below way + +![hubspot-new-app-scopes](https://github.com/user-attachments/assets/0e5d4f47-440e-4f96-ac6e-176973281422) + +2. Obtained the authorization URL (Install URL) from the Auth Section under 'Sample install URL (OAuth)' section. It will be in this format: + +``` +https://app.hubspot.com/oauth/authorize?client_id=&redirect_uri=&scope= +``` +3. Copy and paste the generated URL into your browser. This will redirect you to the HubSpot authorization page. + +![hubspot-oauth-consent-screen](https://github.com/user-attachments/assets/d503ef64-f3e4-4959-a3ee-b390c5c9bc62) + + +4. Once you authorize, you will be redirected to your specified redirect URI with an authorization code in the URL. + +> **Note**: Store the authorization code and use it promptly as it expires quickly. + +5. Use the obtained authorization code to run the following curl command, replacing , , and with your specific values: + + - Linux/MacOS: +```bash +curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' +``` + + - Windows: + ```bash + curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token ^ + --header 'content-type: application/x-www-form-urlencoded' ^ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + + This command will return the access token and refresh token necessary for API calls. + + ```json +{ + "token_type": "bearer", + "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "expires_in": 1800 +} + ``` + + 6. Store the access token securely for use in your application. + ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the `HubSpot CRM Contact Connector` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.obj.contact` module. + +````ballerina +import ballerinax/hubspot.crm.obj.contacts; +```` + +### Step 2: Instantiate a new connector + +1. Create a `OAuth2RefreshTokenGrantConfig` with the obtained access token and initialize the connector with it. + +````ballerina +configurable OAuth2RefreshTokenGrantConfig & readonly auth = ?; + +final contacts:Client hubSpotContacts = check new ({ auth }); +```` + +2. Create a Config.toml file and, configure the obtained credentials in the above steps as follows: + +````toml +[auth] +clientId = "" +clientSecret = "" +refreshToken = "" +credentialBearer = "POST_BODY_BEARER" +```` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. + +**Create a contact** + +```ballerina +contacts:SimplePublicObjectInputForCreate newContact = { + associations: [ + { + to: { + id: "associated_id" + } + } + ], + objectWriteTraceId: "object_write_trace_id", + properties: { + "sample_property": "sample_value" + } +}; + +contacts:SimplePublicObject response = check hubSpotContacts->/.post(newContact); +``` + +**List contacts** + +```ballerina +contacts:CollectionResponseSimplePublicObjectWithAssociationsForwardPaging contacts = check hubSpotContacts->/.get(); +``` + +### Step 4: Run the Ballerina application + +````bash +bal run +```` ## Examples The `Ballerina HubSpot CRM Contacts Connector` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.object.contacts/tree/main/examples/), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Email-Advertising](./examples/Email-Advertising/) - Unsubscribe and remove customers based on email addresses of CSV-imported data. +2. [Event-Registration](./examples/Event-Registration/) - Event registration and follow-up using CSV-imported data. ## Build from the source diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 19fdefa..32479ad 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,16 +1,16 @@ [package] distribution = "2201.10.0" org = "ballerinax" -name = "hubspot.crm.object.contacts" +name = "hubspot.crm.obj.contacts" version = "0.1.0" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] -# icon = "icon.png" # TODO: update icon.png -repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.object.contacts" +keywords = ["hubspot", "customer", "management", "connector", "crm"] +icon = "icon.png" +repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.obj.contacts" [build-options] observabilityIncluded = true -[platform.java21] +[platform.java17] graalvmCompatible = true diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml new file mode 100644 index 0000000..dbeddbb --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,332 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.0" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.12.4" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "oauth2", moduleName = "oauth2"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "os", moduleName = "os"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "url", moduleName = "url"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "hubspot.crm.obj.contacts" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.crm.obj.contacts", moduleName = "hubspot.crm.obj.contacts"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md index acb1231..232c46a 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -1,17 +1,166 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.crm.obj.contacts` package offers APIs to connect and interact with the [HubSpot Contact API](https://api.hubapi.com/crm/v3/objects/contacts) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/crm/objects/contacts). + +Using this API, users can develop applications easily that enables you to manage contacts easily. ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot connector, you must have access to the HubSpot API through a [HubSpot developer account](https://developers.hubspot.com/get-started) and a project under it. If you do not have a HubSpot Developer account, you can sign up for one [here](https://app.hubspot.com/signup-hubspot/developers?_ga=2.207749649.2047916093.1734412948-232493525.1734412948&step=landing_page). + +### Step 1: Create a HubSpot Developer Project + +1. Open the [HubSpot Developer Portal](https://app.hubspot.com/login/?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fsignup-hubspot%2Fdevelopers%3F_ga%3D2.269102326.624948025.1734413225-1764281074.1734413225%26step%3Dlanding_page) + +2. Click on the 'App' tab and select an existing project or create a new project for which you want API keys and Authentication Access. + +![hubspot-app-home](https://github.com/user-attachments/assets/8f14b6d5-f537-41fe-a860-1f5fe9489b1c) + +In order to create a new poject, you must provide a public app name and a redirect url/s. Optionally you can add a app logo and a description for the app. + +![hubspot-new-app-info](https://github.com/user-attachments/assets/9985a3b4-b540-4f8e-9607-04f384f229b0) + +To add redirect url/s for the app, click the 'Auth' tab on top of the page, and navigate to 'Redirect URLs' section. + +![hubspot-new-app-auth](https://github.com/user-attachments/assets/85bff298-0c54-4edb-8620-2f244b8eeb5b) + +![hubspot-new-app-redirect-url](https://github.com/user-attachments/assets/fff04140-2445-411d-9dca-f8b6596f678b) + +### Setp 2. Obtain Client ID and Client Secret. + +1. After completing the project setup, you will be provided with your client id and client secret. Make sure to save the provided client id and client secret. + +![hubspot-new-app-client-id-secret](https://github.com/user-attachments/assets/bc2f76ad-91a9-452f-b35f-0344f130beb8) + +### 2. Setup OAuth 2.0 Flow + +Before proceeding with Quick start, ensure you ave obtained the Access Token and refresh Token using the following steps: + +1. Add necessaryscopes for your app based on API your using. Go to the relevate [API reference](https://developers.hubspot.com/beta-docs/reference/api), select the API you have and go through the operation. + +You will see the scope has defined below way + +![hubspot-new-app-scopes](https://github.com/user-attachments/assets/0e5d4f47-440e-4f96-ac6e-176973281422) + +2. Obtained the authorization URL (Install URL) from the Auth Section under 'Sample install URL (OAuth)' section. It will be in this format: + +``` +https://app.hubspot.com/oauth/authorize?client_id=&redirect_uri=&scope= +``` +3. Copy and paste the generated URL into your browser. This will redirect you to the HubSpot authorization page. + +![hubspot-oauth-consent-screen](https://github.com/user-attachments/assets/d503ef64-f3e4-4959-a3ee-b390c5c9bc62) + +4. Once you authorize, you will be redirected to your specified redirect URI with an authorization code in the URL. + +> **Note**: Store the authorization code and use it promptly as it expires quickly. + +5. Use the obtained authorization code to run the following curl command, replacing , , and with your specific values: + + - Linux/MacOS: +```bash +curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' +``` + + - Windows: + ```bash + curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token ^ + --header 'content-type: application/x-www-form-urlencoded' ^ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + + This command will return the access token and refresh token necessary for API calls. + + ```json +{ + "token_type": "bearer", + "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "expires_in": 1800 +} + ``` + + 6. Store the access token securely for use in your application. + ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the `HubSpot CRM Contact Connector` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.obj.contact` module. + +````ballerina +import ballerinax/hubspot.crm.obj.contacts; +```` + +### Step 2: Instantiate a new connector + +1. Create a `OAuth2RefreshTokenGrantConfig` with the obtained access token and initialize the connector with it. + +```ballerina +configurable OAuth2RefreshTokenGrantConfig & readonly auth = ?; + +final contacts:Client hubSpotContacts = check new ({ auth }); +``` + +2. Create a Config.toml file and, configure the obtained credentials in the above steps as follows: + +````toml +[auth] +clientId = "" +clientSecret = "" +refreshToken = "" +credentialBearer = "POST_BODY_BEARER" +```` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. + +**Create a contact** + +```ballerina +contacts:SimplePublicObjectInputForCreate newContact = { + associations: [ + { + to: { + id: "associated_id" + } + } + ], + objectWriteTraceId: "object_write_trace_id", + properties: { + "sample_property": "sample_value" + } +}; + +contacts:SimplePublicObject response = check hubSpotContacts->/.post(newContact); +``` + +**List contacts** + +```ballerina +contacts:CollectionResponseSimplePublicObjectWithAssociationsForwardPaging contacts = check hubSpotContacts->/.get(); +``` + +### Step 4: Run the Ballerina application + +````bash +bal run +```` + ## Examples The `Ballerina HubSpot CRM Contacts Connector` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.object.contacts/tree/main/examples/), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Email-Advertising](https://github.com/) - Unsubscribe and remove customers based on email addresses imported through a CSV file. +2. [Event-Registration](https://github.com/) - Event registration and follow up using data imported through a CSV file. diff --git a/ballerina/Package.md b/ballerina/Package.md index acb1231..87416b6 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -1,17 +1,165 @@ ## Overview -[//]: # (TODO: Add overview mentioning the purpose of the module, supported REST API versions, and other high-level details.) +[HubSpot](https://www.hubspot.com) is an AI-powered customer relationship management (CRM) platform. + +The `ballerinax/module-ballerinax-hubspot.crm.obj.contacts` package offers APIs to connect and interact with the [HubSpot Contact API](https://api.hubapi.com/crm/v3/objects/contacts) endpoints, specifically based on the [HubSpot REST API](https://developers.hubspot.com/docs/reference/api/crm/objects/contacts). + +Using this API, users can develop applications easily that enables you to manage contacts easily. ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot connector, you must have access to the HubSpot API through a [HubSpot developer account](https://developers.hubspot.com/get-started) and a project under it. If you do not have a HubSpot Developer account, you can sign up for one [here](https://app.hubspot.com/signup-hubspot/developers?_ga=2.207749649.2047916093.1734412948-232493525.1734412948&step=landing_page). + +### Step 1: Create a HubSpot Developer Project + +1. Open the [HubSpot Developer Portal](https://app.hubspot.com/login/?loginRedirectUrl=https%3A%2F%2Fapp.hubspot.com%2Fsignup-hubspot%2Fdevelopers%3F_ga%3D2.269102326.624948025.1734413225-1764281074.1734413225%26step%3Dlanding_page) + +2. Click on the 'App' tab and select an existing project or create a new project for which you want API keys and Authentication Access. + +![hubspot-app-home](https://github.com/user-attachments/assets/8f14b6d5-f537-41fe-a860-1f5fe9489b1c) + +In order to create a new poject, you must provide a public app name and a redirect url/s. Optionally you can add a app logo and a description for the app. + +![hubspot-new-app-info](https://github.com/user-attachments/assets/9985a3b4-b540-4f8e-9607-04f384f229b0) + +To add redirect url/s for the app, click the 'Auth' tab on top of the page, and navigate to 'Redirect URLs' section. + +![hubspot-new-app-auth](https://github.com/user-attachments/assets/85bff298-0c54-4edb-8620-2f244b8eeb5b) + +![hubspot-new-app-redirect-url](https://github.com/user-attachments/assets/fff04140-2445-411d-9dca-f8b6596f678b) + +### Setp 2. Obtain Client ID and Client Secret. + +1. After completing the project setup, you will be provided with your client id and client secret. Make sure to save the provided client id and client secret. + +![hubspot-new-app-client-id-secret](https://github.com/user-attachments/assets/bc2f76ad-91a9-452f-b35f-0344f130beb8) + +### 2. Setup OAuth 2.0 Flow + +Before proceeding with Quick start, ensure you ave obtained the Access Token and refresh Token using the following steps: + +1. Add necessaryscopes for your app based on API your using. Go to the relevate [API reference](https://developers.hubspot.com/beta-docs/reference/api), select the API you have and go through the operation. + +You will see the scope has defined below way + +![hubspot-new-app-scopes](https://github.com/user-attachments/assets/0e5d4f47-440e-4f96-ac6e-176973281422) + +2. Obtained the authorization URL (Install URL) from the Auth Section under 'Sample install URL (OAuth)' section. It will be in this format: + +``` +https://app.hubspot.com/oauth/authorize?client_id=&redirect_uri=&scope= +``` +3. Copy and paste the generated URL into your browser. This will redirect you to the HubSpot authorization page. + +![hubspot-oauth-consent-screen](https://github.com/user-attachments/assets/d503ef64-f3e4-4959-a3ee-b390c5c9bc62) + +4. Once you authorize, you will be redirected to your specified redirect URI with an authorization code in the URL. + +> **Note**: Store the authorization code and use it promptly as it expires quickly. + +5. Use the obtained authorization code to run the following curl command, replacing , , and with your specific values: + + - Linux/MacOS: +```bash +curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' +``` + + - Windows: + ```bash + curl --request POST \ + --url https://api.hubapi.com/oauth/v1/token ^ + --header 'content-type: application/x-www-form-urlencoded' ^ + --data 'grant_type=authorization_code&code=&redirect_uri=&client_id=&client_secret=' + ``` + + This command will return the access token and refresh token necessary for API calls. + + ```json +{ + "token_type": "bearer", + "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "expires_in": 1800 +} + ``` + + 6. Store the access token securely for use in your application. ## Quickstart -[//]: # (TODO: Add a quickstart guide to demonstrate a basic functionality of the module, including sample code snippets.) +To use the `HubSpot CRM Contact Connector` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.obj.contact` module. + +````ballerina +import ballerinax/hubspot.crm.obj.contacts; +```` + +### Step 2: Instantiate a new connector + +1. Create a `OAuth2RefreshTokenGrantConfig` with the obtained access token and initialize the connector with it. + +````ballerina +configurable OAuth2RefreshTokenGrantConfig & readonly auth = ?; + +final contacts:Client hubSpotContacts = check new ({ auth }); +```` + +2. Create a Config.toml file and, configure the obtained credentials in the above steps as follows: + +````toml +[auth] +clientId = "" +clientSecret = "" +refreshToken = "" +credentialBearer = "POST_BODY_BEARER" +```` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. + +**Create a contact** + +```ballerina +contacts:SimplePublicObjectInputForCreate newContact = { + associations: [ + { + to: { + id: "associated_id" + } + } + ], + objectWriteTraceId: "object_write_trace_id", + properties: { + "sample_property": "sample_value" + } +}; + +contacts:SimplePublicObject response = check hubSpotContacts->/.post(newContact); +``` + +**List contacts** + +```ballerina +contacts:CollectionResponseSimplePublicObjectWithAssociationsForwardPaging contacts = check hubSpotContacts->/.get(); +``` + +### Step 4: Run the Ballerina application + +````bash +bal run +```` + ## Examples The `Ballerina HubSpot CRM Contacts Connector` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.object.contacts/tree/main/examples/), covering the following use cases: -[//]: # (TODO: Add examples) +1. [Email-Advertising](https://github.com/) - Unsubscribe and remove customers based on email addresses imported through a CSV file. +2. [Event-Registration](https://github.com/) - Event registration and follow up using data imported through a CSV file. diff --git a/ballerina/build.gradle b/ballerina/build.gradle index cdf9f18..4049f14 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except diff --git a/ballerina/client.bal b/ballerina/client.bal index 66cdc3f..0153af3 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -1,4 +1,7 @@ -// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// AUTO-GENERATED FILE. DO NOT MODIFY. +// This file is auto-generated by the Ballerina OpenAPI tool. + +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). // // WSO2 LLC. licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except @@ -13,3 +16,263 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. + +import ballerina/http; + +public isolated client class Client { + final http:Client clientEp; + final readonly & ApiKeysConfig? apiKeyConfig; + + # Gets invoked to initialize the `connector`. + # + # + config - The configurations to be used when initializing the `connector` + # + serviceUrl - URL of the target service + # + return - An error if connector initialization failed + public isolated function init(ConnectionConfig config, string serviceUrl = "https://api.hubapi.com/crm/v3/objects/contacts") returns error? { + http:ClientConfiguration httpClientConfig = {httpVersion: config.httpVersion, timeout: config.timeout, forwarded: config.forwarded, poolConfig: config.poolConfig, compression: config.compression, circuitBreaker: config.circuitBreaker, retryConfig: config.retryConfig, validation: config.validation}; + do { + if config.http1Settings is ClientHttp1Settings { + ClientHttp1Settings settings = check config.http1Settings.ensureType(ClientHttp1Settings); + httpClientConfig.http1Settings = {...settings}; + } + if config.http2Settings is http:ClientHttp2Settings { + httpClientConfig.http2Settings = check config.http2Settings.ensureType(http:ClientHttp2Settings); + } + if config.cache is http:CacheConfig { + httpClientConfig.cache = check config.cache.ensureType(http:CacheConfig); + } + if config.responseLimits is http:ResponseLimitConfigs { + httpClientConfig.responseLimits = check config.responseLimits.ensureType(http:ResponseLimitConfigs); + } + if config.secureSocket is http:ClientSecureSocket { + httpClientConfig.secureSocket = check config.secureSocket.ensureType(http:ClientSecureSocket); + } + if config.proxy is http:ProxyConfig { + httpClientConfig.proxy = check config.proxy.ensureType(http:ProxyConfig); + } + } + if config.auth is ApiKeysConfig { + self.apiKeyConfig = (config.auth).cloneReadOnly(); + } else { + httpClientConfig.auth = config.auth; + self.apiKeyConfig = (); + } + http:Client httpEp = check new (serviceUrl, httpClientConfig); + self.clientEp = httpEp; + return; + } + + # Archive + # + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function delete [string contactId](map headers = {}) returns http:Response|error { + string resourcePath = string `/${getEncodedUri(contactId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->delete(resourcePath, headers = httpHeaders); + } + + # List + # + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get .(map headers = {}, *ReadPageOfContactsQueries queries) returns CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|error { + string resourcePath = string `/`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map queryParamEncoding = {"properties": {style: FORM, explode: true}, "propertiesWithHistory": {style: FORM, explode: true}, "associations": {style: FORM, explode: true}}; + resourcePath = resourcePath + check getPathForQueryParam(queries, queryParamEncoding); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Read + # + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function get [string contactId](map headers = {}, *ReadContactByIdQueries queries) returns SimplePublicObjectWithAssociations|error { + string resourcePath = string `/${getEncodedUri(contactId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map queryParamEncoding = {"properties": {style: FORM, explode: true}, "propertiesWithHistory": {style: FORM, explode: true}, "associations": {style: FORM, explode: true}}; + resourcePath = resourcePath + check getPathForQueryParam(queries, queryParamEncoding); + map httpHeaders = getMapForHeaders(headerValues); + return self.clientEp->get(resourcePath, httpHeaders); + } + + # Update + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function patch [string contactId](SimplePublicObjectInput payload, map headers = {}) returns SimplePublicObject|error { + string resourcePath = string `/${getEncodedUri(contactId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->patch(resourcePath, request, httpHeaders); + } + + # Create + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post .(SimplePublicObjectInputForCreate payload, map headers = {}) returns SimplePublicObject|error { + string resourcePath = string `/`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Archive a batch of contacts by ID + # + # + headers - Headers to be sent with the request + # + return - No content + resource isolated function post batch/archive(BatchInputSimplePublicObjectId payload, map headers = {}) returns http:Response|error { + string resourcePath = string `/batch/archive`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Create a batch of contacts + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post batch/create(BatchInputSimplePublicObjectInputForCreate payload, map headers = {}) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors|error { + string resourcePath = string `/batch/create`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Read a batch of contacts by internal ID, or unique property values + # + # + headers - Headers to be sent with the request + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function post batch/read(BatchReadInputSimplePublicObjectId payload, map headers = {}, *ReadBatchOfContactsByInternalIdOrUniquePropertyValuesQueries queries) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors|error { + string resourcePath = string `/batch/read`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + resourcePath = resourcePath + check getPathForQueryParam(queries); + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Update a batch of contacts by internal ID, or unique property values + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post batch/update(BatchInputSimplePublicObjectBatchInput payload, map headers = {}) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors|error { + string resourcePath = string `/batch/update`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Create or update a batch of contacts by unique property values + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post batch/upsert(BatchInputSimplePublicObjectBatchInputUpsert payload, map headers = {}) returns BatchResponseSimplePublicUpsertObject|BatchResponseSimplePublicUpsertObjectWithErrors|error { + string resourcePath = string `/batch/upsert`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + resource isolated function post gdpr\-delete(PublicGdprDeleteInput payload, map headers = {}) returns http:Response|error { + string resourcePath = string `/gdpr-delete`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # Merge two contacts with same type + # + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post merge(PublicMergeInput payload, map headers = {}) returns SimplePublicObject|error { + string resourcePath = string `/merge`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } + + # + headers - Headers to be sent with the request + # + return - successful operation + resource isolated function post search(PublicObjectSearchRequest payload, map headers = {}) returns CollectionResponseWithTotalSimplePublicObjectForwardPaging|error { + string resourcePath = string `/search`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app"] = self.apiKeyConfig?.private\-app; + } + map httpHeaders = getMapForHeaders(headerValues); + http:Request request = new; + json jsonBody = payload.toJson(); + request.setPayload(jsonBody, "application/json"); + return self.clientEp->post(resourcePath, request, httpHeaders); + } +} diff --git a/ballerina/icon.png b/ballerina/icon.png new file mode 100644 index 0000000..96f47b5 Binary files /dev/null and b/ballerina/icon.png differ diff --git a/ballerina/tests/mock_service.bal b/ballerina/tests/mock_service.bal new file mode 100644 index 0000000..9b4c689 --- /dev/null +++ b/ballerina/tests/mock_service.bal @@ -0,0 +1,653 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +service on new http:Listener(9090) { + # Archive + # + # + return - returns can be any of following types + # http:NoContent (No content) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function delete [string contactId]() returns http:NoContent { + return http:NO_CONTENT; + } + + # List + # + # + 'limit - The maximum number of results to display per page. + # + after - The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. + # + properties - A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. + # + propertiesWithHistory - A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. Usage of this parameter will reduce the maximum number of objects that can be read by a single request. + # + associations - A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. + # + archived - Whether to return only results that have been archived. + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function get .(string? after, string[]? properties, string[]? propertiesWithHistory, string[]? associations, int:Signed32 'limit = 10, boolean archived = false) returns CollectionResponseSimplePublicObjectWithAssociationsForwardPaging { + return { + "paging": { + "next": { + "link": "?after=NTI1Cg%3D%3D", + "after": "NTI1Cg%3D%3D" + } + }, + "results": [ + { + "associations": { + "additionalProp1": { + "paging": { + "next": null, + "prev": { + "before": "string", + "link": "string" + } + }, + "results": [ + { + "id": "string", + "type": "string" + } + ] + }, + "additionalProp2": { + "paging": { + "next": null, + "prev": { + "before": "string", + "link": "string" + } + }, + "results": [ + { + "id": "string", + "type": "string" + } + ] + } + }, + "createdAt": "2025-01-09T17:04:57.288Z", + "archived": true, + "archivedAt": "2025-01-09T17:04:57.288Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.288Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.288Z" + } + ] + }, + "id": "string", + "properties": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + }, + "updatedAt": "2025-01-09T17:04:57.288Z" + } + ] + }; + } + + # Read + # + # + properties - A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. + # + propertiesWithHistory - A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. + # + associations - A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. + # + archived - Whether to return only results that have been archived. + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function get [string contactId](string[]? properties, string[]? propertiesWithHistory, string[]? associations, boolean archived = false) returns SimplePublicObjectWithAssociations { + return { + "associations": { + "additionalProp1": { + "paging": { + "next": { + "link": "?after=NTI1Cg%3D%3D", + "after": "NTI1Cg%3D%3D" + }, + "prev": { + "before": "string", + "link": "string" + } + }, + "results": [ + { + "id": "string", + "type": "string" + } + ] + }, + "additionalProp2": { + "paging": { + "next": { + "link": "?after=NTI1Cg%3D%3D", + "after": "NTI1Cg%3D%3D" + }, + "prev": { + "before": "string", + "link": "string" + } + }, + "results": [ + { + "id": "string", + "type": "string" + } + ] + } + }, + "createdAt": "2025-01-09T17:04:57.293Z", + "archived": true, + "archivedAt": "2025-01-09T17:04:57.293Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.293Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.293Z" + } + ] + }, + "id": contactId, + "properties": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + }, + "updatedAt": "2025-01-09T17:04:57.293Z" + }; + } + + # Update + # + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function patch [string contactId](@http:Payload SimplePublicObjectInput payload) returns SimplePublicObject { + return { + "createdAt": "2025-01-09T17:04:57.303Z", + "archived": false, + "archivedAt": "2025-01-09T17:04:57.303Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.303Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.303Z" + } + ] + }, + "id": "512", + "properties": payload.properties, + "updatedAt": "2025-01-09T17:04:57.303Z" + }; + } + + # Create + # + # + return - returns can be any of following types + # http:Created (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post .(@http:Payload SimplePublicObjectInputForCreate payload) returns SimplePublicObject { + return { + "createdAt": "2025-01-09T17:04:57.298Z", + "archived": false, + "archivedAt": "2025-01-09T17:04:57.298Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.298Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.298Z" + } + ] + }, + "id": "512", + "properties": payload.properties, + "updatedAt": "2025-01-09T17:04:57.298Z" + }; + } + + # Merge two contacts with same type + # + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post merge(@http:Payload PublicMergeInput payload) returns SimplePublicObject { + return { + "createdAt": "2025-01-14T05:33:22.032Z", + "archived": false, + "archivedAt": "2025-01-14T05:33:22.032Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:22.032Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:22.032Z" + } + ] + }, + "id": "512", + "properties": { + "property_date": "1572480000000", + "property_radio": "option_1", + "property_number": "17", + "property_string": "value", + "property_checkbox": "false", + "property_dropdown": "choice_b", + "property_multiple_checkboxes": "chocolate;strawberry" + }, + "updatedAt": "2025-01-14T05:33:22.032Z" + }; + } + + # Archive a batch of contacts by ID + # + # + return - returns can be any of following types + # http:NoContent (No content) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post batch/archive(@http:Payload BatchInputSimplePublicObjectId payload) returns http:NoContent { + return http:NO_CONTENT; + } + + # Create a batch of contacts + # + # + return - returns can be any of following types + # http:Created (successful operation) + # http:MultiStatus (multiple statuses) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post batch/create(@http:Payload BatchInputSimplePublicObjectInputForCreate payload) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors { + BatchResponseSimplePublicObject response = { + "completedAt": "2025-01-09T17:04:57.262Z", + "requestedAt": "2025-01-09T17:04:57.262Z", + "startedAt": "2025-01-09T17:04:57.262Z", + "links": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + }, + "results": [ + { + "createdAt": "2025-01-09T17:04:57.262Z", + "archived": false, + "archivedAt": "2025-01-09T17:04:57.262Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.262Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.262Z" + } + ], + "additionalProp3": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-09T17:04:57.262Z" + } + ] + }, + "id": "512", + "properties": { + "property_date": "1572480000000", + "property_radio": "option_1", + "property_number": "17", + "property_string": "value", + "property_checkbox": "false", + "property_dropdown": "choice_b", + "property_multiple_checkboxes": "chocolate;strawberry" + }, + "updatedAt": "2025-01-09T17:04:57.262Z" + } + ], + "status": "PENDING" + }; + + return response; + } + + # Read a batch of contacts by internal ID, or unique property values + # + # + archived - Whether to return only results that have been archived. + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:MultiStatus (multiple statuses) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post batch/read(@http:Payload BatchReadInputSimplePublicObjectId payload, boolean archived = false) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors { + BatchResponseSimplePublicObject response = { + "completedAt": "2025-01-14T05:33:21.974Z", + "requestedAt": "2025-01-14T05:33:21.974Z", + "startedAt": "2025-01-14T05:33:21.974Z", + "links": { + "additionalProp1": "string", + "additionalProp2": "string" + }, + "results": [ + { + "createdAt": "2025-01-14T05:33:21.974Z", + "archived": false, + "archivedAt": "2025-01-14T05:33:21.974Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.974Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.974Z" + } + ] + }, + "id": "512", + "properties": { + "property_date": "1572480000000", + "property_radio": "option_1", + "property_number": "17", + "property_string": "value", + "property_checkbox": "false", + "property_dropdown": "choice_b", + "property_multiple_checkboxes": "chocolate;strawberry" + }, + "updatedAt": "2025-01-14T05:33:21.974Z" + } + ], + "status": "PENDING" + }; + + return response; + } + + # Create or update a batch of contacts by unique property values + # + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:MultiStatus (multiple statuses) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post batch/upsert(@http:Payload BatchInputSimplePublicObjectBatchInputUpsert payload) returns BatchResponseSimplePublicUpsertObject|BatchResponseSimplePublicUpsertObjectWithErrors { + BatchResponseSimplePublicUpsertObject response = { + "completedAt": "2025-01-14T05:33:21.962Z", + "requestedAt": "2025-01-14T05:33:21.962Z", + "startedAt": "2025-01-14T05:33:21.962Z", + "links": { + "additionalProp1": "string", + "additionalProp2": "string" + }, + "results": [ + { + "createdAt": "2025-01-14T05:33:21.962Z", + "archived": true, + "archivedAt": "2025-01-14T05:33:21.962Z", + "new": true, + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.962Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.962Z" + } + ] + }, + "id": "string", + "properties": { + "additionalProp1": "string", + "additionalProp2": "string" + }, + "updatedAt": "2025-01-14T05:33:21.962Z" + } + ], + "status": "PENDING" + }; + + return response; + } + + # Update a batch of contacts by internal ID, or unique property values + # + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:MultiStatus (multiple statuses) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post batch/update(@http:Payload BatchInputSimplePublicObjectBatchInput payload) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors { + BatchResponseSimplePublicObject response = { + "completedAt": "2025-01-14T05:33:21.987Z", + "requestedAt": "2025-01-14T05:33:21.987Z", + "startedAt": "2025-01-14T05:33:21.987Z", + "links": { + "additionalProp1": "string", + "additionalProp2": "string" + }, + "results": [ + { + "createdAt": "2025-01-14T05:33:21.987Z", + "archived": false, + "archivedAt": "2025-01-14T05:33:21.987Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.987Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:21.987Z" + } + ] + }, + "id": "512", + "properties": { + "property_date": "1572480000000", + "property_radio": "option_1", + "property_number": "17", + "property_string": "value", + "property_checkbox": "false", + "property_dropdown": "choice_b", + "property_multiple_checkboxes": "chocolate;strawberry" + }, + "updatedAt": "2025-01-14T05:33:21.987Z" + } + ], + "status": "PENDING" + }; + + return response; + } + + # Gdpr-delete + # + # + return - returns can be any of following types + # http:NoContent (No content) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post gdpr\-delete(@http:Payload PublicGdprDeleteInput payload) returns http:NoContent { + return http:NO_CONTENT; + } + + # Search + # + # + return - returns can be any of following types + # http:Ok (successful operation) + # http:DefaultStatusCodeResponse (An error occurred.) + resource function post search(@http:Payload PublicObjectSearchRequest payload) returns CollectionResponseWithTotalSimplePublicObjectForwardPaging { + CollectionResponseWithTotalSimplePublicObjectForwardPaging response = { + "total": 0, + "paging": { + "next": { + "link": "?after=NTI1Cg%3D%3D", + "after": "NTI1Cg%3D%3D" + } + }, + "results": [ + { + "createdAt": "2025-01-14T05:33:22.069Z", + "archived": false, + "archivedAt": "2025-01-14T05:33:22.069Z", + "propertiesWithHistory": { + "additionalProp1": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:22.069Z" + } + ], + "additionalProp2": [ + { + "sourceId": "string", + "sourceType": "string", + "sourceLabel": "string", + "updatedByUserId": 0, + "value": "string", + "timestamp": "2025-01-14T05:33:22.069Z" + } + ] + }, + "id": "512", + "properties": { + "property_date": "1572480000000", + "property_radio": "option_1", + "property_number": "17", + "property_string": "value", + "property_checkbox": "false", + "property_dropdown": "choice_b", + "property_multiple_checkboxes": "chocolate;strawberry" + }, + "updatedAt": "2025-01-14T05:33:22.069Z" + } + ] + }; + + return response; + } +}; diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal new file mode 100644 index 0000000..c414d5e --- /dev/null +++ b/ballerina/tests/test.bal @@ -0,0 +1,270 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/oauth2; +import ballerina/os; +import ballerina/test; + +configurable boolean isLiveServer = os:getEnv("IS_LIVE_SERVER") == "true"; +configurable string serviceUrl = isLiveServer ? "https://api.hubapi.com/crm/v3/objects/contacts" : "http://localhost:9090"; + +final string clientId = os:getEnv("HUBSPOT_CLIENT_ID"); +final string clientSecret = os:getEnv("HUBSPOT_CLIENT_SECRET"); +final string refreshToken = os:getEnv("HUBSPOT_REFRESH_TOKEN"); + +Client hubSpotCrmContact = check initClient(); + +isolated function initClient() returns Client|error { + if isLiveServer { + OAuth2RefreshTokenGrantConfig auth = { + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + }; + return check new ({auth}, serviceUrl); + } + return check new ({ + auth: { + token: "test-token" + } + }, serviceUrl); +} + +const int CONTACT_TO_CONTACT_ASSOCIATION_TYPE_ID = 449; +string testContactId = ""; + +@test:Config { + dependsOn: [testCreateBatchOfContacts], + groups: ["mock_tests", "live_tests"] +} +function testMergeTwoContactsWithSameType() returns error? { + // create a another contact to be merged + string testFirstName = "jimmy"; + SimplePublicObject newContact = check hubSpotCrmContact->/.post({ + associations: [ + { + to: { + id: "1" + } + } + ], + objectWriteTraceId: "1", + properties: { + "firstname": testFirstName + } + }); + string newContactId = newContact.id; + // merge the two contacts created + SimplePublicObject response = check hubSpotCrmContact->/merge.post({ + objectIdToMerge: testContactId, + primaryObjectId: newContactId + }); + test:assertTrue(response.id.length() > 0); +} + +@test:Config { + groups: ["mock_tests", "live_tests"] +} +function testArchiveBatchOfContactsById() returns error? { + string contactId = "4243242"; + http:Response response = check hubSpotCrmContact->/batch/archive.post({ + inputs: [ + { + id: contactId + } + ] + }); + + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + dependsOn: [testCreateContact], + groups: ["mock_tests", "live_tests"] +} +function testReadBatchOfContactsByInternalIdOrUniquePropertyValues() returns error? { + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubSpotCrmContact->/batch/read.post({ + propertiesWithHistory: ["firstname"], + idProperty: "email", + inputs: [ + { + id: testContactId + } + ], + properties: ["firstname"] + }); + + string[] statuses = ["PENDING", "PROCESSING", "CANCELED", "COMPLETE"]; + test:assertTrue(statuses.filter(status => status == response.status).length() > 0); +} + +@test:Config { + groups: ["mock_tests", "live_tests"] +} +function testCreateContact() returns error? { + string testFirstName = "john"; + SimplePublicObject response = check hubSpotCrmContact->/.post({ + associations: [ + { + to: { + id: "1" + } + } + ], + objectWriteTraceId: "1", + properties: { + "firstname": testFirstName + } + }); + // set the test contact id as created one + testContactId = response.id; + test:assertEquals(response.properties["firstname"], testFirstName); +} + +@test:Config { + groups: ["mock_tests", "live_tests"] +} +function testGetPageOfContacts() returns error? { + CollectionResponseSimplePublicObjectWithAssociationsForwardPaging response = check hubSpotCrmContact->/(); + test:assertTrue(response.results.length() >= 0); +} + +@test:Config { + dependsOn: [testCreateContact], + groups: ["mock_tests", "live_tests"] +} +function testGetContactByContactId() returns error? { + SimplePublicObjectWithAssociations response = check hubSpotCrmContact->/[testContactId](); + test:assertEquals(response.id, testContactId); +} + +@test:Config { + dependsOn: [testGetContactByContactId], + groups: ["mock_tests", "live_tests"] +} +function testPartialUpdateOfContactByContactId() returns error? { + string testNewFirstName = "johny"; + SimplePublicObject response = check hubSpotCrmContact->/[testContactId].patch({ + objectWriteTraceId: "1", + properties: { + "firstname": testNewFirstName + } + }); + test:assertEquals(response.properties["firstname"], testNewFirstName); +} + +@test:Config { + dependsOn: [testMergeTwoContactsWithSameType], + groups: ["mock_tests", "live_tests"] +} +function testDeleteContactById() returns error? { + http:Response response = check hubSpotCrmContact->/[testContactId].delete(); + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + dependsOn: [testBatchUpdate], + groups: ["mock_tests", "live_tests"] +} +function testCreateBatchOfContacts() returns error? { + string testFirstName = "gayumi"; + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubSpotCrmContact->/batch/create.post({ + inputs: [ + { + associations: [ + ], + properties: { + "firstname": testFirstName + } + } + ] + }); + + string[] statuses = ["PENDING", "PROCESSING", "CANCELED", "COMPLETE"]; + test:assertTrue(statuses.filter(status => status == response.status).length() > 0); +} + +@test:Config { + groups: ["mock_tests", "live_tests"] +} +function testSearch() returns error? { + string testSearchQuery = "john"; + CollectionResponseWithTotalSimplePublicObjectForwardPaging response = check hubSpotCrmContact->/search.post({ + query: testSearchQuery, + 'limit: 1, + after: "0" + }); + test:assertTrue(response.total >= 0); +} + +@test:Config { + dependsOn: [testDeleteContactById], + groups: ["mock_tests", "live_tests"] +} +function testGDPRDelete() returns error? { + http:Response response = check hubSpotCrmContact->/gdpr\-delete.post({ + objectId: testContactId + }); + test:assertEquals(response.statusCode, 204); +} + +@test:Config { + dependsOn: [testUpsertBatchOfContacts], + groups: ["mock_tests", "live_tests"] +} +function testBatchUpdate() returns error? { + string testEmail = "johnee@example.com"; + string testNewFirstName = "jemy"; + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubSpotCrmContact->/batch/update.post({ + inputs: [ + { + idProperty: "email", + id: testEmail, + properties: { + "firstname": testNewFirstName + } + } + ] + }); + + string[] statuses = ["PENDING", "PROCESSING", "CANCELED", "COMPLETE"]; + test:assertTrue(statuses.filter(status => status == response.status).length() > 0); +} + +@test:Config { + dependsOn: [testPartialUpdateOfContactByContactId], + groups: ["mock_tests", "live_tests"] +} +function testUpsertBatchOfContacts() returns error? { + string testUpdatedFirstName = "johnee"; + string testEmail = "johnee@example.com"; + BatchResponseSimplePublicUpsertObject|BatchResponseSimplePublicUpsertObjectWithErrors response = check hubSpotCrmContact->/batch/upsert.post({ + inputs: [ + { + idProperty: "email", + id: testEmail, + properties: { + "firstname": testUpdatedFirstName + } + } + ] + }); + + string[] statuses = ["PENDING", "PROCESSING", "CANCELED", "COMPLETE"]; + test:assertTrue(statuses.filter(status => status == response.status).length() > 0); +} diff --git a/ballerina/types.bal b/ballerina/types.bal new file mode 100644 index 0000000..fc99f4e --- /dev/null +++ b/ballerina/types.bal @@ -0,0 +1,403 @@ +// AUTO-GENERATED FILE. DO NOT MODIFY. +// This file is auto-generated by the Ballerina OpenAPI tool. + +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; + +public type CollectionResponseWithTotalSimplePublicObjectForwardPagingOk record {| + *http:Ok; + CollectionResponseWithTotalSimplePublicObjectForwardPaging body; +|}; + +public type BatchResponseSimplePublicObject record { + string completedAt; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + SimplePublicObject[] results; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type ErrorDetail record { + # A specific category that contains more specific detail about the error + string subCategory?; + # The status code associated with the error detail + string code?; + # The name of the field or parameter in which the error was found. + string 'in?; + # Context about the error condition + record {|string[]...;|} context?; + # A human readable message describing the error along with remediation steps where appropriate + string message; +}; + +public type SimplePublicObjectOk record {| + *http:Ok; + SimplePublicObject body; +|}; + +public type ForwardPaging record { + NextPage next?; +}; + +public type BatchResponseSimplePublicUpsertObjectWithErrors record { + string completedAt; + int:Signed32 numErrors?; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + SimplePublicUpsertObject[] results; + StandardError[] errors?; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type BatchReadInputSimplePublicObjectId record { + string[] propertiesWithHistory; + string idProperty?; + SimplePublicObjectId[] inputs; + string[] properties; +}; + +public type BatchResponseSimplePublicUpsertObject record { + string completedAt; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + SimplePublicUpsertObject[] results; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +# Represents the Queries record for the operation: readPageOfContacts +public type ReadPageOfContactsQueries record { + # A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. + string[] associations?; + # Whether to return only results that have been archived. + boolean archived = false; + # A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. Usage of this parameter will reduce the maximum number of objects that can be read by a single request. + string[] propertiesWithHistory?; + # The maximum number of results to display per page. + int:Signed32 'limit = 10; + # The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. + string after?; + # A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. + string[] properties?; +}; + +public type BatchInputSimplePublicObjectId record { + SimplePublicObjectId[] inputs; +}; + +# Provides a set of configurations for controlling the behaviours when communicating with a remote HTTP endpoint. +@display {label: "Connection Config"} +public type ConnectionConfig record {| + # Provides Auth configurations needed when communicating with a remote HTTP endpoint. + http:BearerTokenConfig|OAuth2RefreshTokenGrantConfig|ApiKeysConfig auth; + # The HTTP version understood by the client + http:HttpVersion httpVersion = http:HTTP_2_0; + # Configurations related to HTTP/1.x protocol + ClientHttp1Settings http1Settings?; + # Configurations related to HTTP/2 protocol + http:ClientHttp2Settings http2Settings?; + # The maximum time to wait (in seconds) for a response before closing the connection + decimal timeout = 60; + # The choice of setting `forwarded`/`x-forwarded` header + string forwarded = "disable"; + # Configurations associated with request pooling + http:PoolConfiguration poolConfig?; + # HTTP caching related configurations + http:CacheConfig cache?; + # Specifies the way of handling compression (`accept-encoding`) header + http:Compression compression = http:COMPRESSION_AUTO; + # Configurations associated with the behaviour of the Circuit Breaker + http:CircuitBreakerConfig circuitBreaker?; + # Configurations associated with retrying + http:RetryConfig retryConfig?; + # Configurations associated with inbound response size limits + http:ResponseLimitConfigs responseLimits?; + # SSL/TLS-related options + http:ClientSecureSocket secureSocket?; + # Proxy server related options + http:ProxyConfig proxy?; + # Enables the inbound payload validation functionality which provided by the constraint package. Enabled by default + boolean validation = true; +|}; + +public type Paging record { + NextPage next?; + PreviousPage prev?; +}; + +public type SimplePublicObjectBatchInputUpsert record { + string idProperty?; + string objectWriteTraceId?; + string id; + record {|string...;|} properties; +}; + +public type BatchResponseSimplePublicObjectWithErrors record { + string completedAt; + int:Signed32 numErrors?; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + SimplePublicObject[] results; + StandardError[] errors?; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type PublicGdprDeleteInput record { + string idProperty?; + string objectId; +}; + +# Represents the Queries record for the operation: readBatchOfContactsByInternalIdOrUniquePropertyValues +public type ReadBatchOfContactsByInternalIdOrUniquePropertyValuesQueries record { + # Whether to return only results that have been archived. + boolean archived = false; +}; + +public type BatchResponseSimplePublicObjectWithErrorsMultiStatus record {| + *http:MultiStatus; + BatchResponseSimplePublicObjectWithErrors body; +|}; + +public type PublicMergeInput record { + string objectIdToMerge; + string primaryObjectId; +}; + +public type SimplePublicObjectWithAssociations record { + record {|CollectionResponseAssociatedId...;|} associations?; + string createdAt; + boolean archived?; + string archivedAt?; + record {|ValueWithTimestamp[]...;|} propertiesWithHistory?; + string id; + record {|string?...;|} properties; + string updatedAt; +}; + +public type AnydataDefault record {| + *http:DefaultStatusCodeResponse; + anydata body; +|}; + +public type BatchInputSimplePublicObjectInputForCreate record { + SimplePublicObjectInputForCreate[] inputs; +}; + +public type SimplePublicUpsertObject record { + string createdAt; + boolean archived?; + string archivedAt?; + boolean 'new; + record {|ValueWithTimestamp[]...;|} propertiesWithHistory?; + string id; + record {|string...;|} properties; + string updatedAt; +}; + +public type BatchResponseSimplePublicUpsertObjectOk record {| + *http:Ok; + BatchResponseSimplePublicUpsertObject body; +|}; + +public type SimplePublicObjectBatchInput record { + string idProperty?; + string objectWriteTraceId?; + string id; + record {|string...;|} properties; +}; + +# Provides API key configurations needed when communicating with a remote HTTP endpoint. +public type ApiKeysConfig record {| + string private\-app\-legacy; + string private\-app; +|}; + +public type StandardError record { + record {} subCategory?; + record {|string[]...;|} context; + record {|string...;|} links; + string id?; + string category; + string message; + ErrorDetail[] errors; + string status; +}; + +public type CollectionResponseAssociatedId record { + Paging paging?; + AssociatedId[] results; +}; + +public type PublicAssociationsForObject record { + AssociationSpec[] types?; + PublicObjectId to?; +}; + +public type FilterGroup record { + Filter[] filters; +}; + +# Represents the Queries record for the operation: readContactById +public type ReadContactByIdQueries record { + # A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. + string[] associations?; + # Whether to return only results that have been archived. + boolean archived = false; + # A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. + string[] propertiesWithHistory?; + # A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. + string[] properties?; +}; + +public type SimplePublicObjectId record { + string id; +}; + +public type ValueWithTimestamp record { + string sourceId?; + string sourceType; + string sourceLabel?; + int:Signed32 updatedByUserId?; + string value; + string timestamp; +}; + +# OAuth2 Refresh Token Grant Configs +public type OAuth2RefreshTokenGrantConfig record {| + *http:OAuth2RefreshTokenGrantConfig; + # Refresh URL + string refreshUrl = "https://api.hubapi.com/oauth/v1/token"; +|}; + +public type BatchInputSimplePublicObjectBatchInputUpsert record { + SimplePublicObjectBatchInputUpsert[] inputs; +}; + +public type CollectionResponseWithTotalSimplePublicObjectForwardPaging record { + int:Signed32 total; + ForwardPaging paging?; + SimplePublicObject[] results; +}; + +public type SimplePublicObject record { + string createdAt; + boolean archived?; + string archivedAt?; + record {|ValueWithTimestamp[]...;|} propertiesWithHistory?; + string id; + record {|string?...;|} properties; + string updatedAt; +}; + +public type PublicObjectId record { + string id?; +}; + +public type PublicObjectSearchRequest record { + string query?; + int:Signed32 'limit?; + string after?; + string[] sorts?; + string[] properties?; + FilterGroup[] filterGroups?; +}; + +# Proxy server configurations to be used with the HTTP client endpoint. +public type ProxyConfig record {| + # Host name of the proxy server + string host = ""; + # Proxy server port + int port = 0; + # Proxy server username + string userName = ""; + # Proxy server password + @display {label: "", kind: "password"} + string password = ""; +|}; + +public type SimplePublicObjectInput record { + string objectWriteTraceId?; + record {|string...;|} properties; +}; + +public type BatchResponseSimplePublicObjectOk record {| + *http:Ok; + BatchResponseSimplePublicObject body; +|}; + +public type CollectionResponseSimplePublicObjectWithAssociationsForwardPaging record { + ForwardPaging paging?; + SimplePublicObjectWithAssociations[] results; +}; + +public type AssociationSpec record { + "HUBSPOT_DEFINED"|"USER_DEFINED"|"INTEGRATOR_DEFINED" associationCategory?; + int:Signed32 associationTypeId?; +}; + +public type Filter record { + string highValue?; + string propertyName; + string[] values?; + string value?; + # null + "EQ"|"NEQ"|"LT"|"LTE"|"GT"|"GTE"|"BETWEEN"|"IN"|"NOT_IN"|"HAS_PROPERTY"|"NOT_HAS_PROPERTY"|"CONTAINS_TOKEN"|"NOT_CONTAINS_TOKEN" operator; +}; + +# Provides settings related to HTTP/1.x protocol. +public type ClientHttp1Settings record {| + # Specifies whether to reuse a connection for multiple requests + http:KeepAlive keepAlive = http:KEEPALIVE_AUTO; + # The chunking behaviour of the request + http:Chunking chunking = http:CHUNKING_AUTO; + # Proxy server related options + ProxyConfig proxy?; +|}; + +public type BatchInputSimplePublicObjectBatchInput record { + SimplePublicObjectBatchInput[] inputs; +}; + +public type PreviousPage record { + string before; + string link?; +}; + +public type BatchResponseSimplePublicUpsertObjectWithErrorsMultiStatus record {| + *http:MultiStatus; + BatchResponseSimplePublicUpsertObjectWithErrors body; +|}; + +public type NextPage record { + string link?; + string after; +}; + +public type AssociatedId record { + string id; + string 'type; +}; + +public type SimplePublicObjectInputForCreate record { + PublicAssociationsForObject[] associations?; + string objectWriteTraceId?; + record {|string...;|} properties; +}; diff --git a/ballerina/utils.bal b/ballerina/utils.bal new file mode 100644 index 0000000..5d8e591 --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,234 @@ +// AUTO-GENERATED FILE. DO NOT MODIFY. +// This file is auto-generated by the Ballerina OpenAPI tool. + +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/url; + +type SimpleBasicType string|boolean|int|float|decimal; + +# Represents encoding mechanism details. +type Encoding record { + # Defines how multiple values are delimited + string style = FORM; + # Specifies whether arrays and objects should generate as separate fields + boolean explode = true; + # Specifies the custom content type + string contentType?; + # Specifies the custom headers + map headers?; +}; + +enum EncodingStyle { + DEEPOBJECT, FORM, SPACEDELIMITED, PIPEDELIMITED +} + +final Encoding & readonly defaultEncoding = {}; + +# Serialize the record according to the deepObject style. +# +# + parent - Parent record name +# + anyRecord - Record to be serialized +# + return - Serialized record as a string +isolated function getDeepObjectStyleRequest(string parent, record {} anyRecord) returns string { + string[] recordArray = []; + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(parent + "[" + key + "]" + "=" + getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(parent + "[" + key + "]" + "[]", value, DEEPOBJECT, true)); + } else if value is record {} { + string nextParent = parent + "[" + key + "]"; + recordArray.push(getDeepObjectStyleRequest(nextParent, value)); + } else if value is record {}[] { + string nextParent = parent + "[" + key + "]"; + recordArray.push(getSerializedRecordArray(nextParent, value, DEEPOBJECT)); + } + recordArray.push("&"); + } + _ = recordArray.pop(); + return string:'join("", ...recordArray); +} + +# Serialize the record according to the form style. +# +# + parent - Parent record name +# + anyRecord - Record to be serialized +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized record as a string +isolated function getFormStyleRequest(string parent, record {} anyRecord, boolean explode = true) returns string { + string[] recordArray = []; + if explode { + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(key, "=", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(key, value, explode = explode)); + } else if value is record {} { + recordArray.push(getFormStyleRequest(parent, value, explode)); + } + recordArray.push("&"); + } + _ = recordArray.pop(); + } else { + foreach [string, anydata] [key, value] in anyRecord.entries() { + if value is SimpleBasicType { + recordArray.push(key, ",", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + recordArray.push(getSerializedArray(key, value, explode = false)); + } else if value is record {} { + recordArray.push(getFormStyleRequest(parent, value, explode)); + } + recordArray.push(","); + } + _ = recordArray.pop(); + } + return string:'join("", ...recordArray); +} + +# Serialize arrays. +# +# + arrayName - Name of the field with arrays +# + anyArray - Array to be serialized +# + style - Defines how multiple values are delimited +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized array as a string +isolated function getSerializedArray(string arrayName, anydata[] anyArray, string style = "form", boolean explode = true) returns string { + string key = arrayName; + string[] arrayValues = []; + if anyArray.length() > 0 { + if style == FORM && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), ","); + } + } else if style == SPACEDELIMITED && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), "%20"); + } + } else if style == PIPEDELIMITED && !explode { + arrayValues.push(key, "="); + foreach anydata i in anyArray { + arrayValues.push(getEncodedUri(i.toString()), "|"); + } + } else if style == DEEPOBJECT { + foreach anydata i in anyArray { + arrayValues.push(key, "[]", "=", getEncodedUri(i.toString()), "&"); + } + } else { + foreach anydata i in anyArray { + arrayValues.push(key, "=", getEncodedUri(i.toString()), "&"); + } + } + _ = arrayValues.pop(); + } + return string:'join("", ...arrayValues); +} + +# Serialize the array of records according to the form style. +# +# + parent - Parent record name +# + value - Array of records to be serialized +# + style - Defines how multiple values are delimited +# + explode - Specifies whether arrays and objects should generate separate parameters +# + return - Serialized record as a string +isolated function getSerializedRecordArray(string parent, record {}[] value, string style = FORM, boolean explode = true) returns string { + string[] serializedArray = []; + if style == DEEPOBJECT { + int arayIndex = 0; + foreach var recordItem in value { + serializedArray.push(getDeepObjectStyleRequest(parent + "[" + arayIndex.toString() + "]", recordItem), "&"); + arayIndex = arayIndex + 1; + } + } else { + if !explode { + serializedArray.push(parent, "="); + } + foreach var recordItem in value { + serializedArray.push(getFormStyleRequest(parent, recordItem, explode), ","); + } + } + _ = serializedArray.pop(); + return string:'join("", ...serializedArray); +} + +# Get Encoded URI for a given value. +# +# + value - Value to be encoded +# + return - Encoded string +isolated function getEncodedUri(anydata value) returns string { + string|error encoded = url:encode(value.toString(), "UTF8"); + if encoded is string { + return encoded; + } else { + return value.toString(); + } +} + +# Generate query path with query parameter. +# +# + queryParam - Query parameter map +# + encodingMap - Details on serialization mechanism +# + return - Returns generated Path or error at failure of client initialization +isolated function getPathForQueryParam(map queryParam, map encodingMap = {}) returns string|error { + string[] param = []; + if queryParam.length() > 0 { + param.push("?"); + foreach var [key, value] in queryParam.entries() { + if value is () { + _ = queryParam.remove(key); + continue; + } + Encoding encodingData = encodingMap.hasKey(key) ? encodingMap.get(key) : defaultEncoding; + if value is SimpleBasicType { + param.push(key, "=", getEncodedUri(value.toString())); + } else if value is SimpleBasicType[] { + param.push(getSerializedArray(key, value, encodingData.style, encodingData.explode)); + } else if value is record {} { + if encodingData.style == DEEPOBJECT { + param.push(getDeepObjectStyleRequest(key, value)); + } else { + param.push(getFormStyleRequest(key, value, encodingData.explode)); + } + } else { + param.push(key, "=", value.toString()); + } + param.push("&"); + } + _ = param.pop(); + } + string restOfPath = string:'join("", ...param); + return restOfPath; +} + +# Generate header map for given header values. +# +# + headerParam - Headers map +# + return - Returns generated map or error at failure of client initialization +isolated function getMapForHeaders(map headerParam) returns map { + map headerMap = {}; + foreach var [key, value] in headerParam.entries() { + if value is SimpleBasicType[] { + headerMap[key] = from SimpleBasicType data in value + select data.toString(); + } else { + headerMap[key] = value.toString(); + } + } + return headerMap; +} diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 03bb0da..892d82c 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -1,16 +1,16 @@ [package] distribution = "2201.10.0" org = "ballerinax" -name = "hubspot.crm.object.contacts" +name = "hubspot.crm.obj.contacts" version = "@toml.version@" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] # TODO: Add keywords -# icon = "icon.png" # TODO: Add icon -repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.object.contacts" +keywords = ["hubspot", "customer", "management", "connector", "crm"] +icon = "icon.png" +repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.obj.contacts" [build-options] observabilityIncluded = true -[platform.java21] +[platform.java17] graalvmCompatible = true diff --git a/build.gradle b/build.gradle index b8550e4..3dae0e1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except diff --git a/docs/license.txt b/docs/license.txt index 921a7a1..c000777 100644 --- a/docs/license.txt +++ b/docs/license.txt @@ -1,7 +1,4 @@ -// AUTO-GENERATED FILE. DO NOT MODIFY. -// This file is auto-generated by the Ballerina OpenAPI tool. - -// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). // // WSO2 LLC. licenses this file to you under the Apache License, // Version 2.0 (the "License"); you may not use this file except diff --git a/docs/setup/resources/hubspot-app-home.png b/docs/setup/resources/hubspot-app-home.png new file mode 100644 index 0000000..fb2d1e2 Binary files /dev/null and b/docs/setup/resources/hubspot-app-home.png differ diff --git a/docs/setup/resources/hubspot-new-app-auth.png b/docs/setup/resources/hubspot-new-app-auth.png new file mode 100644 index 0000000..a9313d6 Binary files /dev/null and b/docs/setup/resources/hubspot-new-app-auth.png differ diff --git a/docs/setup/resources/hubspot-new-app-client-id-secret.png b/docs/setup/resources/hubspot-new-app-client-id-secret.png new file mode 100644 index 0000000..2f490c9 Binary files /dev/null and b/docs/setup/resources/hubspot-new-app-client-id-secret.png differ diff --git a/docs/setup/resources/hubspot-new-app-info.png b/docs/setup/resources/hubspot-new-app-info.png new file mode 100644 index 0000000..962647d Binary files /dev/null and b/docs/setup/resources/hubspot-new-app-info.png differ diff --git a/docs/setup/resources/hubspot-new-app-redirect-url.png b/docs/setup/resources/hubspot-new-app-redirect-url.png new file mode 100644 index 0000000..2ea0db9 Binary files /dev/null and b/docs/setup/resources/hubspot-new-app-redirect-url.png differ diff --git a/docs/setup/resources/hubspot-new-app-scopes.png b/docs/setup/resources/hubspot-new-app-scopes.png new file mode 100644 index 0000000..f7fdcb0 Binary files /dev/null and b/docs/setup/resources/hubspot-new-app-scopes.png differ diff --git a/docs/setup/resources/hubspot-oauth-consent-screen.png b/docs/setup/resources/hubspot-oauth-consent-screen.png new file mode 100644 index 0000000..8fd341e Binary files /dev/null and b/docs/setup/resources/hubspot-oauth-consent-screen.png differ diff --git a/docs/spec/openapi.json b/docs/spec/openapi.json new file mode 100644 index 0000000..e98319d --- /dev/null +++ b/docs/spec/openapi.json @@ -0,0 +1,1716 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "Contacts", + "description" : "", + "version" : "v3", + "x-hubspot-product-tier-requirements" : { + "marketing" : "FREE", + "sales" : "FREE", + "service" : "FREE", + "cms" : "FREE" + }, + "x-hubspot-documentation-banner" : "NONE", + "x-hubspot-api-use-case" : "Retrieve a contact by ID to bring that contact data into your external system.", + "x-hubspot-related-documentation" : [ { + "name" : "Contacts Guide", + "url" : "https://developers.hubspot.com/beta-docs/guides/api/crm/objects/contacts" + } ], + "x-hubspot-introduction" : "Use the contacts API to create and manage contacts." + }, + "servers" : [ { + "url" : "https://api.hubapi.com/crm/v3/objects/contacts" + } ], + "tags" : [ { + "name" : "Batch" + }, { + "name" : "Basic" + }, { + "name" : "Merge" + }, { + "name" : "GDPR" + }, { + "name" : "Search" + } ], + "paths" : { + "/batch/read" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Read a batch of contacts by internal ID, or unique property values", + "operationId" : "post-/crm/v3/objects/contacts/batch/read_read", + "parameters" : [ { + "name" : "archived", + "in" : "query", + "description" : "Whether to return only results that have been archived.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "boolean", + "default" : false + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchReadInputSimplePublicObjectId" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObject" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObjectWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.read" ] + }, { + "private_apps" : [ "crm.objects.contacts.read" ] + } ] + } + }, + "/{contactId}" : { + "get" : { + "tags" : [ "Basic" ], + "summary" : "Read", + "description" : "Read an Object identified by `{contactId}`. `{contactId}` refers to the internal object ID. Control what is returned via the `properties` query param.", + "operationId" : "get-/crm/v3/objects/contacts/{contactId}_getById", + "parameters" : [ { + "name" : "contactId", + "in" : "path", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "properties", + "in" : "query", + "description" : "A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "propertiesWithHistory", + "in" : "query", + "description" : "A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "associations", + "in" : "query", + "description" : "A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "archived", + "in" : "query", + "description" : "Whether to return only results that have been archived.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "boolean", + "default" : false + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObjectWithAssociations" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.read" ] + }, { + "private_apps" : [ "crm.objects.contacts.read" ] + } ] + }, + "delete" : { + "tags" : [ "Basic" ], + "summary" : "Archive", + "description" : "Move an Object identified by `{contactId}` to the recycling bin.", + "operationId" : "delete-/crm/v3/objects/contacts/{contactId}_archive", + "parameters" : [ { + "name" : "contactId", + "in" : "path", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + }, + "patch" : { + "tags" : [ "Basic" ], + "summary" : "Update", + "description" : "Perform a partial update of an Object identified by `{contactId}`. `{contactId}` refers to the internal object ID. Provided property values will be overwritten. Read-only and non-existent properties result in an error. Properties values can be cleared by passing an empty string.", + "operationId" : "patch-/crm/v3/objects/contacts/{contactId}_update", + "parameters" : [ { + "name" : "contactId", + "in" : "path", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObjectInput" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/merge" : { + "post" : { + "tags" : [ "Merge" ], + "summary" : "Merge two contacts with same type", + "operationId" : "post-/crm/v3/objects/contacts/merge_merge", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicMergeInput" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/batch/archive" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Archive a batch of contacts by ID", + "operationId" : "post-/crm/v3/objects/contacts/batch/archive_archive", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputSimplePublicObjectId" + } + } + }, + "required" : true + }, + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/batch/create" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Create a batch of contacts", + "operationId" : "post-/crm/v3/objects/contacts/batch/create_create", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputSimplePublicObjectInputForCreate" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObject" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObjectWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/batch/update" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Update a batch of contacts by internal ID, or unique property values", + "operationId" : "post-/crm/v3/objects/contacts/batch/update_update", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputSimplePublicObjectBatchInput" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObject" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicObjectWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/gdpr-delete" : { + "post" : { + "tags" : [ "GDPR" ], + "summary" : "GDPR DELETE", + "description" : "Permanently delete a contact and all associated content to follow GDPR. Use optional property 'idProperty' set to 'email' to identify contact by email address. If email address is not found, the email address will be added to a blocklist and prevent it from being used in the future.", + "operationId" : "post-/crm/v3/objects/contacts/gdpr-delete_purge", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicGdprDeleteInput" + } + } + }, + "required" : true + }, + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/" : { + "get" : { + "tags" : [ "Basic" ], + "summary" : "List", + "description" : "Read a page of contacts. Control what is returned via the `properties` query param.", + "operationId" : "get-/crm/v3/objects/contacts_getPage", + "parameters" : [ { + "name" : "limit", + "in" : "query", + "description" : "The maximum number of results to display per page.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "integer", + "format" : "int32", + "default" : 10 + } + }, { + "name" : "after", + "in" : "query", + "description" : "The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + }, { + "name" : "properties", + "in" : "query", + "description" : "A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "propertiesWithHistory", + "in" : "query", + "description" : "A comma separated list of the properties to be returned along with their history of previous values. If any of the specified properties are not present on the requested object(s), they will be ignored. Usage of this parameter will reduce the maximum number of objects that can be read by a single request.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "associations", + "in" : "query", + "description" : "A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, { + "name" : "archived", + "in" : "query", + "description" : "Whether to return only results that have been archived.", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "boolean", + "default" : false + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CollectionResponseSimplePublicObjectWithAssociationsForwardPaging" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.read" ] + }, { + "private_apps" : [ "crm.objects.contacts.read" ] + } ] + }, + "post" : { + "tags" : [ "Basic" ], + "summary" : "Create", + "description" : "Create a contact with the given properties and return a copy of the object, including the ID. Documentation and examples for creating standard contacts is provided.", + "operationId" : "post-/crm/v3/objects/contacts_create", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObjectInputForCreate" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/batch/upsert" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Create or update a batch of contacts by unique property values", + "description" : "Create or update records identified by a unique property value as specified by the `idProperty` query param. `idProperty` query param refers to a property whose values are unique for the object.", + "operationId" : "post-/crm/v3/objects/contacts/batch/upsert_upsert", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchInputSimplePublicObjectBatchInputUpsert" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicUpsertObject" + } + } + } + }, + "207" : { + "description" : "multiple statuses", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/BatchResponseSimplePublicUpsertObjectWithErrors" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.write" ] + }, { + "private_apps" : [ "crm.objects.contacts.write" ] + } ] + } + }, + "/search" : { + "post" : { + "tags" : [ "Search" ], + "operationId" : "post-/crm/v3/objects/contacts/search_doSearch", + "parameters" : [ ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PublicObjectSearchRequest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CollectionResponseWithTotalSimplePublicObjectForwardPaging" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2" : [ "crm.objects.contacts.read" ] + }, { + "private_apps" : [ "crm.objects.contacts.read" ] + } ], + "x-hubspot-rate-limit-exemptions" : [ "ten-secondly" ] + } + } + }, + "components" : { + "schemas" : { + "StandardError" : { + "required" : [ "category", "context", "errors", "links", "message", "status" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "object", + "properties" : { } + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "id" : { + "type" : "string" + }, + "category" : { + "type" : "string" + }, + "message" : { + "type" : "string" + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ErrorDetail" + } + }, + "status" : { + "type" : "string" + } + } + }, + "CollectionResponseAssociatedId" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/Paging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/AssociatedId" + } + } + } + }, + "PublicAssociationsForObject" : { + "type" : "object", + "properties" : { + "types" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/AssociationSpec" + } + }, + "to" : { + "$ref" : "#/components/schemas/PublicObjectId" + } + } + }, + "BatchResponseSimplePublicObject" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "FilterGroup" : { + "required" : [ "filters" ], + "type" : "object", + "properties" : { + "filters" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Filter" + } + } + } + }, + "ErrorDetail" : { + "required" : [ "message" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "string", + "description" : "A specific category that contains more specific detail about the error" + }, + "code" : { + "type" : "string", + "description" : "The status code associated with the error detail" + }, + "in" : { + "type" : "string", + "description" : "The name of the field or parameter in which the error was found." + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "description" : "Context about the error condition", + "example" : { + "missingScopes" : [ "scope1", "scope2" ] + } + }, + "message" : { + "type" : "string", + "description" : "A human readable message describing the error along with remediation steps where appropriate" + } + } + }, + "ForwardPaging" : { + "type" : "object", + "properties" : { + "next" : { + "$ref" : "#/components/schemas/NextPage" + } + } + }, + "SimplePublicObjectId" : { + "required" : [ "id" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + } + } + }, + "BatchResponseSimplePublicUpsertObjectWithErrors" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicUpsertObject" + } + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/StandardError" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "BatchReadInputSimplePublicObjectId" : { + "required" : [ "inputs", "properties", "propertiesWithHistory" ], + "type" : "object", + "properties" : { + "propertiesWithHistory" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "idProperty" : { + "type" : "string" + }, + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectId" + } + }, + "properties" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } + }, + "BatchResponseSimplePublicUpsertObject" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicUpsertObject" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "BatchInputSimplePublicObjectId" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectId" + } + } + } + }, + "ValueWithTimestamp" : { + "required" : [ "sourceType", "timestamp", "value" ], + "type" : "object", + "properties" : { + "sourceId" : { + "type" : "string" + }, + "sourceType" : { + "type" : "string" + }, + "sourceLabel" : { + "type" : "string" + }, + "updatedByUserId" : { + "type" : "integer", + "format" : "int32" + }, + "value" : { + "type" : "string" + }, + "timestamp" : { + "type" : "string", + "format" : "date-time" + } + } + }, + "BatchInputSimplePublicObjectBatchInputUpsert" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectBatchInputUpsert" + } + } + } + }, + "CollectionResponseWithTotalSimplePublicObjectForwardPaging" : { + "required" : [ "results", "total" ], + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32" + }, + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + } + } + }, + "SimplePublicObject" : { + "required" : [ "createdAt", "id", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "string", + "format" : "date-time" + }, + "archived" : { + "type" : "boolean", + "example" : false + }, + "archivedAt" : { + "type" : "string", + "format" : "date-time" + }, + "propertiesWithHistory" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ValueWithTimestamp" + } + } + }, + "id" : { + "type" : "string", + "example" : "512" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "nullable" : true + }, + "example" : { + "property_date" : "1572480000000", + "property_radio" : "option_1", + "property_number" : "17", + "property_string" : "value", + "property_checkbox" : "false", + "property_dropdown" : "choice_b", + "property_multiple_checkboxes" : "chocolate;strawberry" + } + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time" + } + }, + "example" : { + "id" : "512", + "properties" : { + "company" : "Biglytics", + "createdate" : "2019-10-30T03:30:17.883Z", + "email" : "bcooper@biglytics.net", + "firstname" : "Bryan", + "lastmodifieddate" : "2019-12-07T16:50:06.678Z", + "lastname" : "Cooper", + "phone" : "(877) 929-0687", + "website" : "biglytics.net" + }, + "createdAt" : "2019-10-30T03:30:17.883Z", + "updatedAt" : "2019-12-07T16:50:06.678Z", + "archived" : false + } + }, + "PublicObjectId" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + } + } + }, + "Paging" : { + "type" : "object", + "properties" : { + "next" : { + "$ref" : "#/components/schemas/NextPage" + }, + "prev" : { + "$ref" : "#/components/schemas/PreviousPage" + } + } + }, + "PublicObjectSearchRequest" : { + "type" : "object", + "properties" : { + "query" : { + "type" : "string" + }, + "limit" : { + "type" : "integer", + "format" : "int32" + }, + "after" : { + "type" : "string" + }, + "sorts" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "properties" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "filterGroups" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/FilterGroup" + } + } + } + }, + "Error" : { + "required" : [ "category", "correlationId", "message" ], + "type" : "object", + "properties" : { + "subCategory" : { + "type" : "string", + "description" : "A specific category that contains more specific detail about the error" + }, + "context" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "description" : "Context about the error condition", + "example" : { + "missingScopes" : [ "scope1", "scope2" ], + "invalidPropertyName" : [ "propertyValue" ] + } + }, + "correlationId" : { + "type" : "string", + "description" : "A unique identifier for the request. Include this value with any error reports or support tickets", + "format" : "uuid", + "example" : "aeb5f871-7f07-4993-9211-075dc63e7cbf" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + }, + "description" : "A map of link names to associated URIs containing documentation about the error or recommended remediation steps", + "example" : { + "knowledge-base" : "https://www.hubspot.com/products/service/knowledge-base" + } + }, + "message" : { + "type" : "string", + "description" : "A human readable message describing the error along with remediation steps where appropriate", + "example" : "Invalid input (details will vary based on the error)" + }, + "category" : { + "type" : "string", + "description" : "The error category", + "example" : "VALIDATION_ERROR" + }, + "errors" : { + "type" : "array", + "description" : "further information about the error", + "items" : { + "$ref" : "#/components/schemas/ErrorDetail" + } + } + }, + "example" : { + "message" : "Invalid input (details will vary based on the error)", + "correlationId" : "aeb5f871-7f07-4993-9211-075dc63e7cbf", + "category" : "VALIDATION_ERROR", + "links" : { + "knowledge-base" : "https://www.hubspot.com/products/service/knowledge-base" + } + } + }, + "SimplePublicObjectBatchInputUpsert" : { + "required" : [ "id", "properties" ], + "type" : "object", + "properties" : { + "idProperty" : { + "type" : "string" + }, + "objectWriteTraceId" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + } + }, + "BatchResponseSimplePublicObjectWithErrors" : { + "required" : [ "completedAt", "results", "startedAt", "status" ], + "type" : "object", + "properties" : { + "completedAt" : { + "type" : "string", + "format" : "date-time" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "date-time" + }, + "startedAt" : { + "type" : "string", + "format" : "date-time" + }, + "links" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObject" + } + }, + "errors" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/StandardError" + } + }, + "status" : { + "type" : "string", + "enum" : [ "PENDING", "PROCESSING", "CANCELED", "COMPLETE" ] + } + } + }, + "PublicGdprDeleteInput" : { + "required" : [ "objectId" ], + "type" : "object", + "properties" : { + "idProperty" : { + "type" : "string" + }, + "objectId" : { + "type" : "string" + } + } + }, + "SimplePublicObjectInput" : { + "required" : [ "properties" ], + "type" : "object", + "properties" : { + "objectWriteTraceId" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + }, + "example" : { + "property_date" : "1572480000000", + "property_radio" : "option_1", + "property_number" : "17", + "property_string" : "value", + "property_checkbox" : "false", + "property_dropdown" : "choice_b", + "property_multiple_checkboxes" : "chocolate;strawberry" + } + } + }, + "example" : { + "properties" : { + "email" : "bcooper@biglytics.net", + "phone" : "(877) 929-0687", + "company" : "Biglytics", + "website" : "biglytics.net", + "lastname" : "Cooper", + "firstname" : "Bryan" + }, + "associations" : [ ] + } + }, + "CollectionResponseSimplePublicObjectWithAssociationsForwardPaging" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectWithAssociations" + } + } + } + }, + "AssociationSpec" : { + "type" : "object", + "properties" : { + "associationCategory" : { + "type" : "string", + "enum" : [ "HUBSPOT_DEFINED", "USER_DEFINED", "INTEGRATOR_DEFINED" ] + }, + "associationTypeId" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "PublicMergeInput" : { + "required" : [ "objectIdToMerge", "primaryObjectId" ], + "type" : "object", + "properties" : { + "objectIdToMerge" : { + "type" : "string" + }, + "primaryObjectId" : { + "type" : "string" + } + } + }, + "SimplePublicObjectWithAssociations" : { + "required" : [ "createdAt", "id", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "associations" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/CollectionResponseAssociatedId" + } + }, + "createdAt" : { + "type" : "string", + "format" : "date-time" + }, + "archived" : { + "type" : "boolean" + }, + "archivedAt" : { + "type" : "string", + "format" : "date-time" + }, + "propertiesWithHistory" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ValueWithTimestamp" + } + } + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string", + "nullable" : true + } + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time" + } + }, + "example" : { + "properties" : { + "company" : "Biglytics", + "createdate" : "2019-10-30T03:30:17.883Z", + "email" : "bcooper@biglytics.net", + "firstname" : "Bryan", + "lastmodifieddate" : "2019-12-07T16:50:06.678Z", + "lastname" : "Cooper", + "phone" : "(877) 929-0687", + "website" : "biglytics.net" + } + } + }, + "Filter" : { + "required" : [ "operator", "propertyName" ], + "type" : "object", + "properties" : { + "highValue" : { + "type" : "string" + }, + "propertyName" : { + "type" : "string" + }, + "values" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "value" : { + "type" : "string" + }, + "operator" : { + "type" : "string", + "description" : "null", + "enum" : [ "EQ", "NEQ", "LT", "LTE", "GT", "GTE", "BETWEEN", "IN", "NOT_IN", "HAS_PROPERTY", "NOT_HAS_PROPERTY", "CONTAINS_TOKEN", "NOT_CONTAINS_TOKEN" ] + } + } + }, + "BatchInputSimplePublicObjectBatchInput" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectBatchInput" + } + } + } + }, + "BatchInputSimplePublicObjectInputForCreate" : { + "required" : [ "inputs" ], + "type" : "object", + "properties" : { + "inputs" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectInputForCreate" + } + } + } + }, + "PreviousPage" : { + "required" : [ "before" ], + "type" : "object", + "properties" : { + "before" : { + "type" : "string" + }, + "link" : { + "type" : "string" + } + } + }, + "SimplePublicUpsertObject" : { + "required" : [ "createdAt", "id", "new", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "createdAt" : { + "type" : "string", + "format" : "date-time" + }, + "archived" : { + "type" : "boolean" + }, + "archivedAt" : { + "type" : "string", + "format" : "date-time" + }, + "new" : { + "type" : "boolean" + }, + "propertiesWithHistory" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ValueWithTimestamp" + } + } + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "updatedAt" : { + "type" : "string", + "format" : "date-time" + } + } + }, + "SimplePublicObjectBatchInput" : { + "required" : [ "id", "properties" ], + "type" : "object", + "properties" : { + "idProperty" : { + "type" : "string", + "example" : "my_unique_property_name" + }, + "objectWriteTraceId" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + }, + "example" : { + "id" : "1", + "properties" : { + "email" : "bcooper@biglytics.net", + "phone" : "(877) 929-0687", + "company" : "Biglytics", + "website" : "biglytics.net", + "lastname" : "Cooper", + "firstname" : "Bryan" + } + } + }, + "AssociatedId" : { + "required" : [ "id", "type" ], + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + }, + "type" : { + "type" : "string" + } + } + }, + "NextPage" : { + "required" : [ "after" ], + "type" : "object", + "properties" : { + "link" : { + "type" : "string", + "example" : "?after=NTI1Cg%3D%3D" + }, + "after" : { + "type" : "string", + "example" : "NTI1Cg%3D%3D" + } + }, + "example" : { + "after" : "NTI1Cg%3D%3D", + "link" : "?after=NTI1Cg%3D%3D" + } + }, + "SimplePublicObjectInputForCreate" : { + "required" : [ "properties" ], + "type" : "object", + "properties" : { + "associations" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicAssociationsForObject" + } + }, + "objectWriteTraceId" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + }, + "example" : { + "properties" : { + "email" : "bcooper@biglytics.net", + "phone" : "(877) 929-0687", + "company" : "Biglytics", + "website" : "biglytics.net", + "lastname" : "Cooper", + "firstname" : "Bryan" + }, + "associations" : [ ] + } + } + }, + "responses" : { + "Error" : { + "description" : "An error occurred.", + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Error" + } + } + } + } + }, + "securitySchemes" : { + "oauth2_legacy" : { + "type" : "oauth2", + "flows" : { + "authorizationCode" : { + "authorizationUrl" : "https://app.hubspot.com/oauth/authorize", + "tokenUrl" : "https://api.hubapi.com/oauth/v1/token", + "scopes" : { + "crm.objects.goals.read" : "Read goals", + "tickets" : "Read and write tickets", + "crm.objects.custom.read" : "View custom object records", + "e-commerce" : "e-commerce", + "crm.objects.custom.write" : "Change custom object records", + "media_bridge.read" : "Read media and media events" + } + } + } + }, + "oauth2" : { + "type" : "oauth2", + "flows" : { + "authorizationCode" : { + "authorizationUrl" : "https://app.hubspot.com/oauth/authorize", + "tokenUrl" : "https://api.hubapi.com/oauth/v1/token", + "scopes" : { + "crm.objects.companies.write" : " ", + "crm.objects.contacts.write" : " ", + "crm.objects.users.write" : "Write User CRM objects", + "crm.objects.commercepayments.read" : "Read the COMMERCE_PAYMENT object.", + "crm.objects.leads.write" : "Modify lead objects", + "crm.objects.subscriptions.read" : "Read Commerce Subscriptions", + "crm.objects.carts.read" : "Read carts", + "crm.objects.orders.write" : "Write orders", + "crm.objects.quotes.read" : "Quotes", + "crm.objects.services.read" : "Read services", + "crm.objects.orders.read" : "Read Orders", + "crm.objects.contacts.read" : " ", + "crm.objects.listings.read" : "Read listings", + "crm.objects.carts.write" : "Write cart", + "crm.objects.courses.write" : "Write courses", + "crm.objects.quotes.write" : "Quotes", + "crm.objects.users.read" : "Read User CRM objects", + "crm.objects.companies.read" : " ", + "crm.objects.appointments.read" : "Read appointments", + "crm.objects.partner-clients.write" : "Modify Partner Client CRM objects", + "crm.objects.leads.read" : "Read lead objects", + "crm.objects.appointments.write" : "Write appointments", + "crm.objects.services.write" : "Write services", + "crm.objects.line_items.read" : "Line Items", + "crm.objects.courses.read" : "Read courses", + "crm.objects.deals.read" : " ", + "crm.objects.invoices.read" : "Read invoices objects", + "crm.objects.partner-clients.read" : "View Partner Client CRM objects", + "crm.objects.deals.write" : " ", + "crm.objects.line_items.write" : "Line Items", + "crm.objects.listings.write" : "Write listings" + } + } + } + }, + "private_apps_legacy" : { + "type" : "apiKey", + "name" : "private-app-legacy", + "in" : "header" + }, + "private_apps" : { + "type" : "apiKey", + "name" : "private-app", + "in" : "header" + } + } + }, + "x-hubspot-available-client-libraries" : [ "PHP", "Node", "Ruby", "Python" ], + "x-hubspot-product-tier-requirements" : { + "marketing" : "FREE", + "sales" : "FREE", + "service" : "FREE", + "cms" : "FREE" + }, + "x-hubspot-documentation-banner" : "NONE" +} \ No newline at end of file diff --git a/docs/spec/sanitations.md b/docs/spec/sanitations.md index e249cf4..3562ee8 100644 --- a/docs/spec/sanitations.md +++ b/docs/spec/sanitations.md @@ -1,24 +1,34 @@ -_Author_: \ -_Created_: \ -_Updated_: \ +_Author_: @ShavinAnjithaAlpha +_Created_: 2025/01/06 +_Updated_: 2025/01/06 _Edition_: Swan Lake # Sanitation for OpenAPI specification This document records the sanitation done on top of the official OpenAPI specification from Ballerina HubSpot CRM Contacts Connector. -The OpenAPI specification is obtained from (TODO: Add source link). +The OpenAPI specification is obtained from [HubSpot CRM Contact OPenAPI Documentation](https://github.com/HubSpot/HubSpot-public-api-spec-collection/blob/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json). These changes are done in order to improve the overall usability, and as workarounds for some known language limitations. -[//]: # (TODO: Add sanitation details) -1. -2. -3. +1. **Change the `url` property of the `servers` object**: + + - **Original**: [https://api.hubapi.com](https://api.hubapi.com) + - **Updated**: [https://api.hubapi.com/crm/v3/objects/contacts](https://api.hubapi.com/crm/v3/objects/contacts) + - **Reason**: This change is made to ensure that all API paths are relative to the versioned base URL (/3), which improves the consistency and usability of the APIs. + +2. **Update API Paths**: + + - **Original**: Paths included the version prefix in each endpoint (e.g., /crm/v3/objects/contacts). + - **Updated**: Paths are modified to remove the common prefix from the endpoints, as it is now included in the base URL. For example: + - **Original**: /crm/v3/objects/contacts/batch/create + - **Updated**: /batch/create + - **Reason**: This modification simplifies the API paths, making them shorter and more readable. It also centralizes the versioning to the base URL, which is a common best practice. + ## OpenAPI cli command The following command was used to generate the Ballerina client from the OpenAPI specification. The command should be executed from the repository root directory. ```bash -# TODO: Add OpenAPI CLI command used to generate the client +bal openapi -i docs/spec/openapi.json --mode client --license docs/license.txt -o ballerina ``` -Note: The license year is hardcoded to 2024, change if necessary. +Note: The license year is hardcoded to 2025, change if necessary. diff --git a/examples/Email-Advertising/Ballerina.toml b/examples/Email-Advertising/Ballerina.toml new file mode 100644 index 0000000..ee1a2c0 --- /dev/null +++ b/examples/Email-Advertising/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "Email_Advertising" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true diff --git a/examples/Email-Advertising/Dependencies.toml b/examples/Email-Advertising/Dependencies.toml new file mode 100644 index 0000000..e4bc092 --- /dev/null +++ b/examples/Email-Advertising/Dependencies.toml @@ -0,0 +1,318 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "file", moduleName = "file"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.12.4" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "hubspot.crm.obj.contacts" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.crm.obj.contacts", moduleName = "hubspot.crm.obj.contacts"} +] + +[[package]] +org = "wso2" +name = "Email_Advertising" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "hubspot.crm.obj.contacts"} +] +modules = [ + {org = "wso2", packageName = "Email_Advertising", moduleName = "Email_Advertising"} +] + diff --git a/examples/Email-Advertising/Email Advertising.md b/examples/Email-Advertising/Email Advertising.md new file mode 100644 index 0000000..68a10bb --- /dev/null +++ b/examples/Email-Advertising/Email Advertising.md @@ -0,0 +1,28 @@ +## Email advertising + +This use case demonstrates how the **HubSpot Contacts Connector** can be used to **unsubscribe and remove customers from advertisement groups**. By leveraging the connector, you can easily retrieve contact information and update or delete details related to advertisement campaigns. + +## Prerequisites + +### 1. Setup Hubspot developer account + +Refer to the [Setup guide](https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.object.contacts/blob/main/README.md#setup-guide) to obtain necessary credentials (client Id, client secret, refresh tokens). + +### 2. Configuration + +Create a `Config.toml` file in the example's root directory and, provide your Hubspot account related configurations as follows: + +```toml +clientId = "" +clientSecret = "" +refreshToken = "" +credentialBearer = "POST_BODY_BEARER" +``` + +## Run the example + +Execute the following command to run the example: + +```bash +bal run +``` \ No newline at end of file diff --git a/examples/Email-Advertising/main.bal b/examples/Email-Advertising/main.bal new file mode 100644 index 0000000..9812ee1 --- /dev/null +++ b/examples/Email-Advertising/main.bal @@ -0,0 +1,94 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/io; +import ballerinax/hubspot.crm.obj.contacts; + +configurable contacts:OAuth2RefreshTokenGrantConfig & readonly auth = ?; + +final contacts:Client contactClient = check new ({auth}); + +public function main(string csvFilePath) returns error? { + // load the csv file data into an 2D array if exists + // format of the csv data shoud be as follows: + // email --- action --- + + io:println("[TASK] loading csv data"); + string[][] csvData = check io:fileReadCsv(csvFilePath, 1); + io:println("[TASK] finish loading csv data"); + + io:println("[TASK] start fetching contacts from API"); + // fetch the contacts from the contacts API + contacts:CollectionResponseSimplePublicObjectWithAssociationsForwardPaging contactsResponse = check contactClient->/('limit = 100, properties = ["email"], propertiesWithHistory = []); + + // get the id with their emails + map contactsWithEmails = {}; + foreach int i in int:range(0, contactsResponse.results.length(), 1) { + if (contactsResponse.results[i].properties["email"] !== ()) { + string email = contactsResponse.results[i].properties["email"].toString(); + contactsWithEmails[email] = contactsResponse.results[i].id; + } + } + + // iterate through the email in the csv file and take the action mentioned in the `action` column + // possible actions as follow: + // UNSUBSCRIBE: unsubsribe from email advertiesments + // DELETE: remove the contacts from the system + io:println("[TASK] start unsubscribing and deleting customers via HubSpot"); + foreach string[] row in csvData { + string email = row[0]; + string action = row[1]; + + match action { + "UNSUBSCRIBE" => { + check unsubscribeCustomer(email, contactsWithEmails); + } + + "DELETE" => { + check deleteCustomer(email, contactsWithEmails); + } + + _ => { + // done nothing + } + } + } + + io:println("[TASK] finish all the tasks"); +} + +public function unsubscribeCustomer(string email, map contactsWithEmail) returns error? { + if (contactsWithEmail.hasKey(email)) { + // get the id of the contact with given email + string? id = contactsWithEmail[email]; + // update the contact with the given email as removed from subscribers + contacts:SimplePublicObject response = check contactClient->/[id.toString()].patch({ + properties: { + "opt-out": "true" + } + }); + } +} + +public function deleteCustomer(string email, map contactsWithEmail) returns error? { + if (contactsWithEmail.hasKey(email)) { + // get the id of the contact with given email + string? id = contactsWithEmail[email]; + // delete the contact with customer with given id + http:Response response = check contactClient->/[id.toString()].delete(); + } +} diff --git a/examples/Event-Registration/Ballerina.toml b/examples/Event-Registration/Ballerina.toml new file mode 100644 index 0000000..1df2c35 --- /dev/null +++ b/examples/Event-Registration/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "Event_Registration" +version = "0.1.0" +distribution = "2201.10.3" + +[build-options] +observabilityIncluded = true diff --git a/examples/Event-Registration/Dependencies.toml b/examples/Event-Registration/Dependencies.toml new file mode 100644 index 0000000..32668e4 --- /dev/null +++ b/examples/Event-Registration/Dependencies.toml @@ -0,0 +1,313 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.10.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "file", moduleName = "file"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.12.4" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.6.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.13.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.4.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "hubspot.crm.obj.contacts" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.crm.obj.contacts", moduleName = "hubspot.crm.obj.contacts"} +] + +[[package]] +org = "wso2" +name = "Event_Registration" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "hubspot.crm.obj.contacts"} +] +modules = [ + {org = "wso2", packageName = "Event_Registration", moduleName = "Event_Registration"} +] + diff --git a/examples/Event-Registration/Event Registration.md b/examples/Event-Registration/Event Registration.md new file mode 100644 index 0000000..b4b6bb2 --- /dev/null +++ b/examples/Event-Registration/Event Registration.md @@ -0,0 +1,28 @@ +## Event registration + +This use case demonstrates how you can manage **event registrations** and **follow-up date updates** through **batch updates** using contact information extracted from **CSV files**. By utilizing batch operations via the HubSpot Contacts API, you can efficiently update follow-up dates for event attendees. + +## Prerequisites + +### 1. Setup Hubspot developer account + +Refer to the [Setup guide](https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.object.contacts/blob/main/README.md) to obtain necessary credentials (client Id, client secret, refresh tokens). + +### 2. Configuration + +Create a `Config.toml` file in the example's root directory and, provide your Hubspot account related configurations as follows: + +```toml +clientId = "" +clientSecret = "" +refreshToken = "" +credentialBearer = "POST_BODY_BEARER" +``` + +## Run the example + +Execute the following command to run the example: + +```bash +bal run +``` \ No newline at end of file diff --git a/examples/Event-Registration/main.bal b/examples/Event-Registration/main.bal new file mode 100644 index 0000000..d1ae131 --- /dev/null +++ b/examples/Event-Registration/main.bal @@ -0,0 +1,116 @@ +// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerinax/hubspot.crm.obj.contacts; + +configurable contacts:OAuth2RefreshTokenGrantConfig & readonly auth = ?; + +final contacts:Client contactClient = check new ({auth}); + +public function main(string csvFilePath, string task) returns error? { + + io:println("[TASK] loading csv data"); + string[][] csvData = check io:fileReadCsv(csvFilePath, 1); + io:println("[TASK] finish loading csv data"); + + match task { + "REGISTER" => { + check registerParticipants(csvData); + } + "ATTENDANCE" => { + check markBatchAttendance(csvData); + } + _ => { + io:println("task is not defined"); + return; + } + } + + return; +} + +public function registerParticipants(string[][] csvData) returns error? { + // create input list with information of the registrants + contacts:SimplePublicObjectInputForCreate[] inputs = []; + + // push registrants information into the inputs in the following format + foreach string[] row in csvData { + // csv data should include following columns: + // email -- first name -- last name -- company + string email = row[0]; + string firstName = row[1]; + string lastName = row[2]; + string company = row[3]; + + inputs.push({ + properties: { + "email": email, + "firstname": firstName, + "lastname": lastName, + "company": company + } + }); + } + + io:println("[TASK] start calling API"); + // call the batch create endpoint via client + contacts:BatchResponseSimplePublicObject|contacts:BatchResponseSimplePublicObjectWithErrors response = check contactClient->/batch/create.post({ + inputs + }); + + if (response is contacts:BatchResponseSimplePublicObjectWithErrors) { + io:println("[ERROR] there are errors associated with the request"); + } else { + io:println("[TASK] REGISTRATION INFO STATUS: ", response.status); + io:println("[TASK] finished all tasks"); + } + +} + +public function markBatchAttendance(string[][] csvData) returns error? { + + contacts:SimplePublicObjectBatchInputUpsert[] inputs = []; + + foreach string[] row in csvData { + string email = row[0]; + string attendance = row[1]; + string followUpDate = row[2]; + + inputs.push({ + idProperty: "email", + id: email, + properties: { + "event_attended": attendance, + "follow_up_date": followUpDate + } + }); + } + + io:println("[TASK] start calling API"); + // call the batch upsert endpoint via client + contacts:BatchResponseSimplePublicUpsertObject|contacts:BatchResponseSimplePublicUpsertObjectWithErrors response = check contactClient->/batch/upsert.post({ + inputs + }); + + if (response is contacts:BatchResponseSimplePublicUpsertObjectWithErrors) { + io:println("[ERROR] there are errors associated with the request"); + } else { + io:println("[TASK] UPDATING INFO STATUS: ", response.status); + io:println("[TASK] finished all tasks"); + } + +} diff --git a/examples/README.md b/examples/README.md index 6bc7864..f2497c0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,13 +2,23 @@ The `ballerinax/hubspot.crm.object.contacts` connector provides practical examples illustrating usage in various scenarios. -[//]: # (TODO: Add examples) -1. -2. +1. [Email-Advertising](./Email-Advertising/) - Unsubscribe and remove customers based on email addresses of CSV-imported data.. +2. [Event-Registration](./Event-Registration/) - Event registration and follow-up using CSV-imported data. ## Prerequisites -[//]: # (TODO: Add prerequisites) +1. Generate HubSpot credentials to authenticate the connector as described in the [Setup guide](https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.object.contacts/blob/main/README.md#setup-guide). + +2. For each example, create a `Config.toml` file with the related configuration. Below is an example of how your `Config.toml` file should be structured: + + ```toml + [auth] + clientId = "" + clientSecret = "" + refreshToken = "" + credentialBearer = "POST_BODY_BEARER" + ``` + ## Running an example diff --git a/examples/build.gradle b/examples/build.gradle index 051e29d..152acca 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except