diff --git a/.gitignore b/.gitignore index 2163ca8..81ec529 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,8 @@ build # Ignore Docker env file docker.env + +#Examples +examples/sales_analytics/target +examples/sales_analytics/generated +examples/sales_analytics/Config.toml diff --git a/README.md b/README.md index 0446b89..2253d4a 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,199 @@ ## 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/hubspot.crm.commerce.quotes` package offers APIs to connect and interact with [HubSpot API for CRM Quotes](https://developers.hubspot.com/docs/reference/api/crm/commerce/quotes) endpoints, specifically based on [HubSpot CRM Quotes REST API](https://developers.hubspot.com/docs/reference/api). + ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot CRM Quotes connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://app.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a [Developer Test Account](https://developers.hubspot.com/beta-docs/getting-started/account-types#developer-test-accounts) under your account + +Within app developer accounts, you can create developer test accounts to test apps and integrations without affecting any real HubSpot data. + +>**Note:** These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + +1. Go to Test Account section from the left sidebar. + + ![Hubspot developer testacc1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_1.png) + +2. Click Create developer test account. + + ![Hubspot developer testacc2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_2.png) + +3. In the dialogue box, give a name to your test account and click create. + + ![Hubspot developer testacc3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_3.png) + +### Step 3: Create a HubSpot App under your account. + +1. In your developer account, navigate to the "Apps" section. Click on "Create App" + + ![Create account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_app.png) + +2. Provide the necessary details, including the app name and description. + +### Step 4: Configure the Authentication Flow. + +1. Move to the Auth Tab. + + ![Authentication 1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_1.png) + +2. In the Scopes section, add the following scopes for your app using the "Add new scope" button. + + `crm.lists.read` + `crm.lists.write` + `cms.membership.access_groups.write` + + ![Authentication 2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_2.png) + +4. Add your Redirect URI in the relevant section. You can also use localhost addresses for local development purposes. Click Create App. + + ![Authentication 3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_3.png) + +### Step 5: Get your Client ID and Client Secret + +- Navigate to the Auth section of your app. Make sure to save the provided Client ID and Client Secret. + + ![Client Id_& Client Secret](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/clientId_clientSecret.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + +1. Create an authorization URL using the following format. + +2. Paste it in the browser and select your developer test account to intall the app when prompted. + + ![Setup auth flow](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/setup_auth_flow.png) + +3. A code will be displayed in the browser. Copy the code. + +4. Run the following curl command. Replace the ``, ` and `` with your specific value. Use the code you received in the above step 3 as the ``. + + - 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 necessary for API calls. + + ```json + { + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 + } + ``` + +5. 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 Quotes` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.commerce.quotes` module and `oauth2` module. + +```ballerina +import ballerina/oauth2; +import ballerinax/hubspot.crm.commerce.quotes as crmquotes; +``` + +### Step 2: Instantiate a new connector + +1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + + ```toml + clientId = + clientSecret = + refreshToken = + ``` + +2. Instantiate a `OAuth2RefreshTokenGrantConfig` with the obtained credentials and initialize the connector with it. + + ```ballerina + configurable string clientId = ?; + configurable string clientSecret = ?; + configurable string refreshToken = ?; + + OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + }; + + final crmlists:Client crmListClient = check new (config = {auth}); + + ``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +#### Create a CRM List + +```ballerina + +OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +public function main() returns error? { + final Client hubspotClient = check new (config = {auth}); + + // Define the payload for creating a quote + json payload = { + "name": "Test Quote", + "hs_expiration_date": "2025-12-31", + "hs_status": "DRAFT", + "hs_owner_id": "", + "hs_currency": "USD", + "hs_total_amount": 1500, + "hs_associated_deal_id": "" + }; + + // Send the request to create a quote + http:Response response = check hubspotClient->/crm/v3/objects/quotes.post(payload); + + // Print the response + io:println("Response: ", response.getJsonPayload()); +} +``` ## Examples -The `HubSpot CRM Commerce Quotes` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.commerce.quotes/tree/main/examples/), covering the following use cases: +The `HubSpot CRM Commerce Quotes` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.commerce.quotes/examples/), covering the following use cases: -[//]: # (TODO: Add examples) +1. Sales Analytics System ## Build from the source diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index efb4e7b..bbe78a8 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -5,12 +5,12 @@ name = "hubspot.crm.commerce.quotes" version = "1.0.0" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] -# icon = "icon.png" # TODO: update icon.png +keywords = ["hubspot", "commerce", "quotes"] +icon = "icon.png" repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes" [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..0fb117c --- /dev/null +++ b/ballerina/Dependencies.toml @@ -0,0 +1,328 @@ +# 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"} +] + +[[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.commerce.quotes" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "test"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.crm.commerce.quotes", moduleName = "hubspot.crm.commerce.quotes"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md index bada2c1..617df60 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -1,17 +1,193 @@ ## 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/hubspot.crm.commerce.quotes` package offers APIs to connect and interact with [HubSpot API for CRM Quotes](https://developers.hubspot.com/docs/reference/api/crm/commerce/quotes) endpoints, specifically based on [HubSpot CRM Quotes REST API](https://developers.hubspot.com/docs/reference/api). ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot CRM Quotes connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://app.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a [Developer Test Account](https://developers.hubspot.com/beta-docs/getting-started/account-types#developer-test-accounts) under your account + +Within app developer accounts, you can create developer test accounts to test apps and integrations without affecting any real HubSpot data. + +>**Note:** These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + +1. Go to Test Account section from the left sidebar. + + ![Hubspot developer testacc1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_1.png) + +2. Click Create developer test account. + + ![Hubspot developer testacc2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_2.png) + +3. In the dialogue box, give a name to your test account and click create. + + ![Hubspot developer testacc3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_3.png) + +### Step 3: Create a HubSpot App under your account. + +1. In your developer account, navigate to the "Apps" section. Click on "Create App" + + ![Create account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_app.png) + +2. Provide the necessary details, including the app name and description. + +### Step 4: Configure the Authentication Flow. + +1. Move to the Auth Tab. + + ![Authentication 1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_1.png) + +2. In the Scopes section, add the following scopes for your app using the "Add new scope" button. + + `crm.lists.read` + `crm.lists.write` + `cms.membership.access_groups.write` + + ![Authentication 2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_2.png) + +4. Add your Redirect URI in the relevant section. You can also use localhost addresses for local development purposes. Click Create App. + + ![Authentication 3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_3.png) + +### Step 5: Get your Client ID and Client Secret + +- Navigate to the Auth section of your app. Make sure to save the provided Client ID and Client Secret. + + ![Client Id_& Client Secret](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/clientId_clientSecret.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + +1. Create an authorization URL using the following format. + +2. Paste it in the browser and select your developer test account to intall the app when prompted. + + ![Setup auth flow](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/setup_auth_flow.png) + +3. A code will be displayed in the browser. Copy the code. + +4. Run the following curl command. Replace the ``, ` and `` with your specific value. Use the code you received in the above step 3 as the ``. + + - 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 necessary for API calls. + + ```json + { + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 + } + ``` + +5. 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 Quotes` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.commerce.quotes` module and `oauth2` module. + +```ballerina +import ballerina/oauth2; +import ballerinax/hubspot.crm.commerce.quotes as crmquotes; +``` + +### Step 2: Instantiate a new connector + +1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + + ```toml + clientId = + clientSecret = + refreshToken = + ``` + +2. Instantiate a `OAuth2RefreshTokenGrantConfig` with the obtained credentials and initialize the connector with it. + + ```ballerina + configurable string clientId = ?; + configurable string clientSecret = ?; + configurable string refreshToken = ?; + + OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + }; + + final crmlists:Client crmListClient = check new (config = {auth}); + + ``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +#### Create a CRM List + +```ballerina + +OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +public function main() returns error? { + final Client hubspotClient = check new (config = {auth}); + + // Define the payload for creating a quote + json payload = { + "name": "Test Quote", + "hs_expiration_date": "2025-12-31", + "hs_status": "DRAFT", + "hs_owner_id": "", + "hs_currency": "USD", + "hs_total_amount": 1500, + "hs_associated_deal_id": "" + }; + + // Send the request to create a quote + http:Response response = check hubspotClient->/crm/v3/objects/quotes.post(payload); + + // Print the response + io:println("Response: ", response.getJsonPayload()); +} +``` ## Examples The `HubSpot CRM Commerce Quotes` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.commerce.quotes/tree/main/examples/), covering the following use cases: -[//]: # (TODO: Add examples) diff --git a/ballerina/Package.md b/ballerina/Package.md index bada2c1..d1702c6 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -1,17 +1,191 @@ ## 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/hubspot.crm.commerce.quotes` package offers APIs to connect and interact with [HubSpot API for CRM Quotes](https://developers.hubspot.com/docs/reference/api/crm/commerce/quotes) endpoints, specifically based on [HubSpot CRM Quotes REST API](https://developers.hubspot.com/docs/reference/api). ## Setup guide -[//]: # (TODO: Add detailed steps to obtain credentials and configure the module.) +To use the HubSpot CRM Quotes connector, you must have access to the HubSpot API through a HubSpot developer account and a HubSpot App under it. Therefore you need to register for a developer account at HubSpot if you don't have one already. + +### Step 1: Create/Login to a HubSpot Developer Account + +If you have an account already, go to the [HubSpot developer portal](https://app.hubspot.com/) + +If you don't have a HubSpot Developer Account you can sign up to a free account [here](https://developers.hubspot.com/get-started) + +### Step 2 (Optional): Create a [Developer Test Account](https://developers.hubspot.com/beta-docs/getting-started/account-types#developer-test-accounts) under your account + +Within app developer accounts, you can create developer test accounts to test apps and integrations without affecting any real HubSpot data. + +>**Note:** These accounts are only for development and testing purposes. In production you should not use Developer Test Accounts. + +1. Go to Test Account section from the left sidebar. + + ![Hubspot developer testacc1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_1.png) + +2. Click Create developer test account. + + ![Hubspot developer testacc2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_2.png) + +3. In the dialogue box, give a name to your test account and click create. + + ![Hubspot developer testacc3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_developer_account_3.png) + +### Step 3: Create a HubSpot App under your account. + +1. In your developer account, navigate to the "Apps" section. Click on "Create App" + + ![Create account](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/create_app.png) + +2. Provide the necessary details, including the app name and description. + +### Step 4: Configure the Authentication Flow. + +1. Move to the Auth Tab. + + ![Authentication 1](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_1.png) + +2. In the Scopes section, add the following scopes for your app using the "Add new scope" button. + + `crm.lists.read` + `crm.lists.write` + `cms.membership.access_groups.write` + + ![Authentication 2](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_2.png) + +4. Add your Redirect URI in the relevant section. You can also use localhost addresses for local development purposes. Click Create App. + + ![Authentication 3](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/authentication_3.png) + +### Step 5: Get your Client ID and Client Secret + +- Navigate to the Auth section of your app. Make sure to save the provided Client ID and Client Secret. + + ![Client Id_& Client Secret](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/clientId_clientSecret.png) + +### Step 6: Setup Authentication Flow + +Before proceeding with the Quickstart, ensure you have obtained the Access Token using the following steps: + +1. Create an authorization URL using the following format. + +2. Paste it in the browser and select your developer test account to intall the app when prompted. + + ![Setup auth flow](https://raw.githubusercontent.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes/blob/main/docs/setup/resources/setup_auth_flow.png) + +3. A code will be displayed in the browser. Copy the code. + +4. Run the following curl command. Replace the ``, ` and `` with your specific value. Use the code you received in the above step 3 as the ``. + + - 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 necessary for API calls. + + ```json + { + "token_type": "bearer", + "refresh_token": "", + "access_token": "", + "expires_in": 1800 + } + ``` + +5. 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 Quotes` connector in your Ballerina application, update the `.bal` file as follows: + +### Step 1: Import the module + +Import the `hubspot.crm.commerce.quotes` module and `oauth2` module. + +```ballerina +import ballerina/oauth2; +import ballerinax/hubspot.crm.commerce.quotes as crmquotes; +``` + +### Step 2: Instantiate a new connector + +1. Create a `Config.toml` file and, configure the obtained credentials in the above steps as follows: + + ```toml + clientId = + clientSecret = + refreshToken = + ``` + +2. Instantiate a `OAuth2RefreshTokenGrantConfig` with the obtained credentials and initialize the connector with it. + + ```ballerina + configurable string clientId = ?; + configurable string clientSecret = ?; + configurable string refreshToken = ?; + + OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + }; + + final crmlists:Client crmListClient = check new (config = {auth}); + + ``` + +### Step 3: Invoke the connector operation + +Now, utilize the available connector operations. A sample usecase is shown below. + +#### Create a CRM List + +```ballerina + +OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +public function main() returns error? { + final Client hubspotClient = check new (config = {auth}); + + // Define the payload for creating a quote + SimplePublicObjectInputForCreate payload = { + associations:[], + properties:{ + "hs_title": "Test Quote", + "hs_expiration_date": "2025-01-31" + } + }; + + // Send the request to create a quote + http:Response response = check hubspotClient->/crm/v3/objects/quotes.post(payload); + + // Print the response + io:println("Response: ", response.getJsonPayload()); +} +``` ## Examples The `HubSpot CRM Commerce Quotes` connector provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/module-ballerinax-hubspot.crm.commerce.quotes/tree/main/examples/), covering the following use cases: -[//]: # (TODO: Add examples) diff --git a/ballerina/client.bal b/ballerina/client.bal index 66cdc3f..d63aa47 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -1,4 +1,4 @@ -// 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 @@ -13,3 +13,245 @@ // 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/quotes") 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 quoteId](map headers = {}) returns http:Response|error { + string resourcePath = string `/${getEncodedUri(quoteId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 = {}, *GetCrmV3ObjectsQuotes_getpageQueries queries) returns CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|error { + string resourcePath = string `/`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quoteId](map headers = {}, *GetCrmV3ObjectsQuotesQuoteid_getbyidQueries queries) returns SimplePublicObjectWithAssociations|error { + string resourcePath = string `/${getEncodedUri(quoteId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 + # + queries - Queries to be sent with the request + # + return - successful operation + resource isolated function patch [string quoteId](SimplePublicObjectInput payload, map headers = {}, *PatchCrmV3ObjectsQuotesQuoteid_updateQueries queries) returns SimplePublicObject|error { + string resourcePath = string `/${getEncodedUri(quoteId)}`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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->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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quotes 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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quotes + # + # + 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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quotes 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 = {}, *PostCrmV3ObjectsQuotesBatchRead_readQueries queries) returns BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors|error { + string resourcePath = string `/batch/read`; + map headerValues = {...headers}; + if self.apiKeyConfig is ApiKeysConfig { + headerValues["private-app-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quotes 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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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 quotes 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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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-legacy"] = self.apiKeyConfig?.private\-app\-legacy; + 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..8d57a94 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..e5ec554 --- /dev/null +++ b/ballerina/tests/mock_service.bal @@ -0,0 +1,123 @@ +// 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 + resource function delete crm/v3/objects/quotes/[string quoteId]() returns http:Response { + http:Response response = new(); + response.statusCode = http:STATUS_NO_CONTENT; + response.setPayload("Successfully deleted"); + return response; + } + + // Read + resource function get crm/v3/objects/quotes/[string quoteId](string[]? properties, string[]? propertiesWithHistory, string[]? associations, string? idProperty, boolean archived = false) returns SimplePublicObjectWithAssociations|error { + return { + id: "0", + properties: { + "hs_title": "Test Quote 0", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + } + + // Update + resource function patch crm/v3/objects/quotes/[string quoteId](string? idProperty, @http:Payload SimplePublicObjectInput payload) returns SimplePublicObject|error { + return { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + } + + // Create + resource function post crm/v3/objects/quotes(@http:Payload SimplePublicObjectInputForCreate payload) returns SimplePublicObject|error { + return { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + } + + resource function post crm/v3/objects/quotes/batch/upsert(@http:Payload BatchInputSimplePublicObjectBatchInputUpsert payload, map headers = {}) returns BatchResponseSimplePublicUpsertObject|error { + SimplePublicUpsertObject ob1 = { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + 'new: true, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + SimplePublicUpsertObject ob2 = { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + }, + 'new: true, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + return { + completedAt: "2025-01-10", + startedAt: "2025-01-08", + requestedAt: "2025-01-08", + results:[ob1,ob2], + status: "COMPLETE" + }; + } + + resource isolated function post crm/v3/objects/quotes/search(@http:Payload PublicObjectSearchRequest payload, map headers = {}) + returns CollectionResponseWithTotalSimplePublicObjectForwardPaging|error { + SimplePublicObject ob1 = { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + SimplePublicObject ob2 = { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + return { + total: 1500, + results: [ob1, ob2] + }; + } +}; diff --git a/ballerina/tests/mock_test.bal b/ballerina/tests/mock_test.bal new file mode 100644 index 0000000..fdad6b0 --- /dev/null +++ b/ballerina/tests/mock_test.bal @@ -0,0 +1,194 @@ +// 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/test; + +final Client quotesClient = check new(config = {auth:{ + token: "test-token" +}}, serviceUrl="http://localhost:9090/crm/v3/objects/quotes" +); + +// Test function for creating a quote +@test:Config{} +function testCreateAQuote() returns error? { + SimplePublicObjectInputForCreate payload = { + associations: [], + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + } + }; + + SimplePublicObject response = check quotesClient->/.post(payload); + + test:assertEquals(response.properties["hs_title"], payload.properties["hs_title"], "New quote creation failed."); +} + +// Test function to get a quote bu ID +@test:Config{} +function testGetAQuoteById() returns error? { + SimplePublicObjectWithAssociations response = check quotesClient->/["0"].get(); + + SimplePublicObjectWithAssociations expectedQuote = { + id: "0", + properties: { + "hs_title": "Test Quote 0", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + + test:assertEquals(response, expectedQuote, "Reading this quote is failed."); +} + +// Test function to archive a quote by ID +@test:Config{} +function testArchiveAQuoteById() returns error?{ + + http:Response response = check quotesClient->/["0"].delete(); + + test:assertTrue(response.statusCode == 204); +} + +// Test function to update a quote +@test:Config{} +function testUpdateAQuoteById() returns error? { + SimplePublicObjectInput payload = { + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + } + }; + + // Call the Quotes API to update the quote + SimplePublicObject response = check quotesClient->/["1"].patch(payload); + + SimplePublicObject updatedQuote = { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + + test:assertEquals(response, updatedQuote, "Quote update failed."); +} + +// Test function to upsert a quote +@test:Config{} +function testUpsertAQuote() returns error? { + BatchInputSimplePublicObjectBatchInputUpsert payload = { + inputs: [ + { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + } + }, + { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-03-31" + } + } + ] + }; + + BatchResponseSimplePublicUpsertObject response = check quotesClient->/batch/upsert.post(payload = payload); + + BatchResponseSimplePublicUpsertObject expectedBatch = { + completedAt: "2025-01-10", + startedAt: "2025-01-08", + requestedAt: "2025-01-08", + results:[ + { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + 'new: true, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }, + { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + }, + 'new: true, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + } + ], + status: "COMPLETE" + }; + + test:assertEquals(response, expectedBatch, "Quote upsert failed."); +} + +// Test function to upsert a quote +@test:Config{} +function testSearchAQuote() returns error? { + + PublicObjectSearchRequest payload = { + filterGroups: [ + { + filters: [ + { + propertyName: "hs_title", + operator: "EQ", + value: "Test Quote 1" + } + ] + } + ], + properties: ["hs_title", "hs_expiration_date"] +}; + + CollectionResponseWithTotalSimplePublicObjectForwardPaging response = check quotesClient->/search.post(payload = payload); + + SimplePublicObject batchOutput1 = { + id: "1", + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + SimplePublicObject batchOutput2 = { + id: "2", + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-01-31" + }, + createdAt: "2025-01-08", + updatedAt: "2025-01-15" + }; + + test:assertEquals(response, { + total: 1500, + results: [batchOutput1, batchOutput2] + }, "Quote search failed."); +} diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal new file mode 100644 index 0000000..35bb3c3 --- /dev/null +++ b/ballerina/tests/test.bal @@ -0,0 +1,228 @@ +// 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/test; + +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER +}; + +final Client hubspotClient = check new ({auth}); + +string testQuoteId = ""; + +boolean enableLiveServerTest = false; + +// Test function for creating a quote +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testCreateNewQuote() returns error? { + SimplePublicObjectInputForCreate payload = { + associations: [], + properties: { + "hs_title": "Test Quote", + "hs_expiration_date": "2025-01-31" + } + }; + + // Call the Quotes API to create a new quote + SimplePublicObject response = check hubspotClient->/.post(payload); + + // Set test id + testQuoteId = response.id; + + // Validate the response + test:assertEquals(response.properties["hs_title"], "Test Quote", + "New quote not created successfully."); + +} + + +// Test function for creating a batch of quotes +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testCreateNewBatchOfQuotes() returns error? { + + SimplePublicObjectInputForCreate batchInput1 = { + associations: [], + properties: { + "hs_title": "Test Quote 1", + "hs_expiration_date": "2025-02-28" + } + }; + + SimplePublicObjectInputForCreate batchInput2 = { + associations: [], + properties: { + "hs_title": "Test Quote 2", + "hs_expiration_date": "2025-04-30" + } + }; + + BatchInputSimplePublicObjectInputForCreate payload = { + inputs: [batchInput1, batchInput2] + }; + + // Call the Quotes API to create a new quote + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubspotClient->/batch/create.post(payload); + + // Validate the response + test:assertEquals(response.results.length(), payload.inputs.length(), + "New batch of quotes not created successfully."); + +} + + +// Test for retrieving all quotes +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testGetAllQuotes() returns error? { + + CollectionResponseSimplePublicObjectWithAssociationsForwardPaging response = check hubspotClient->/.get(); + + test:assertTrue(response.results.length() > 0, + msg = "No quotes found in the response."); +} + +// Test function for retrieving a quote +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testGetOneQuote() returns error? { + SimplePublicObjectWithAssociations response = check hubspotClient->/[testQuoteId].get(); + + test:assertEquals(response.id, testQuoteId, "Quote ID is missing."); +} + +// Test function for retrieving a batch of quotes +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testGetBatchOfQuotes() returns error? { + + SimplePublicObjectId batchInput0 = { + id: testQuoteId + }; + + BatchReadInputSimplePublicObjectId payload = { + properties: [], + propertiesWithHistory: [], + inputs: [batchInput0] + }; + + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubspotClient->/batch/read.post(payload); + + // Validate essential fields + test:assertEquals(response.results.length(), payload.inputs.length(), msg = string`Only ${response.results.length()} IDs found.`); +} + + +// Archive a quote by ID +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testArchiveOneQuote() returns error?{ + + http:Response response = check hubspotClient->/["0"].delete(); + + test:assertTrue(response.statusCode == 204); +} + +// Archive batch of quotes by ID +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testArchiveBatchOfQuoteById() returns error?{ + + SimplePublicObjectId id0 = {id:"0"}; + + BatchInputSimplePublicObjectId payload = { + inputs:[ + id0 + ] + }; + + http:Response response = check hubspotClient->/batch/archive.post(payload); + + test:assertTrue(response.statusCode == 204); +} + + +// Test function for updating a quote +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testUpdateOneQuote() returns error? { + SimplePublicObjectInput payload = { + properties: { + "hs_title": "Test Quote Modified", + "hs_expiration_date": "2025-03-31" + } + }; + + // Call the Quotes API to update the quote + SimplePublicObject response = check hubspotClient->/[testQuoteId].patch(payload); + + test:assertEquals(response.properties["hs_title"], "Test Quote Modified", + "Quote not updated successfully."); +} + +// Test function for updating a batch of quotes +@test:Config{ + groups: ["live_tests"], + enable: enableLiveServerTest +} +function testUpdateBatchOfQuotes() returns error? { + + SimplePublicObjectBatchInput batchInput3 = { + id: testQuoteId, + properties: { + "hs_title": "Test Quote 3", + "hs_expiration_date": "2025-04-30" + } + }; + + BatchInputSimplePublicObjectBatchInput payload = { + inputs: [batchInput3] + }; + + // Call the Quotes API to create a new quote + BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors response = check hubspotClient->/batch/update.post(payload); + + test:assertEquals(response.results.length(), payload.inputs.length(), + "Quote in response does not match the expected quote."); +} + diff --git a/ballerina/types.bal b/ballerina/types.bal new file mode 100644 index 0000000..51c75b8 --- /dev/null +++ b/ballerina/types.bal @@ -0,0 +1,363 @@ +// 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 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; +}; + +# Represents the Queries record for the operation: get-/crm/v3/objects/quotes/{quoteId}_getById +public type GetCrmV3ObjectsQuotesQuoteid_getbyidQueries 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?; + # The name of a property whose values are unique for this object type + string idProperty?; + # 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 PublicAssociationsForObject record { + AssociationSpec[] types; + PublicObjectId to; +}; + +public type BatchResponseSimplePublicObject record { + string completedAt; + string requestedAt?; + string startedAt; + record {|string...;|} links?; + SimplePublicObject[] results; + "PENDING"|"PROCESSING"|"CANCELED"|"COMPLETE" status; +}; + +public type FilterGroup record { + Filter[] filters; +}; + +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 ForwardPaging record { + NextPage next?; +}; + +public type SimplePublicObjectId record { + string id; +}; + +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: post-/crm/v3/objects/quotes/batch/read_read +public type PostCrmV3ObjectsQuotesBatchRead_readQueries record { + # Whether to return only results that have been archived. + boolean archived = false; +}; + +public type ValueWithTimestamp record { + string sourceId?; + string sourceType; + string sourceLabel?; + int:Signed32 updatedByUserId?; + string value; + string timestamp; +}; + +public type BatchInputSimplePublicObjectId record { + SimplePublicObjectId[] inputs; +}; + +# 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; +}; + +# 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 PublicObjectId record { + string id; +}; + +public type Paging record { + NextPage next?; + PreviousPage prev?; +}; + +# Represents the Queries record for the operation: patch-/crm/v3/objects/quotes/{quoteId}_update +public type PatchCrmV3ObjectsQuotesQuoteid_updateQueries record { + # The name of a property whose values are unique for this object type + string idProperty?; +}; + +public type PublicObjectSearchRequest record { + string query?; + int:Signed32 'limit?; + string after?; + string[] sorts?; + string[] properties?; + FilterGroup[] filterGroups?; +}; + +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; +}; + +# 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 CollectionResponseSimplePublicObjectWithAssociationsForwardPaging record { + ForwardPaging paging?; + SimplePublicObjectWithAssociations[] results; +}; + +public type AssociationSpec record { + "HUBSPOT_DEFINED"|"USER_DEFINED"|"INTEGRATOR_DEFINED" associationCategory; + int:Signed32 associationTypeId; +}; + +# Represents the Queries record for the operation: get-/crm/v3/objects/quotes_getPage +public type GetCrmV3ObjectsQuotes_getpageQueries 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 SimplePublicObjectWithAssociations record { + record {|CollectionResponseAssociatedId...;|} associations?; + string createdAt; + boolean archived?; + string archivedAt?; + record {|ValueWithTimestamp[]...;|} propertiesWithHistory?; + string id; + record {|string?...;|} properties; + string updatedAt; +}; + +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 PreviousPage record { + string before; + string link?; +}; + +public type BatchInputSimplePublicObjectBatchInput record { + SimplePublicObjectBatchInput[] inputs; +}; + +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 SimplePublicObjectBatchInput record { + string idProperty?; + string objectWriteTraceId?; + string id; + record {|string...;|} properties; +}; + +public type NextPage record { + string link?; + string after; +}; + +public type AssociatedId record { + string id; + string 'type; +}; + +# 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 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..b2aff1f --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,231 @@ +// 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 15d7441..f1b36a0 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -5,12 +5,12 @@ name = "hubspot.crm.commerce.quotes" version = "@toml.version@" license = ["Apache-2.0"] authors = ["Ballerina"] -keywords = [] # TODO: Add keywords -# icon = "icon.png" # TODO: Add icon +keywords = ["hubspot", "commerce", "quotes"] +icon = "icon.png" repository = "https://github.com/ballerina-platform/module-ballerinax-hubspot.crm.commerce.quotes" [build-options] observabilityIncluded = true -[platform.java21] +[platform.java17] graalvmCompatible = true diff --git a/build-config/resources/icon.png b/build-config/resources/icon.png new file mode 100644 index 0000000..8d57a94 Binary files /dev/null and b/build-config/resources/icon.png differ diff --git a/docs/license.txt b/docs/license.txt index 921a7a1..adfc413 100644 --- a/docs/license.txt +++ b/docs/license.txt @@ -1,7 +1,7 @@ // 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/authentication_1.png b/docs/setup/resources/authentication_1.png new file mode 100644 index 0000000..212a957 Binary files /dev/null and b/docs/setup/resources/authentication_1.png differ diff --git a/docs/setup/resources/authentication_2.png b/docs/setup/resources/authentication_2.png new file mode 100644 index 0000000..59b27b0 Binary files /dev/null and b/docs/setup/resources/authentication_2.png differ diff --git a/docs/setup/resources/authentication_3.png b/docs/setup/resources/authentication_3.png new file mode 100644 index 0000000..4050328 Binary files /dev/null and b/docs/setup/resources/authentication_3.png differ diff --git a/docs/setup/resources/clientId_clientSecret.png b/docs/setup/resources/clientId_clientSecret.png new file mode 100644 index 0000000..889b0cc Binary files /dev/null and b/docs/setup/resources/clientId_clientSecret.png differ diff --git a/docs/setup/resources/create_app.png b/docs/setup/resources/create_app.png new file mode 100644 index 0000000..8f16098 Binary files /dev/null and b/docs/setup/resources/create_app.png differ diff --git a/docs/setup/resources/create_developer_account_1.png b/docs/setup/resources/create_developer_account_1.png new file mode 100644 index 0000000..0609ac4 Binary files /dev/null and b/docs/setup/resources/create_developer_account_1.png differ diff --git a/docs/setup/resources/create_developer_account_2.png b/docs/setup/resources/create_developer_account_2.png new file mode 100644 index 0000000..e894448 Binary files /dev/null and b/docs/setup/resources/create_developer_account_2.png differ diff --git a/docs/setup/resources/create_developer_account_3.png b/docs/setup/resources/create_developer_account_3.png new file mode 100644 index 0000000..a98b580 Binary files /dev/null and b/docs/setup/resources/create_developer_account_3.png differ diff --git a/docs/setup/resources/setup_auth_flow.png b/docs/setup/resources/setup_auth_flow.png new file mode 100644 index 0000000..af106bc Binary files /dev/null and b/docs/setup/resources/setup_auth_flow.png differ diff --git a/docs/spec/quotes.json b/docs/spec/quotes.json new file mode 100644 index 0000000..e0be725 --- /dev/null +++ b/docs/spec/quotes.json @@ -0,0 +1,1688 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "Quotes", + "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" : "Create a contract proposal for a customer who is interested in signing up for one of your annual SEO auditing service packages.", + "x-hubspot-related-documentation" : [ { + "name" : "Quotes guide", + "url" : "https://developers.hubspot.com/beta-docs/guides/api/crm/commerce/quotes" + } ], + "x-hubspot-introduction" : "Use the quotes API to create and manage sales quotes for sharing pricing information with potential buyers." + }, + "servers" : [ { + "url" : "https://api.hubapi.com/crm/v3/objects/quotes" + } ], + "tags" : [ { + "name" : "Batch" + }, { + "name" : "Basic" + }, { + "name" : "Search" + } ], + "paths" : { + "/batch/read" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Read a batch of quotes by internal ID, or unique property values", + "operationId" : "post-/crm/v3/objects/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.read" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.read" ] + } ] + } + }, + "/{quoteId}" : { + "get" : { + "tags" : [ "Basic" ], + "summary" : "Read", + "description" : "Read an Object identified by `{quoteId}`. `{quoteId}` refers to the internal object ID by default, or optionally any unique property value as specified by the `idProperty` query param. Control what is returned via the `properties` query param.", + "operationId" : "get-/crm/v3/objects/quotes/{quoteId}_getById", + "parameters" : [ { + "name" : "quoteId", + "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 + } + }, { + "name" : "idProperty", + "in" : "query", + "description" : "The name of a property whose values are unique for this object type", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SimplePublicObjectWithAssociations" + } + } + } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.read" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.read" ] + } ] + }, + "delete" : { + "tags" : [ "Basic" ], + "summary" : "Archive", + "description" : "Move an Object identified by `{quoteId}` to the recycling bin.", + "operationId" : "delete-/crm/v3/objects/quotes/{quoteId}_archive", + "parameters" : [ { + "name" : "quoteId", + "in" : "path", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No content", + "content" : { } + }, + "default" : { + "$ref" : "#/components/responses/Error" + } + }, + "security" : [ { + "oauth2_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + }, + "patch" : { + "tags" : [ "Basic" ], + "summary" : "Update", + "description" : "Perform a partial update of an Object identified by `{quoteId}`. `{quoteId}` refers to the internal object ID by default, or optionally any unique property value as specified by the `idProperty` query param. Provided property values will be overwritten. Read-only and non-existent properties will be ignored. Properties values can be cleared by passing an empty string.", + "operationId" : "patch-/crm/v3/objects/quotes/{quoteId}_update", + "parameters" : [ { + "name" : "quoteId", + "in" : "path", + "required" : true, + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + }, { + "name" : "idProperty", + "in" : "query", + "description" : "The name of a property whose values are unique for this object type", + "required" : false, + "style" : "form", + "explode" : true, + "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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/batch/archive" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Archive a batch of quotes by ID", + "operationId" : "post-/crm/v3/objects/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/batch/create" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Create a batch of quotes", + "operationId" : "post-/crm/v3/objects/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/batch/update" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Update a batch of quotes by internal ID, or unique property values", + "operationId" : "post-/crm/v3/objects/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/" : { + "get" : { + "tags" : [ "Basic" ], + "summary" : "List", + "description" : "Read a page of quotes. Control what is returned via the `properties` query param.", + "operationId" : "get-/crm/v3/objects/quotes_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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.read" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.read" ] + } ] + }, + "post" : { + "tags" : [ "Basic" ], + "summary" : "Create", + "description" : "Create a quote with the given properties and return a copy of the object, including the ID. Documentation and examples for creating standard quotes is provided.", + "operationId" : "post-/crm/v3/objects/quotes_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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/batch/upsert" : { + "post" : { + "tags" : [ "Batch" ], + "summary" : "Create or update a batch of quotes 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/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.write" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.write" ] + } ] + } + }, + "/search" : { + "post" : { + "tags" : [ "Search" ], + "operationId" : "post-/crm/v3/objects/quotes/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_legacy" : [ "e-commerce" ] + }, { + "oauth2" : [ "crm.objects.quotes.read" ] + }, { + "private_apps_legacy" : [ "e-commerce" ] + }, { + "private_apps" : [ "crm.objects.quotes.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" : { + "required" : [ "to", "types" ], + "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" : "datetime" + }, + "requestedAt" : { + "type" : "string", + "format" : "datetime" + }, + "startedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "datetime" + }, + "startedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + }, + "requestedAt" : { + "type" : "string", + "format" : "datetime" + }, + "startedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + } + } + }, + "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" : "datetime" + }, + "archived" : { + "type" : "boolean", + "example" : false + }, + "archivedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + } + }, + "example" : { + "id" : "512", + "properties" : { + "hs_createdate" : "2019-10-30T03:30:17.883Z", + "hs_expiration_date" : "2020-09-06T02:43:14.491Z", + "hs_quote_amount" : "3000.00", + "hs_quote_number" : "20200916-092813983", + "hs_status" : "PENDING_APPROVAL", + "hs_terms" : "discount provided, two year term with customer", + "hs_title" : "Services Proposal", + "hubspot_owner_id" : "910901" + }, + "createdAt" : "2019-10-30T03:30:17.883Z", + "updatedAt" : "2019-12-07T16:50:06.678Z", + "archived" : false + } + }, + "PublicObjectId" : { + "required" : [ "id" ], + "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" : "datetime" + }, + "numErrors" : { + "type" : "integer", + "format" : "int32" + }, + "requestedAt" : { + "type" : "string", + "format" : "datetime" + }, + "startedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" ] + } + } + }, + "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" : { + "hs_title" : "Pawnee Paper Deal", + "hs_status" : "PENDING_APPROVAL" + }, + "associations" : [ { + "to" : { + "id" : "101" + }, + "types" : [ { + "associationCategory" : "HUBSPOT_DEFINED", + "associationTypeId" : 2 + } ] + } ] + } + }, + "CollectionResponseSimplePublicObjectWithAssociationsForwardPaging" : { + "required" : [ "results" ], + "type" : "object", + "properties" : { + "paging" : { + "$ref" : "#/components/schemas/ForwardPaging" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SimplePublicObjectWithAssociations" + } + } + } + }, + "AssociationSpec" : { + "required" : [ "associationCategory", "associationTypeId" ], + "type" : "object", + "properties" : { + "associationCategory" : { + "type" : "string", + "enum" : [ "HUBSPOT_DEFINED", "USER_DEFINED", "INTEGRATOR_DEFINED" ] + }, + "associationTypeId" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "SimplePublicObjectWithAssociations" : { + "required" : [ "createdAt", "id", "properties", "updatedAt" ], + "type" : "object", + "properties" : { + "associations" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/CollectionResponseAssociatedId" + } + }, + "createdAt" : { + "type" : "string", + "format" : "datetime" + }, + "archived" : { + "type" : "boolean" + }, + "archivedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + } + }, + "example" : { + "properties" : { + "hs_createdate" : "2019-10-30T03:30:17.883Z", + "hs_expiration_date" : "2020-09-06T02:43:14.491Z", + "hs_quote_amount" : "3000.00", + "hs_quote_number" : "20200916-092813983", + "hs_status" : "PENDING_APPROVAL", + "hs_terms" : "discount provided, two year term with customer", + "hs_title" : "Services Proposal", + "hubspot_owner_id" : "910901" + } + } + }, + "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" : "datetime" + }, + "archived" : { + "type" : "boolean" + }, + "archivedAt" : { + "type" : "string", + "format" : "datetime" + }, + "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" : "datetime" + } + } + }, + "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" : { + "hs_title" : "Pawnee Paper Deal", + "hs_status" : "PENDING_APPROVAL" + } + } + }, + "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" : [ "associations", "properties" ], + "type" : "object", + "properties" : { + "associations" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PublicAssociationsForObject" + } + }, + "objectWriteTraceId" : { + "type" : "string" + }, + "properties" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + }, + "example" : { + "properties" : { + "hs_title" : "Pawnee Paper Deal", + "hs_status" : "PENDING_APPROVAL" + }, + "associations" : [ { + "to" : { + "id" : "101" + }, + "types" : [ { + "associationCategory" : "HUBSPOT_DEFINED", + "associationTypeId" : 2 + } ] + } ] + } + } + }, + "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 4c71d11..7dd6c45 100644 --- a/docs/spec/sanitations.md +++ b/docs/spec/sanitations.md @@ -1,6 +1,6 @@ -_Author_: \ -_Created_: \ -_Updated_: \ +_Author_: @harithmaduranga \ +_Created_: 2025/01/09 \ +_Updated_: 2025/01/09 \ _Edition_: Swan Lake # Sanitation for OpenAPI specification @@ -9,16 +9,24 @@ This document records the sanitation done on top of the official OpenAPI specifi The OpenAPI specification is obtained from (TODO: Add source link). 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/quotes](https://api.hubapi.com/crm/v3/objects/quotes) + - **Reason**: This change is made to ensure that all API paths are relative to the versioned base URL 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/quotes). + - **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/quotes/batch/read + - **Updated**: /batch/read + - **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 quotes.json --mode client --license docs/license.txt -o ballerina``` Note: The license year is hardcoded to 2024, change if necessary. diff --git a/examples/README.md b/examples/README.md index 7a6cbf9..64b4168 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,13 +2,20 @@ The `ballerinax/hubspot.crm.commerce.quotes` connector provides practical examples illustrating usage in various scenarios. -[//]: # (TODO: Add examples) -1. -2. +1. [Sales Analytics System](./sales_analytics/) - A store can insert their quotes to the system, and system record and analyses the details on the quotes. ## Prerequisites -[//]: # (TODO: Add prerequisites) +1. Generate HubSpot credentials to authenticate the connector as described in the [Setup guide](https://central.ballerina.io/ballerinax/twitter/latest#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/sales_analytics/.devcontainer.json b/examples/sales_analytics/.devcontainer.json new file mode 100644 index 0000000..75bd926 --- /dev/null +++ b/examples/sales_analytics/.devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.10.3", + "customizations": { + "vscode": { + "extensions": ["WSO2.ballerina"] + } + } +} diff --git a/examples/sales_analytics/Ballerina.toml b/examples/sales_analytics/Ballerina.toml new file mode 100644 index 0000000..9515f61 --- /dev/null +++ b/examples/sales_analytics/Ballerina.toml @@ -0,0 +1,14 @@ +[package] +org = "wso2" +name = "sales_analytics" +version = "0.1.0" +distribution = "2201.10.3" + +[[dependency]] +org = "ballerinax" +name = "hubspot.crm.commerce.quotes" +version = "1.0.0" +repository = "local" + +[build-options] +observabilityIncluded = true diff --git a/examples/sales_analytics/Dependencies.toml b/examples/sales_analytics/Dependencies.toml new file mode 100644 index 0000000..0dc7c04 --- /dev/null +++ b/examples/sales_analytics/Dependencies.toml @@ -0,0 +1,321 @@ +# 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"} +] + +[[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"} +] +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"} +] + +[[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.commerce.quotes" +version = "1.0.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] +modules = [ + {org = "ballerinax", packageName = "hubspot.crm.commerce.quotes", moduleName = "hubspot.crm.commerce.quotes"} +] + +[[package]] +org = "wso2" +name = "sales_analytics" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "hubspot.crm.commerce.quotes"} +] +modules = [ + {org = "wso2", packageName = "sales_analytics", moduleName = "sales_analytics"} +] + diff --git a/examples/sales_analytics/README.md b/examples/sales_analytics/README.md new file mode 100644 index 0000000..2fdee04 --- /dev/null +++ b/examples/sales_analytics/README.md @@ -0,0 +1,29 @@ +## Sales Analytics System + +A store can insert, update and delete their daily quotes in the system. +System records and analyses the data on the quotes, and user can get recorded data and analitics. + +## Prerequisites + +### 1. Setup Hubspot developer account + +Refer to the [Setup guide](../../ballerina/Package.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/sales_analytics/main.bal b/examples/sales_analytics/main.bal new file mode 100644 index 0000000..84ee1c9 --- /dev/null +++ b/examples/sales_analytics/main.bal @@ -0,0 +1,133 @@ +// 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 ballerina/oauth2; +import ballerinax/hubspot.crm.commerce.quotes as hsQuotes; + +configurable string clientId = ?; +configurable string clientSecret = ?; +configurable string refreshToken = ?; + +public function main() returns error? { + + hsQuotes:OAuth2RefreshTokenGrantConfig auth = { + clientId, + clientSecret, + refreshToken, + credentialBearer: oauth2:POST_BODY_BEARER + }; + + final hsQuotes:Client storeClient = check new ({auth}); + + // Add new sales quote + hsQuotes:SimplePublicObjectInputForCreate payload = { + associations: [], + properties: { + "hs_title": "Premium Subscription Quote", + "hs_expiration_date": "2025-02-28", + "hs_currency": "USD" + } + }; + var createdNewQuote = check storeClient->/.post(payload); + string quoteId = createdNewQuote.id; + io:println(createdNewQuote); + + // Add a batch of quotes + hsQuotes:SimplePublicObjectInputForCreate batchInput1 = { + associations: [], + properties: { + "hs_title": "Quote 1", + "hs_expiration_date": "2025-02-28" + } + }; + hsQuotes:SimplePublicObjectInputForCreate batchInput2 = { + associations: [], + properties: { + "hs_title": "Quote 2", + "hs_expiration_date": "2025-04-30" + } + }; + hsQuotes:BatchInputSimplePublicObjectInputForCreate batchCreatePayload = { + inputs: [batchInput1, batchInput2] + }; + + // Call the Quotes API to create a new quote + hsQuotes:BatchResponseSimplePublicObject|hsQuotes:BatchResponseSimplePublicObjectWithErrors createdQuotes = check storeClient->/batch/create.post(batchCreatePayload); + io:println(createdQuotes.results); + + // Get all existing sales quotes + hsQuotes:CollectionResponseSimplePublicObjectWithAssociationsForwardPaging allExistingQuotes = check storeClient->/.get(); + io:println(allExistingQuotes); + + // Get one sales quote by ID + hsQuotes:SimplePublicObjectWithAssociations quote = check storeClient->/[quoteId].get(); + io:println(quote); + + // Get a batch of quotes + hsQuotes:SimplePublicObjectId ob0 = { + id: quoteId + }; + hsQuotes:BatchReadInputSimplePublicObjectId batchGetPayload = { + properties: [], + propertiesWithHistory: [], + inputs: [ob0] + }; + hsQuotes:BatchResponseSimplePublicObject|hsQuotes:BatchResponseSimplePublicObjectWithErrors retrievedQuotes = check storeClient->/batch/read.post(batchGetPayload); + io:println(retrievedQuotes.results); + + // Update one sales quote by ID + hsQuotes:SimplePublicObjectInput modifyPayload = { + properties: { + "hs_title": "Premium Subscription Quote", + "hs_expiration_date": "2025-03-31", + "hs_currency": "USD" + } + }; + hsQuotes:SimplePublicObject modifiedQuote = check storeClient->/[quoteId].patch(modifyPayload); + io:println(modifiedQuote); + + // Update a batch of quotes + hsQuotes:SimplePublicObjectBatchInput batchInput3 = { + id: quoteId, + properties: { + "hs_title": "Test Quote 3", + "hs_expiration_date": "2025-04-30" + } + }; + hsQuotes:BatchInputSimplePublicObjectBatchInput batchUpdatePayload = { + inputs: [batchInput3] + }; + + // Call the Quotes API to create a new quote + hsQuotes:BatchResponseSimplePublicObject|hsQuotes:BatchResponseSimplePublicObjectWithErrors modifiedQuotes = check storeClient->/batch/update.post(batchUpdatePayload); + io:println(modifiedQuotes.results); + + // Archive one sales quote by ID + http:Response archive_response = check storeClient->/[quoteId].delete(); + io:println(archive_response); + + // Archive a batch of quotes + hsQuotes:SimplePublicObjectId id0 = {id:"0"}; + hsQuotes:BatchInputSimplePublicObjectId batchArchivePayload = { + inputs:[ + id0 + ] + }; + http:Response batchArchiveResponse = check storeClient->/batch/archive.post(batchArchivePayload); + io:println(batchArchiveResponse); +}