diff --git a/.circleci/config.yml b/.circleci/config.yml index b5bb3eb..7b08763 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,7 +82,7 @@ jobs: override-ci-command: npm install - run: name: Audit Dependencies - command: npm audit --production --audit-level=high + command: npm run audit - run: name: Running Mocha Tests command: npm test diff --git a/.nsprc b/.nsprc index 1772f0e..273f24a 100644 --- a/.nsprc +++ b/.nsprc @@ -1,6 +1,11 @@ { - "GHSA-27h2-hvpr-p74q": { - "active": true, - "notes": "We don't use verify function from jsonwebtoken, so not affected" - } -} + "GHSA-fjxv-7rqg-78g4": { + "active": true + }, + "GHSA-jr5f-v2jv-69x6": { + "active": true + }, + "GHSA-4hjh-wcwx-xvwj": { + "active": true + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 38bd6bd..a0e16c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 2.9.0 (October 15, 2025) +* **Major Improvement** to `Get Updated Objects Polling` trigger: + * Re-architected the polling mechanism to use keyset pagination (`LastModifiedDate` and `Id`). + * **Fixed a critical bug** that caused silent data loss when a large number of records had the identical `LastModifiedDate`. + * Fixed the underlying cause of a potential infinite loop when a full page of records shared the same timestamp. + * Disallowed `Size of Polling Page` to be 0. +* Updated and improved the `README.md` documentation for clarity, consistency, and accuracy, including: + * Adding a `Required Permissions` section for the polling trigger. + * Removing obsolete limitations that were resolved by the bug fixes. + * General language and formatting enhancements. + ## 2.8.6 (January 31, 2025) * Upgrade Sailor version to 2.7.4 * Enhanced error message text in the `Raw Request` action diff --git a/README.md b/README.md index ecb28f3..13305ea 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ # Salesforce Component ## Table of Contents -* [General information](#general-information) +* [General Information](#general-information) * [Description](#description) * [Completeness Matrix](#completeness-matrix) - * [API version](#api-version) - * [Environment variables](#environment-variables) + * [API Version](#api-version) + * [Environment Variables](#environment-variables) * [Credentials](#credentials) * [Triggers](#triggers) * [Get Updated Objects Polling](#get-updated-objects-polling) * [Query Trigger](#query-trigger) - * [Subscribe to platform events (REALTIME FLOWS ONLY)](#subscribe-to-platform-events-realtime-flows-only) - * [Subscribe to PubSub](#subscribe-to-pubsub) - * [Deprecated triggers](#deprecated-triggers) + * [Subscribe to Platform Events (Real-time Flows Only)](#subscribe-to-platform-events-real-time-flows-only) + * [Subscribe to Pub/Sub Events](#subscribe-to-pubsub-events) + * [Deprecated Triggers](#deprecated-triggers) * [Actions](#actions) * [Bulk Create/Update/Delete/Upsert](#bulk-createupdatedeleteupsert) * [Bulk Query](#bulk-query) @@ -27,420 +27,295 @@ * [Permissions](#permissions) * [Known Limitations](#known-limitations) -## General information +## General Information ### Description -[elastic.io](http://www.elastic.io;) iPaaS component that connects to Salesforce API +An [elastic.io](http://www.elastic.io;) component for seamless integration with the Salesforce REST API, enabling you to trigger flows based on Salesforce data and perform various actions. ### Completeness Matrix -![Salesforse-component Completeness Matrix](https://user-images.githubusercontent.com/16806832/93742890-972ca200-fbf7-11ea-9b7c-4a0aeff1c0fb.png) +![Salesforce-component Completeness Matrix](https://user-images.githubusercontent.com/16806832/93742890-972ca200-fbf7-11ea-9b7c-4a0aeff1c0fb.png) -[Salesforse-component Completeness Matrix](https://docs.google.com/spreadsheets/d/1_4vvDLdQeXqs3c8OxFYE80CvpeSC8e3Wmwl1dcEGO2Q/edit?usp=sharing) +[Salesforce-component Completeness Matrix](https://docs.google.com/spreadsheets/d/1_4vvDLdQeXqs3c8OxFYE80CvpeSC8e3Wmwl1dcEGO2Q/edit?usp=sharing) -### API version -The component uses Salesforce - API Version 46.0 by defaults but can be overwritten by the environment variable `SALESFORCE_API_VERSION` +### API Version +The component uses Salesforce API Version 46.0 by default, but this can be overridden by the environment variable `SALESFORCE_API_VERSION`. -### Environment variables -| Name | Mandatory | Description | Values | -|------------------------|-----------|--------------------------------------------------------------------------------------|--------------------------| -| SALESFORCE_API_VERSION | false | Determines API version of Salesforce to use | Default: `46.0` | -| REFRESH_TOKEN_RETRIES | false | Determines how many retries to refresh token should be done before throwing an error | Default: `10` | -| HASH_LIMIT_TIME | false | Hash expiration time in ms | Default: `600000` | -| HASH_LIMIT_ELEMENTS | false | Hash size number limit | Default: `10` | -| UPSERT_TIME_OUT | false | Time out for `Upsert Object` action in ms | Default: `120000` (2min) | +### Environment Variables +| Name | Mandatory | Description | Values | +|------------------------|-----------|--------------------------------------------------------------------------|--------------------------| +| `SALESFORCE_API_VERSION` | No | Overrides the default Salesforce API version. | Default: `46.0` | +| `REFRESH_TOKEN_RETRIES` | No | The number of retries to refresh a token before throwing an error. | Default: `10` | +| `HASH_LIMIT_TIME` | No | Cache expiration time in milliseconds for `Lookup` actions. | Default: `600000` | +| `HASH_LIMIT_ELEMENTS` | No | The maximum number of entries in the cache for `Lookup` actions. | Default: `10` | +| `UPSERT_TIME_OUT` | No | Timeout for the `Upsert Object` action in milliseconds. | Default: `120000` (2min) | ## Credentials Authentication occurs via OAuth 2.0. -In order to make OAuth work, you need a new App in your Salesforce. During app creation process you will be asked to specify -the callback URL, to process OAuth authentication via elastic.io platform your callback URL should be ``https://your-tenant.elastic.io/callback/oauth2``. -More information you can find [here](https://help.salesforce.com/apex/HTViewHelpDoc?id=connected_app_create.htm). -During credentials creation you would need to: -- select existing Auth Client from drop-down list ``Choose Auth Client`` or create the new one. -For creating Auth Client you should specify following fields: +To use OAuth 2.0, you must create a **Connected App** in your Salesforce instance. During the app creation process, you will be asked to specify a **Callback URL**. To process OAuth authentication via the elastic.io platform, your callback URL should be in the format `https://your-tenant.elastic.io/callback/oauth2`. -| Field name | Mandatory | Description | -|------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Name | true | your Auth Client's name | -| Client ID | true | your OAuth client key | -| Client Secret | true | your OAuth client secret | -| Authorization Endpoint | true | your OAuth authorization endpoint. For production use `https://login.salesforce.com/services/oauth2/authorize`, for sandbox - `https://test.salesforce.com/services/oauth2/authorize` | -| Token Endpoint | true | your OAuth Token endpoint for refreshing access token. For production use `https://login.salesforce.com/services/oauth2/token`, for sandbox - `https://test.salesforce.com/services/oauth2/token` | +More information can be found in the official [Salesforce documentation](https://help.salesforce.com/apex/HTViewHelpDoc?id=connected_app_create.htm). -- fill field ``Name Your Credential`` -- click on ``Authenticate`` button - if you have not logged in Salesforce before then log in by entering data in the login window that appears -- click on ``Verify`` button for verifying your credentials -- click on ``Save`` button for saving your credentials +To create credentials in the elastic.io platform: -**Please note:** Salesforce migration or any changes that affect endpoints, single sign-on (SSO), OAuth and JSON web tokens (JWT), and other connections can lead to unpredictable behavior that can cause authentication issues. To avoid this after making changes you need to create new credentials and authenticate again, once this is done the old ones can be safely removed from the platform. +1. Select an existing Auth Client from the **Choose Auth Client** dropdown or create a new one. To create a new Auth Client, specify the following fields: + + | Field Name | Mandatory | Description | + |------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | Name | Yes | A name for your Auth Client. | + | Client ID | Yes | Your Connected App's Consumer Key. | + | Client Secret | Yes | Your Connected App's Consumer Secret. | + | Authorization Endpoint | Yes | Your OAuth authorization endpoint. For production, use `https://login.salesforce.com/services/oauth2/authorize`. For sandboxes, use `https://test.salesforce.com/services/oauth2/authorize`. | + | Token Endpoint | Yes | Your OAuth token endpoint for refreshing access tokens. For production, use `https://login.salesforce.com/services/oauth2/token`. For sandboxes, use `https://test.salesforce.com/services/oauth2/token`. | + +2. Provide a name for your credential in the **Name Your Credential** field. +3. Click **Authenticate**. If you are not already logged into Salesforce, a login window will appear. Please enter your credentials. +4. Click **Verify** to confirm the credentials are working. +5. Click **Save** to save your credentials. + +> **Please Note:** Salesforce migrations or any changes that affect endpoints, single sign-on (SSO), OAuth, or other connections can lead to unpredictable behavior and authentication issues. To avoid this, you must create new credentials after making such changes. Once the new credentials are created and verified, the old ones can be safely removed. ## Triggers ### Get Updated Objects Polling -### Config Fields - * **Object Type** Dropdown: Indicates Object Type to be fetched - * **Selected Fields** Multiselect dropdown: list with all Object Fields. Select fields, which will be returned in response. That can prevent [431 and 414 Errors](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_api.htm). - * **Include linked objects** Multiselect dropdown: list with all the related child and parent objects of the selected object type. List entries are given as `Object Name/Reference To (Relationship Name)`. Select one or more related objects, which will be join queried and included in the response from your Salesforce Organization. Please see the **Limitations** section below for use case advisories. - * **Emit behavior** Dropdown: Indicates emit objects individually or emit by page - * **Start Time** - TextField (string, optional): Indicates the beginning time to start retrieving events from in ISO 8601 Date time utc format - YYYY-MM-DDThh:mm:ssZ - * **End Time** - TextField (string, optional, defaults to never): If provided, don’t fetch records modified after this time in ISO 8601 Date time utc format - YYYY-MM-DDThh:mm:ssZ - * **Size of Polling Page** - TextField (optional, positive integer, max 10000, defaults to 10000): Indicates the size of pages to be fetched - * **Process Single Page Per Execution** - Checkbox: Indicates that if the number of changed records exceeds the maximum number of results in a page, instead of fetching the next page immediately, wait until the next flow start to fetch the next page +Polls for objects that have been created or updated within a given time frame. + +#### Configuration Fields +* **Object Type** (dropdown, required): The type of Salesforce object to be fetched. +* **Selected Fields** (multiselect, optional): A list of fields to be returned in the response. If left empty, all fields will be returned. Selecting only the necessary fields can prevent [431 and 414 Errors](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_api.htm). +* **Include linked objects** (multiselect, optional): A list of related child and parent objects to be join-queried and included in the response. List entries are given as `Object Name/Reference To (Relationship Name)`. +* **Emit behavior** (dropdown, required): Choose to emit objects `Individually` or as a single `Fetch page`. +* **Start Time** (string, optional): The beginning of the time window to retrieve objects from, in ISO 8601 format (`YYYY-MM-DDThh:mm:ssZ`). Defaults to the beginning of time (`1970-01-01T00:00:00.000Z`). +* **End Time** (string, optional): If provided, the trigger will not fetch records modified after this time. Must be in ISO 8601 format. +* **Size of Polling Page** (integer, optional): The maximum number of records to fetch per page. Defaults to `10000`. +* **Process Single Page Per Execution** (checkbox, optional): If checked, the trigger will process only one page of results per flow execution. If unchecked, it will retrieve all pages in a single execution. + +#### Required Permissions +Due to the trigger's use of keyset pagination for reliability, the user profile for the credential must have **read access** (Field-Level Security) to the `Id` and `LastModifiedDate` fields for the object being polled. Without these permissions, the trigger will fail. #### Output Metadata -- For `Fetch page`: An object with key ***results*** that has an array as its value -- For `Emit Individually`: Each object fill the entire message +* **For `Fetch page`:** An object with a `results` property, which contains an array of records. +* **For `Emit Individually`:** Each record is emitted as a separate message. ### Limitations - * If records reach `Size of Polling Page` flow will find largest update date and use it as `Start Time` for next iterations, results with this date will be excluded from that iteration and include in the next one. - * If all records from one iteration will have same 'LastModifiedDate' they will be proceed, but all objects in the next iteration will start from date strictly greater than this, to avoid this use bigger `Size of Polling Page` - * Highly not recommended use very small (less than 5) `Size of Polling Page` (look at previous point) - * When a binary field (primitive type `base64`, e.g. Documents, Attachments, etc) is selected on **Include linked objects**, an error will be thrown: `MALFORMED_QUERY: Binary fields cannot be selected in join queries. Instead of querying objects with binary fields as linked objects (such as children Attachments), try querying them directly.` There is also a limit to the number of linked objects that you can query at once - beyond two or three, depending on the number of fields in the linked objects, Salesforce could potentially return a Status Code 431 or 414 error, meaning the query is too long. Finally, due to a bug with multiselect dropdowns, it is recommended to deselect all of the elements in this field before you change your selection in the *Object* dropdown list. +* When a binary field (primitive type `base64`, e.g., in Documents or Attachments) is selected via **Include linked objects**, an error will be thrown: `MALFORMED_QUERY: Binary fields cannot be selected in join queries.` Instead of querying these as linked objects, query them directly. +* There is a limit to the number of linked objects that can be queried at once. Beyond two or three (depending on the number of fields), Salesforce may return a `431` or `414` error, indicating the query is too long. +* Due to a known issue with multiselect dropdowns, it is recommended to deselect all items in the **Include linked objects** field before changing the **Object Type**. ### Query Trigger -Continuously runs the same SOQL Query and emits results according to ``Output method`` configuration field. -Use the Salesforce Object Query Language (SOQL) to search your organization’s Salesforce data for specific information. -SOQL is similar to the SELECT statement in the widely used Structured Query Language (SQL) but is designed specifically for Salesforce data. -This trigger allows you to interact with your data using SOQL. - -#### List of Expected Config fields +Executes a user-defined SOQL query during each polling interval to fetch records. -* **SOQL Query** - Input field for your SOQL Query -* **Output method** - dropdown list with options: `Emit all` - all found records will be emitted in one array `records`, and `Emit individually` - each found object will be emitted individual. Optional field, defaults to: `Emit individually`. -* **Don't emit on empty results** - checkbox, optional. If selected, component will not produce empty messages when result is empty. +Use the Salesforce Object Query Language (SOQL) to search your organization’s Salesforce data for specific information. SOQL is similar to the `SELECT` statement in SQL but is designed specifically for Salesforce data. -### Subscribe to platform events (REALTIME FLOWS ONLY) -This trigger will subscribe for any platform Event using Salesforce streaming API. +#### Configuration Fields +* **SOQL Query** (string, required): The SOQL query to execute. +* **Output method** (dropdown, optional): `Emit all` emits all found records in a single message with a `records` array. `Emit individually` emits each record as a separate message. Defaults to `Emit individually`. +* **Don't emit on empty results** (checkbox, optional): If selected, the component will not produce an empty message if the query returns no results. -### Limittions: -* SUPPORTS REALTIME FLOWS ONLY -* `Run Now` action is required after the flow transitions from SUSPEND to RESUME -* Due to Salesforce API limitations, the trigger does not store messages in the queues during the SUSPEND state. To continue receiving and processing messages, the flow should be triggered by the Run Now action after RESUME. +### Subscribe to Platform Events (Real-time Flows Only) +Subscribes to a specified Platform Event using the Salesforce Streaming API. -#### Input field description -* **Event object name** - Input field where you should select the type of platform event which you want to subscribe E.g. `My platform event` +#### How to Create a Platform Event +In Salesforce, navigate to `Setup --> Integrations --> Platform Events --> New Platform Event`. -#### How to create new custom Platform event Entity: -`Setup --> Integrations --> Platform Events --> New Platform Event` ![Screenshot from 2019-03-11 11-51-10](https://user-images.githubusercontent.com/13310949/54114889-1088e900-43f4-11e9-8b49-3a8113b6577d.png) -You can find more detail information in the [Platform Events Intro Documentation](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm). +For more details, see the [Platform Events Intro Documentation](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm). -#### Limitations: -At the moment this trigger can be used only for **"Realtime"** flows. +#### Configuration Fields +* **Event object name** (dropdown, required): The name of the Platform Event to subscribe to (e.g., `My_Platform_Event__e`). + +#### Limitations +* **This trigger is designed for Real-time flows only** and is not supported in Ordinary flows. +* Due to Salesforce API limitations, the trigger does not queue messages while the flow is in a `SUSPEND` state. To resume processing messages, you must manually trigger the flow with the **Run Now** action after it resumes. -### Subscribe to PubSub -This trigger will subscribe for any platform Event using [Pub/Sub API](https://developer.salesforce.com/docs/platform/pub-sub-api/overview). +### Subscribe to Pub/Sub Events +Subscribes to a specified Platform Event using the Salesforce Pub/Sub API. #### Configuration Fields -* **Event object name** - (dropdown, required): Input field where you should select the type of platform event to which you want to subscribe E.g. `My platform event` -* **Pub/Sub API Endpoint** - (string, optional): You can set Pub/Sub API Endpoint manually or leave it blank for default: `api.pubsub.salesforce.com:7443`. Details about Pub/Sub API Endpoints can be found [here](https://developer.salesforce.com/docs/platform/pub-sub-api/guide/pub-sub-endpoints.html) -* **Number of events per request** - (positive integer, optional, defaults to 10, max 100): Salesforce uses batches of events to deliver to the component, the bigger number may increase processing speed, but if the batch size is too big, you can get out of memory error. If there are fewer events ready than the batch size, they will be delivered anyway. -* **Start from Replay Id** - (positive integer, optional): In the Salesforce platform events and change data capture events are retained in the event bus for 3 days and you can subscribe at any position in the stream by providing here Replay Id from the last event. This field is used only for the first execution, following executions will use the Replay Id from the latest event to get a new one. +* **Event object name** (dropdown, required): The name of the Platform Event to subscribe to. +* **Pub/Sub API Endpoint** (string, optional): The Pub/Sub API endpoint. If left blank, it defaults to `api.pubsub.salesforce.com:7443`. For more details, see the [Pub/Sub API Endpoints documentation](https://developer.salesforce.com/docs/platform/pub-sub-api/guide/pub-sub-endpoints.html). +* **Number of events per request** (integer, optional): The maximum number of events to retrieve in a single batch. A larger batch size may improve performance, but setting it too high can cause memory errors. Defaults to `10` (max `100`). +* **Start from Replay ID** (integer, optional): A specific Replay ID to start the event stream from. This is only used for the first execution. Subsequent executions will automatically use the Replay ID from the last processed event. -#### Input Metadata +#### Output Metadata +* **event**: An object containing the `replayId` of the message. +* **payload**: An object containing the dynamically generated content of the event. -None. +#### Limitations +* The component begins tracking changes after the first execution. You must run the flow once (either manually or on its schedule) to establish the connection before events will be detected. +* If you are using an **Ordinary (polling) flow**, you must ensure it executes at least once every 3 days, as Salesforce retains events for a maximum of 72 hours. +* To retrieve a new sample, you must trigger an event in Salesforce or provide a sample manually. -#### Output Metadata +### Deprecated Triggers -* **event** - (object, required): Store `replayId` of this message which can be used to retrieve records that were created after (using it as `Start from Replay Id` in configuration) -* **payload** - (object, required): Dynamically generated content of the event - -#### Limitations: -* The component starts tracking changes after the first execution (it means you have to "run-now" flow with this trigger or wait for the first execution by the scheduler to establish a connection and only after this new event will be listened) -* If you use **"Ordinary"** flow: - * Make sure that you execute it at least once per 3 days - according to the [documentation](https://developer.salesforce.com/docs/platform/pub-sub-api/references/methods/subscribe-rpc.html#replaying-an-event-stream) Salesforce stores events for up to 3 days. -* To `Retrieve new sample from Salesforce v2` you need to trigger an event on Salesforce side or provide a sample manually - -### Deprecated triggers - -
- Get New and Updated Objects Polling - -### Get New and Updated Objects Polling -Polls existing and updated objects. You can select any custom or built-in object for your Salesforce instance. - -#### Input field description -* **Object** - Input field where you should select the type of object which updates you want to get. E.g. `Account`; -* **Start Time** - Indicates the beginning time to start polling from. Defaults to `1970-01-01T00:00:00.000Z`; -* **End Time** - If provided, don’t fetch records modified after this time; -* **Size of Polling Page** - Indicates the size of pages to be fetched. You can set positive integer, max `10 000`, defaults to `1000`; -* **Process single page per execution** - You can select on of options (defaults to `yes`): - 1. `yes` - if the number of changed records exceeds the maximum number of results in a page, wait until the next flow start to fetch the next page; - 2. `no` - if the number of changed records exceeds the maximum number of results in a page, the next pages will fetching in the same execution. -* **Include linked objects** - Multiselect dropdown list with all the related child and parent objects of the selected object type. List entries are given as `Object Name/Reference To (Relationship Name)`. Select one or more related objects, which will be join queried and included in the response from your Salesforce Organization. Please see the **Limitations** section below for use case advisories. -* **Output method** - dropdown list with options: `Emit all` - all found records will be emitted in one array `records`, and `Emit individually` - each found object will be emitted individual. Optional field, defaults to: `Emit individually`. -* **Max Fetch Count** - limit for a number of messages that can be fetched. 1,000 is the default value when the variable is not set. - -For example, you have 234 “Contact” objects, 213 of them were changed from 2019-01-01. -You want to select all “Contacts” that were changed from 2019-01-01, set the page size to 100 and process single page per execution. -For you purpose you need to specify following fields: - * Object: `Contact` - * Start Time: `2019-01-01T00:00:00.000Z` - * Size of Polling Page: `100` - * Process single page per execution: `yes` (or leave this empty) - -![image](https://user-images.githubusercontent.com/16806832/93762053-8ab84180-fc17-11ea-92da-0fb9669b44f9.png) - -As a result, all contacts will be fetched in three calls of the trigger: two of them by 100 items, and the last one by 13. -If you select `no` in **Process single page per execution**, all 213 contacts will be fetched in one call of the trigger. +
+ Get New and Updated Objects Polling (Legacy) -#### Limitations -When a binary field (primitive type `base64`, e.g. Documents, Attachments, etc) is selected on **Include linked objects**, an error will be thrown: 'MALFORMED_QUERY: Binary fields cannot be selected in join queries. Instead of querying objects with binary fields as linked objects (such as children Attachments), try querying them directly.' There is also a limit to the number of linked objects that you can query at once - beyond two or three, depending on the number of fields in the linked objects, Salesforce could potentially return a Status Code 431 or 414 error, meaning the query is too long. Finally, due to a bug with multiselect dropdowns, it is recommended to deselect all of the elements in this field before you change your selection in the *Object* dropdown list. +This trigger is deprecated and will be removed in a future version. Please use the current **Get Updated Objects Polling** trigger.
## Actions + ### Bulk Create/Update/Delete/Upsert -Bulk API provides a simple interface for quickly loading large amounts of data from CSV file into Salesforce (up to 10'000 records). -Action takes a CSV file from the attachment as an input. CSV file format is described in the [Salesforce documentatio](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles.htm) +Uses the Bulk API 2.0 to quickly load large amounts of data (up to 10,000 records) from a CSV file into Salesforce. This action takes a CSV file from an attachment as input. The required CSV format is described in the [Salesforce documentation](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles.htm). -#### List of Expected Config fields -* **Operation** - dropdown list with 4 supported operations: `Create`, `Update`, `Upsert` and `Delete`. -* **Object** - dropdown list where you should choose the object type to perform bulk operation. E.g. `Case`. -* **Timeout** - maximum time to wait until the server completes a bulk operation (default: `600` sec). +#### Configuration Fields +* **Operation** (dropdown, required): The bulk operation to perform: `Create`, `Update`, `Upsert`, or `Delete`. +* **Object** (dropdown, required): The type of object to perform the bulk operation on (e.g., `Case`). +* **Timeout** (integer, optional): The maximum time in seconds to wait for the server to complete the bulk operation. Defaults to `600`. -#### Expected input metadata -* **External ID Field** - a name of the External ID field for `Upsert` operation. E.g. `my_external_id__c` +#### Input Metadata +* **External ID Field**: For the `Upsert` operation, specify the name of the External ID field (e.g., `my_external_id__c`). -#### Expected output metadata -Result is an object with a property **result**: `array`. It contains objects with 3 fields. -* **id** - `string`, salesforce object id -* **success** - `boolean`, if operation was successful `true` -* **errors** - `array`, if operation failed contains description of errors +#### Output Metadata +The action outputs a message with a `result` property, which is an array of objects. Each object in the array represents the outcome for a record and contains the following fields: +* **id**: The Salesforce ID of the object. +* **success**: A boolean indicating if the operation was successful (`true` or `false`). +* **errors**: An array containing error descriptions if the operation failed. #### Limitations -* No errors thrown in case of failed Object Create/Update/Delete/Upsert (`"success": "false"`). -* Object ID is needed for Update and Delete. -* External ID is needed for Upsert. -* Salesforce processes up to 10'000 records from the input CSV file. +* The action does not throw an error for failed records. You must check the `success` field in the output to identify failures. +* An `Object ID` is required for `Update` and `Delete` operations. +* An `External ID` is required for the `Upsert` operation. +* Salesforce processes a maximum of 10,000 records from the input CSV file per operation. ### Bulk Query -Fetches records to a CSV file. - -#### Expected input metadata - -* **SOQL Query** - Input field where you should type the SOQL query. E.g. `"SELECT ID, Name from Contact where Name like 'John Smi%'"` +Fetches a large number of records using a SOQL query and streams the result as a CSV file in an attachment. -Result is a CSV file in the attachment. +#### Input Metadata +* **SOQL Query** (string, required): The SOQL query to execute (e.g., `SELECT Id, Name from Contact`). ### Create Object -Creates a new Selected Object. -Action creates a single object. - -Note: -In case of an **Attachment** object type you should specify `Body` in base64 encoding. -`ParentId` is a Salesforce ID of an object (Account, Lead, Contact) which an attachment is going to be attached to. +Creates a single new object in Salesforce. -#### List of Expected Config fields -* **Object** - Input field where you should choose the object type, which you want to find. E.g. `Account` -* **Utilize data attachment from previous step (for objects with a binary field)** - a checkbox, if it is checked and an input message contains an attachment and specified object has a binary field (type of base64) then the input data is put into object's binary field. In this case any data specified for the binary field in the data mapper is discarded. +**Note:** To create an `Attachment`, you must provide the file content as a base64-encoded string in the `Body` field. The `ParentId` must be the Salesforce ID of the object (e.g., Account, Lead) the attachment will be associated with. -This action will automatically retrieve all existing fields of chosen object type that available on your Salesforce organization - -#### Expected input metadata -Input metadata is fetched dynamically from your Salesforce account. +#### Configuration Fields +* **Object** (dropdown, required): The type of object to create (e.g., `Account`). +* **Utilize data attachment from previous step...**: If checked, and if an attachment is present in the input message, the component will use the attachment data for any binary field (e.g., the `Body` of a `Document`). -#### Expected output metadata -Output metadata is the same as input metadata, so you may expect all fields that you mapped as input to be returned as output. +#### Input/Output Metadata +This action dynamically retrieves all available fields for the chosen object type. The output metadata will mirror the input metadata. #### Limitations -When **Utilize data attachment from previous step (for objects with a binary field)** is checked and this action is used with Local Agent error would be thrown: 'getaddrinfo ENOTFOUND steward-service.platform.svc.cluster.local steward-service.platform.svc.cluster.local:8200' +* When **Utilize data attachment...** is checked, this action may fail if used with a Local Agent due to networking constraints. ### Delete Object (at most 1) -Deletes an object by a selected field. One can filter by either unique fields or all fields of that sobject. +Deletes a single object found by a specified field and value. -#### List of Expected Config fields -* **Object** - dropdown list where you should choose the object type, which you want to find. E.g. `Account`. -* **Type Of Search** - dropdown list with two values: `Unique Fields` and `All Fields`. -* **Lookup by field** - dropdown list with all fields on the selected object, if on *Type Of Search* is chosen `All Fields`, or with all fields on the selected object where `type` is `id` or `unique` is `true` , if on *Type Of Search* is chosen `Unique Fields` then all searchable fields both custom and standard will be available for selection. +#### Configuration Fields +* **Object** (dropdown, required): The type of object to delete. +* **Type Of Search** (dropdown, required): Choose to look up the object by `Unique Fields` or `All Fields`. +* **Lookup by field** (dropdown, required): The field to use for the lookup. -#### Expected input metadata -Input metadata is fetched dynamically from your Salesforce account and depends on field `Lookup by field`. +#### Input Metadata +The input metadata is dynamically generated based on the **Lookup by field**. -#### Expected output metadata -Result is an object with 3 fields. -* **id** - `string`, salesforce object id -* **success** - `boolean`, if operation was successful `true` -* **errors** - `array`, if operation fails, it will contain description of errors +#### Output Metadata +* **id**: The Salesforce ID of the deleted object. +* **success**: A boolean indicating if the operation was successful. +* **errors**: An array of errors if the operation failed. ### Lookup Object (at most 1) -Lookup an object by a selected field. -Action creates a single object. - -#### List of Expected Config fields -* **Object** - Dropdown list displaying all searchable object types. Select one type to query, e.g. `Account`. -* **Type Of Search** - Dropdown list with two values: `Unique Fields` and `All Fields`. -* **Lookup by field** - Dropdown list with all fields on the selected object if the *Type Of Search* is `All Fields`. If the *Type Of Search* is `Unique Fields`, the dropdown lists instead all fields on the selected object where `type` is `id` or `unique` is `true`. -* **Include referenced objects** - Multiselect dropdown list with all the related child and parent objects of the selected object type. List entries are given as `Object Name/Reference To (Relationship Name)`. Select one or more related objects, which will be join queried and included in the response from your Salesforce Organization. Please see the **Limitations** section below for use case advisories. -* **Allow criteria to be omitted** - Checkbox. If checked and nothing is specified in criteria, an empty object will be returned. If not checked and nothing is found, the action will throw an error. -* **Allow zero results** - Checkbox. If checked and nothing is found in your Salesforce Organization, an empty object will be returned. If not checked and nothing is found, the action will throw an error. -* **Pass binary data to the next component (if found object has it)** - Checkbox. If it is checked and the found object record has a binary field (primitive type `base64`), then its data will be passed to the next component as a binary attachment and link to it will be replaced to link on the platform -* **Enable Cache Usage** - Flag to enable cache usage. - -#### Expected input metadata -Input metadata is fetched dynamically from your Salesforce account. -Metadata contains one field whose name, type and mandatoriness are generated according to the value of the configuration fields *Lookup by field* and *Allow criteria to be omitted*. - -#### Expected output metadata -Output metadata is the same as input metadata, so you may expect all fields that you mapped as input to be returned as output. +Looks up a single object by a specified field and value. -#### Limitations -1. When a binary field (primitive type `base64`, e.g. Documents, Attachments, etc) is selected on **Include linked objects**, an error will be thrown: `MALFORMED_QUERY: Binary fields cannot be selected in join queries. Instead of querying objects with binary fields as linked objects (such as children Attachments), try querying them directly.` -There is also a limit to the number of linked objects that you can query at once - beyond two or three, depending on the number of fields in the linked objects, Salesforce could potentially return a Status Code 431 or 414 error, meaning the query is too long. Finally, due to a bug with multiselect dropdowns, it is recommended to deselect all of the elements in this field before you change your selection in the *Object* dropdown list. - -2. Not supported object lookup when selected field value is `null` -3. When **Pass binary data to the next component (if found object has it)** is checked and this action is used with Local Agent, an error will be thrown: 'getaddrinfo ENOTFOUND steward-service.platform.svc.cluster.local steward-service.platform.svc.cluster.local:8200' +#### Configuration Fields +* **Object** (dropdown, required): The type of object to look up. +* **Type Of Search** (dropdown, required): Choose to look up the object by `Unique Fields` or `All Fields`. +* **Lookup by field** (dropdown, required): The field to use for the lookup. +* **Include referenced objects** (multiselect, optional): A list of related objects to include in the result. +* **Allow criteria to be omitted** (checkbox, optional): If checked, an empty object will be returned if the lookup criteria is missing from the input. +* **Allow zero results** (checkbox, optional): If checked, an empty object will be returned if no matching record is found. +* **Pass binary data to the next component...**: If checked, and if the found object contains a binary field, its data will be passed as an attachment. +* **Enable Cache Usage** (checkbox, optional): Enables caching for this action. -#### Note -Action has caching mechanism. By default action stores last 10 request-response pairs for 10 min duration. -This parameters can be changed by setting environment variables: -* **HASH_LIMIT_TIME** - Hash expiration time in milis -* **HASH_LIMIT_ELEMENTS** - Hash size number limit +#### Limitations +1. Selecting a binary field (e.g., in Documents or Attachments) under **Include referenced objects** will cause a `MALFORMED_QUERY` error. +2. The action does not support looking up objects where the lookup field value is `null`. +3. Passing binary data as an attachment may fail if used with a Local Agent. ### Lookup Objects -Lookup a list of objects satisfying specified criteria. - -#### List of Expected Config fields -* **Object** - dropdown list where you should choose the object type, which you want to find. E.g. `Account`. -* **Include deleted** - checkbox, if checked - deleted records will be included into the result list. -* **Output method** - dropdown list with following values: "Emit all", "Emit page", "Emit individually". -* **Number of search terms** - text field to specify a number of search terms (positive integer number [1-99] or 0). -* **Enable Cache Usage** - Flag to enable cache usage. -* **Max Fetch Count** - limit for a number of messages that can be fetched. 1,000 is the default value when the variable is not set. - -#### Expected input metadata -Depending on the the configuration field *Output method* the input metadata can contain different fields: -*Output method* - "Emit page": -Field "Page size" - optional positive integer that defaults to 1000; -Field "Page number" - required non-negative integer (starts with 0, default value 0); - -*Output method* - "Emit all": -Field "Maximum number of records" - optional positive integer (default value 1000); +Looks up a list of objects that satisfy the specified criteria. -*Output method* - "Emit individually": -Field "Maximum number of records" - optional positive integer (default value 10000); - -Note that the number of records the component emits may affect the performance of the platform/component. - -Groups of fields for each search term go next: - -Field "Field name" - string represents object's field (a list of allowed values is available); -Field "Field value" - string represents value for selected field; -Field "Condition" - one of the following: "=", "!=", "<", "<=", ">", ">=", "LIKE", "IN", "NOT IN"; - -Between each two term's group of fields: - -Field "Logical operator" - one of the following: "AND", "OR"; - -#### Expected output metadata -Output data is an object, with a field "results" that is an array of objects. +#### Configuration Fields +* **Object** (dropdown, required): The type of object to look up. +* **Include deleted** (checkbox, optional): If checked, deleted records will be included in the results. +* **Output method** (dropdown, required): Choose to `Emit all`, `Emit page`, or `Emit individually`. +* **Number of search terms** (integer, required): The number of filter conditions to apply (0-99). +* **Enable Cache Usage** (checkbox, optional): Enables caching for this action. +* **Max Fetch Count** (integer, optional): The maximum number of records to fetch. Defaults to `1000`. -#### Note -Action has caching mechanism. By default action stores last 10 request-response pairs for 10 min duration. -This parameters can be changed by setting environment variables: -* **HASH_LIMIT_TIME** - Hash expiration time in milis -* **HASH_LIMIT_ELEMENTS** - Hash size number limit +#### Input Metadata +The input metadata changes based on the **Output method** and the **Number of search terms**. -#### Known limitations -If `Output method` set to `Emit page` maximum "Page number" must be less or equal to "Page size"/2000 +#### Output Metadata +An object with a `results` property, which contains an array of found objects. ### Query Action -Executing a SOQL Query that may return many objects. -Use the Salesforce Object Query Language (SOQL) to search your organization’s Salesforce data for specific information. -SOQL is similar to the SELECT statement in the widely used Structured Query Language (SQL) but is designed specifically for Salesforce data. -This action allows you to interact with your data using SOQL. -Empty object will be returned, if query doesn't find any data. +Executes a SOQL query. -#### List of Expected Config fields -* **Optional batch size** - A positive integer specifying batch size. If no batch size is specified then results of the query will be emitted one-by-one, otherwise, query results will be emitted in an array of maximum batch size. -* **Allow all results to be returned in a set** - checkbox which allows emitting query results in a single array. `Optional batch size` and `Max Fetch Count` options are ignored in this case. -* **Include deleted** - checkbox, if checked - deleted records will be included into the result list. -* **Max Fetch Count** - limit for a number of messages that can be fetched. 1,000 is the default value when the variable is not set. +#### Configuration Fields +* **Optional batch size** (integer, optional): If specified, results will be emitted in arrays of this size. If empty, results are emitted one-by-one. +* **Allow all results to be returned in a set** (checkbox, optional): If checked, all results are returned in a single array, ignoring batch size. +* **Include deleted** (checkbox, optional): If checked, deleted records will be included in the results. +* **Max Fetch Count** (integer, optional): The maximum number of records to fetch. Defaults to `1000`. -#### Expected input metadata -* **SOQL Query** - Input field where you should type the SOQL query. E.g. `"SELECT ID, Name from Contact where Name like 'John Smi%'"` +#### Input Metadata +* **SOQL Query** (string, required): The SOQL query to execute. ### Raw Request +Executes a custom REST API call to a Salesforce endpoint. -This function executes a custom REST API call request. By default, the service called is `/services/data`, which encompasses most of the services provided by Salesforce. -Alternatively, you can call any other services, such as `/services/apexrest`, by specifying the full URL instead of a relative one. - -#### Input Metadata -* HTTP Verb - Allowed values GET, POST, PUT, PATCH, DELETE, HEAD, Required. HTTP verb to use in the request. - * To call services based on the `/services/data` endpoint, you can utilize a relative path, for instance, `query/?q=SELECT+Id,Name+FROM+Account`. This automatically constructs the URL as follows: `https://{your_instance}.salesforce.com/services/data/{SALESFORCE_API_VERSION}/query/?q=SELECT+Id,Name+FROM+Account` - * For calling other services like `/services/apexrest`, provide **the full URL**, such as `https://{your_instance}.salesforce.com/services/apexrest/myApexClass` -* Path - String, Required. Use a relative path to make a request (for a list of all types of objects - `sobjects`, e.g., to list the type of objects Account - `sobjects/account`). Since Salesforce sends the endpoint that must be called dynamically, there is no need to enter the base URL like this - `https://{INSTANCE_NAME}.salesforce.com/services/data/v{SALESFORCE_API_VERSION}/sobjects/{SALESFORCE_OBJECT}`. Instead, you should use a relative path - `sobjects/{SALESFORCE_OBJECT}`. -* Request Body - Object, Optional. Body to attach to the HTTP Request +#### Configuration Fields +* **HTTP Verb** (dropdown, required): The HTTP method to use (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`). +* **Path** (string, required): The URL path for the request. + * For standard REST API calls, use a relative path (e.g., `query/?q=SELECT+Id,Name+FROM+Account`). + * For other services like Apex REST, provide the full URL. +* **Request Body** (object, optional): The body to attach to the request. #### Output Metadata -* Response Object (Object, optional): HTTP response body +* **Response Object**: The HTTP response body from the API call. -#### Resources List -* More information about available resources you can find [here](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_list.htm) +### Upsert Object +Creates a new object or updates an existing one. -#### Request Examples -* Examples of using REST API resources can be found [here](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_user_tasks.htm) +#### Configuration Fields +* **Object** (dropdown, required): The type of object to upsert. +* **Type Of Search** (dropdown, required): The type of field to use for the lookup. + * `External IDs`: Uses Salesforce's native, high-performance upsert functionality based on an External ID field. + * `Unique Fields` or `All Fields`: The component will first look up an object. If one is found, it is updated. If none are found, a new one is created. If multiple are found, an error is thrown. +* **Lookup by field** (dropdown, required): The field to use for the lookup, based on the **Type Of Search**. -#### Known limitations -For the methods PUT and HEAD you need to specify the whole path (e.g. `services/OpportunityLineItem/00kR0000001WJJAIA4/OpportunityLineItemSchedules`) which have conflicts with `/services/data/v{SALESFORCE_API_VERSION}/{RESOURCE}` path, so Raw Request does not work for these two methods (PUT and HEAD) just for now. +#### Input Metadata +Dynamically generated based on the selected object and lookup field. -### Upsert Object -Creates or Updates Selected Object. -Action creates a single object. - -#### List of Expected Config fields -* **Object** - Input field where you should choose the object type, which you want to find. E.g. `Account` -* **Type Of Search** - Dropdown list with values: `Unique Fields`, `All Fields` and `External IDs` - * `All Fields` - all available fields in the object - * `Unique Fields` - fields where `type` is `id` or `unique` is `true` - * `External IDs` - fields where `externalId` is `true`, this option uses built-in salesforce method [upsert](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_upsert.htm). - - It works as following: - * If there is no value in the lookup field - a new object will be created - * If a lookup value is specified and `External IDs` selected as a Type Of Search - it is the most efficient (fast) way to go. In this case an object will be upserted directly on the Salesforce side. When this field has an attribute `Unique` it would guarantee that no errors are emitted. - * If a lookup value is specified and one of `Unique Fields` or `All Fields` selected - then an action will first lookup for an existing object in Salesforce: - * If no objects found - a new one will be created - * If 1 object found - it will be updated - * If more than 1 object found - ar error `Found more than 1 Object` will be thrown -* **Lookup by field** - Dropdown list with fields on the selected object, depending on the *Type Of Search* - -#### Expected input metadata -* lookup by - *name of filed selected in 'Lookup by field'* -* other fields, that used by selected **Object** - -#### Expected output metadata -The result of creating or updating an object -* **id** - Unic identificator from salesforce -* **success** - Boolean result of creation/update object -* **errors** - Arrey of errors if they exist - -#### Known limitations -If you add a new field to an object in Salesforce, you must restart the flow to re-generate metadata +#### Output Metadata +* **id**: The unique Salesforce identifier of the created or updated object. +* **success**: A boolean indicating the result of the operation. +* **errors**: An array of errors if the operation failed. ## Permissions -By default, certain user profiles in Salesforce have disabled permissions. In order to ensure the visibility of an object in the metadata of component’s actions and triggers, it is necessary to enable the required standard object permissions. +By default, certain user profiles in Salesforce have disabled permissions. To ensure an object is visible in the component's dropdowns, you may need to enable its standard object permissions. -To enable these permissions, please follow these steps: +To enable these permissions: -1. Go to the Salesforce Setup page. -2. Navigate to the “ADMINISTRATION” section. -3. Under “Profiles”, select the profile that needs modification. -4. Click on the “Edit” button to proceed. +1. Go to the Salesforce **Setup** page. +2. Navigate to **Administration > Users > Profiles**. +3. Select the profile that needs modification and click **Edit**. +4. Under **Standard Object Permissions**, ensure the necessary permissions are enabled. For example, to use the **Get Updated Objects Polling** trigger, the `Read`, `Create`, and `Edit` permissions are typically required for the object. -
- Salesforse setup page -![image](https://github.com/elasticio/salesforce-component-v2/assets/108279772/ad2b7d68-c843-4356-92b3-7650bab6a3f2) +
+ Salesforce Setup Page + Salesforce Setup Page
-Once you are on the profile editing page, ensure that all the required standard object permissions are enabled. For instance, if you intend to utilize the [Get New and Updated Objects Polling trigger](https://github.com/elasticio/salesforce-component-v2#get-updated-objects-polling), the following permissions are necessary: Read, Create, and Edit. - -
- Standart objects permissions -![image](https://github.com/elasticio/salesforce-component-v2/assets/108279772/f80445e4-098a-42cf-a728-6a204b8e7329) +
+ Standard Object Permissions + Standard Object Permissions
-Carefully review the permissions and make any necessary adjustments to enable the required access. +Carefully review and adjust the permissions to enable the required access. -## Known limitations -Attachments mechanism does not work with [Local Agent Installation](https://docs.elastic.io/getting-started/local-agent.html) +## Known Limitations +* The attachment mechanism does not work with a Local Agent installation. diff --git a/component.json b/component.json index 3acb052..84c90a1 100644 --- a/component.json +++ b/component.json @@ -3,7 +3,7 @@ "description": "Customer relationship management (CRM) software & cloud computing from the leader in CRM solutions for businesses large & small.", "docsUrl": "https://github.com/elasticio/salesforce-component-v2", "url": "https://www.salesforce.com/", - "version": "2.8.6", + "version": "2.9.0", "authClientTypes": [ "oauth2" ], diff --git a/lib/salesForceClient.js b/lib/salesForceClient.js index d7f4531..b881960 100644 --- a/lib/salesForceClient.js +++ b/lib/salesForceClient.js @@ -67,6 +67,10 @@ class SalesForceClient { return this.getSObjectList('event sobject', (object) => object.name.endsWith('__e')); } + async simpleQuery(query) { + return this.connection.query(query); + } + async queryEmitAll(query) { const result = []; await new Promise((resolve, reject) => { @@ -214,7 +218,7 @@ class SalesForceClient { async pollingSelectQuery(options) { const sobject = options.sobject || this.configuration.sobject; const { - selectedObjects, linkedObjects, whereCondition, maxFetch, + selectedObjects, linkedObjects, whereCondition, maxFetch, orderBy, } = options; let query = this.connection.sobject(sobject) .select(selectedObjects); @@ -229,7 +233,7 @@ class SalesForceClient { return newQuery; }, query); return query.where(whereCondition) - .sort({ LastModifiedDate: 1 }) + .sort(orderBy || { LastModifiedDate: 1 }) .execute({ autoFetch: true, maxFetch }); } diff --git a/lib/triggers/getUpdatedObjectsPolling.js b/lib/triggers/getUpdatedObjectsPolling.js index 915adbe..354a07a 100644 --- a/lib/triggers/getUpdatedObjectsPolling.js +++ b/lib/triggers/getUpdatedObjectsPolling.js @@ -18,13 +18,45 @@ function getSelectedFields(cfg) { if (!selectedFields.includes('LastModifiedDate')) { selectedFields.push('LastModifiedDate'); } + if (!selectedFields.includes('Id')) { + selectedFields.push('Id'); + } return selectedFields.toString(); } -exports.process = async function processTrigger(_msg, cfg, snapshot) { +function buildWhereClause(from, to, lastModificationTime, lastSeenId) { + const whereParts = []; + if (lastModificationTime) { + const lastDate = timeToString(lastModificationTime); + if (lastSeenId) { + whereParts.push(`(LastModifiedDate = ${lastDate} AND Id > '${lastSeenId}')`); + whereParts.push(`(LastModifiedDate > ${lastDate})`); + } else { + whereParts.push(`LastModifiedDate >= ${lastDate}`); + } + } else { + whereParts.push(`LastModifiedDate >= ${timeToString(from)}`); + } + whereParts.push(`LastModifiedDate < ${timeToString(to)}`); + + if (whereParts.length > 2) { + return `((${whereParts.slice(0, 2).join(' OR ')}) AND ${whereParts[2]})`; + } + return whereParts.join(' AND '); +} + +exports.process = async function processTrigger(_msg, cfg, snapshot = {}) { this.logger.info('Start processing "Get Updated Objects Polling" trigger'); const currentTime = new Date(); + // eslint-disable-next-line prefer-const + let { lastModificationTime, lastSeenId } = snapshot; + + if (snapshot.nextStartTime) { + this.logger.warn('Found a snapshot with the old format (nextStartTime), converting to the new format.'); + lastModificationTime = snapshot.nextStartTime; + } + const { sobject, linkedObjects = [], @@ -35,16 +67,16 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) { } = cfg; let { startTime, endTime, pageSize } = cfg; - if (!pageSize) pageSize = MAX_FETCH; - if (!startTime) startTime = 0; + if (pageSize === null || pageSize === undefined) pageSize = MAX_FETCH; + if (!startTime) startTime = '1970-01-01T00:00:00.000Z'; if (!endTime) endTime = currentTime; if (!isDateValid(startTime)) throw new Error('invalid "Start Time" date format, use ISO 8601 Date time utc format - YYYY-MM-DDThh:mm:ssZ'); if (!isDateValid(endTime)) throw new Error('invalid "End Time" date format, use ISO 8601 Date time utc format - YYYY-MM-DDThh:mm:ssZ'); - if (pageSize > MAX_FETCH || isNumberNaN(pageSize) || Number(pageSize) < 0) throw new Error(`"Size of Polling Page" must be valid number between 0 and ${MAX_FETCH}`); + if (pageSize > MAX_FETCH || isNumberNaN(pageSize) || Number(pageSize) <= 0) throw new Error(`"Size of Polling Page" must be valid number between 1 and ${MAX_FETCH}`); pageSize = Number(pageSize); - let from = snapshot?.nextStartTime || startTime; + + const from = lastModificationTime || startTime; const to = endTime || currentTime; - let nextStartTime = currentTime; if (timestamp(from) > timestamp(to)) { this.logger.info('"Start Time" is higher than "End Time", finishing trigger process'); @@ -54,12 +86,18 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) { this.logger.info(`Filter "${sobject}" updated from ${timeToString(from)} to ${timeToString(to)}, page limit ${pageSize}`); + const preliminaryWhere = buildWhereClause(from, to, lastModificationTime, lastSeenId); + const countQuery = `SELECT COUNT() FROM ${sobject} WHERE ${preliminaryWhere}`; + const countResult = await callJSForceMethod.call(this, cfg, 'simpleQuery', countQuery); + this.logger.info(`Found ${countResult.totalSize} objects to process for this run.`); + const selectedFields = getSelectedFields(cfg); const options = { sobject, selectedObjects: linkedObjects.reduce((query, obj) => (obj.startsWith('!') ? query : `${query}, ${obj}.*`), selectedFields), linkedObjects, maxFetch: pageSize, + orderBy: 'LastModifiedDate, Id', }; if (isDebugFlow) { @@ -69,27 +107,27 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) { } let proceed = true; - let emitted; + let emitted = false; let iteration = 1; let results; + let nextLastModificationTime = lastModificationTime; + let nextLastSeenId = lastSeenId; + try { do { - options.whereCondition = `LastModifiedDate >= ${timeToString(from)} AND LastModifiedDate < ${timeToString(to)}`; + options.whereCondition = buildWhereClause(from, to, nextLastModificationTime, nextLastSeenId); + this.logger.debug('Start poll object with options: %j', options); results = await callJSForceMethod.call(this, cfg, 'pollingSelectQuery', options); this.logger.info(`Polling iteration ${iteration} - ${results.length} results found`); iteration++; - if (results.length !== 0) { + + if (results.length > 0) { emitted = true; - nextStartTime = results[results.length - 1].LastModifiedDate; - if (results.length === pageSize) { - this.logger.warn('All entries that have the same LastModifiedDate as the last entry will be deleted from the resulting array to prevent emitting duplicates'); - results = results.filter((item) => item.LastModifiedDate !== nextStartTime); - this.logger.warn('Entries that have the same LastModifiedDate as the last entry deleted. Current size of the resulting array is %s', results.length); - } else { - nextStartTime = timeToString(timestamp(nextStartTime) + 1000); - proceed = false; - } + const lastRecord = results[results.length - 1]; + nextLastModificationTime = lastRecord.LastModifiedDate; + nextLastSeenId = lastRecord.Id; + if (emitBehavior === 'fetchPage') { this.logger.debug('Emit Behavior set as Fetch Page, going to emit one page...'); await this.emit('data', messages.newMessageWithBody({ results })); @@ -99,15 +137,23 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) { await this.emit('data', messages.newMessageWithBody(record)); } } - if (singlePagePerInterval) proceed = false; - from = nextStartTime; + + if (results.length < pageSize) { + proceed = false; + } + + if (singlePagePerInterval) { + proceed = false; + } } else { proceed = false; } } while (proceed); + this.logger.info('Processing Polling trigger finished successfully'); - this.logger.debug('Going to emit snapshot: %j', { nextStartTime }); - await this.emit('snapshot', { nextStartTime }); + const newSnapshot = { lastModificationTime: nextLastModificationTime || from, lastSeenId: nextLastSeenId }; + this.logger.debug('Going to emit snapshot: %j', newSnapshot); + await this.emit('snapshot', newSnapshot); } catch (e) { if (e.statusCode) { throw new Error(`Got error - ${e.name}\n message: \n${e.message}\n statusCode: \n${e.statusCode}\n body: \n${JSON.stringify(e.body)}`); @@ -116,9 +162,7 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) { } if (isDebugFlow && !emitted) { - throw new Error(`No object found. Execution stopped. - This error is only applicable to the Retrieve Sample. - In flow executions there will be no error, just an execution skip.`); + throw new Error('No object found. Execution stopped.\n This error is only applicable to the Retrieve Sample.\n In flow executions there will be no error, just an execution skip.'); } }; diff --git a/package-lock.json b/package-lock.json index 3daa691..06498de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,32 +38,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.14.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", @@ -318,19 +306,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -345,52 +335,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.8.tgz", - "integrity": "sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/types": "^7.28.4" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -399,41 +365,40 @@ } }, "node_modules/@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, + "license": "MIT", "peer": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.18.9.tgz", - "integrity": "sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "core-js-pure": "^3.20.2", - "regenerator-runtime": "^0.13.4" + "core-js-pure": "^3.43.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -493,14 +458,14 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -639,13 +604,15 @@ "license": "MIT" }, "node_modules/@elastic.io/component-commons-library/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -788,13 +755,15 @@ } }, "node_modules/@elastic.io/maester-client/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1816,10 +1785,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "devOptional": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1939,6 +1909,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2262,11 +2245,12 @@ "dev": true }, "node_modules/core-js-pure": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.24.1.tgz", - "integrity": "sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -2284,10 +2268,11 @@ "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2478,6 +2463,20 @@ "node": ">=0.10" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2662,6 +2661,51 @@ "node": ">= 0.4" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -2703,15 +2747,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz", @@ -3578,10 +3613,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -3643,14 +3681,24 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3665,6 +3713,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3744,6 +3805,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -3792,15 +3865,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -3814,10 +3878,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3826,12 +3890,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3856,6 +3920,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -4912,6 +4988,15 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", @@ -5140,10 +5225,11 @@ } }, "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6009,6 +6095,13 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -6249,13 +6342,6 @@ "node": ">=8.10.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true, - "peer": true - }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -6342,6 +6428,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", "integrity": "sha512-paa/JFJUwUCx5ksokBlaGIXAvIDB+izsRU6FpHrlezFU2fj8555sKN4r+wPyql5d5Bp1ya/vrUPfVqM51v2H0g==", + "license": "ISC", "dependencies": { "lodash": "^4.13.1" }, @@ -6357,6 +6444,7 @@ "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", "integrity": "sha512-Y75hrP+fdfWTg8R9rGNFmLi2JsZ3LlmHa+HcljiXY88NU86TaChwfkhsPN+pdojFluO2Qr0Jb+lV/aCkYpeAyw==", "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "license": "ISC", "dependencies": { "request-promise-core": "1.1.1", "stealthy-require": "^1.1.0", @@ -6747,7 +6835,8 @@ "node_modules/stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", "engines": { "node": ">=0.10.0" } @@ -6927,18 +7016,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7016,15 +7093,6 @@ "readable-stream": "3" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7556,26 +7624,14 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, "@babel/compat-data": { @@ -7767,15 +7823,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true }, "@babel/helper-validator-option": { @@ -7785,76 +7841,50 @@ "dev": true }, "@babel/helpers": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.8.tgz", - "integrity": "sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "requires": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" } }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "@babel/types": "^7.28.4" } }, - "@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true - }, "@babel/runtime": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", - "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, - "peer": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } + "peer": true }, "@babel/runtime-corejs3": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.18.9.tgz", - "integrity": "sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", "dev": true, "peer": true, "requires": { - "core-js-pure": "^3.20.2", - "regenerator-runtime": "^0.13.4" + "core-js-pure": "^3.43.0" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { @@ -7899,14 +7929,13 @@ } }, "@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@elastic.io/bunyan-logger": { @@ -8006,12 +8035,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -8119,12 +8150,14 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } } @@ -8948,9 +8981,9 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "devOptional": true, "requires": { "balanced-match": "^1.0.0", @@ -9043,6 +9076,15 @@ "get-intrinsic": "^1.0.2" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -9295,9 +9337,9 @@ } }, "core-js-pure": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.24.1.tgz", - "integrity": "sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", "dev": true, "peer": true }, @@ -9312,9 +9354,9 @@ "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -9461,6 +9503,16 @@ "nan": "^2.14.0" } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -9617,6 +9669,35 @@ } } }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -9649,12 +9730,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "eslint": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz", @@ -10296,10 +10371,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -10343,14 +10417,20 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -10359,6 +10439,15 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -10413,6 +10502,11 @@ "type-fest": "^0.8.1" } }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -10448,12 +10542,6 @@ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -10464,18 +10552,16 @@ } }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" } }, "hasha": { @@ -10488,6 +10574,14 @@ "type-fest": "^0.8.0" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -11287,6 +11381,11 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", @@ -11452,9 +11551,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -12123,6 +12222,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -12304,13 +12409,6 @@ "picomatch": "^2.2.1" } }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true, - "peer": true - }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -12683,7 +12781,7 @@ "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==" }, "stream-counter": { "version": "1.0.0", @@ -12812,15 +12910,6 @@ "peek-readable": "^4.1.0" } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -12881,12 +12970,6 @@ "readable-stream": "3" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/spec/triggers/getUpdatedObjectsPolling.spec.js b/spec/triggers/getUpdatedObjectsPolling.spec.js index a49cba3..69a3077 100644 --- a/spec/triggers/getUpdatedObjectsPolling.spec.js +++ b/spec/triggers/getUpdatedObjectsPolling.spec.js @@ -8,160 +8,198 @@ const duplicateRecords = require('../testData/trigger.results.1612.json'); describe('getUpdatedObjectsPolling trigger', () => { let execRequest; - describe('succeed', () => { - afterEach(() => { - sinon.restore(); + + beforeEach(() => { + execRequest = sinon.stub(callJSForceMethod, 'call'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should handle multiple pages of records with the same timestamp without data loss', async () => { + const sameTimestamp = '2025-10-08T10:00:00.000Z'; + const records = [ + { Id: '001a', Name: 'Record 1', LastModifiedDate: sameTimestamp }, + { Id: '001b', Name: 'Record 2', LastModifiedDate: sameTimestamp }, + { Id: '001c', Name: 'Record 3', LastModifiedDate: sameTimestamp }, + { Id: '001d', Name: 'Record 4', LastModifiedDate: sameTimestamp }, + { Id: '001e', Name: 'Record 5', LastModifiedDate: sameTimestamp }, + ]; + const whereConditions = []; + + // Mock the initial COUNT query + execRequest.onCall(0).returns({ totalSize: 5 }); + + execRequest.callsFake((...args) => { + // Skip the COUNT query call + if (execRequest.callCount > 1) { + whereConditions.push(args[3].whereCondition); + } + // Return data for polling calls + if (execRequest.callCount === 1) return { totalSize: 5 }; // COUNT call + if (execRequest.callCount === 2) return [records[0], records[1]]; + if (execRequest.callCount === 3) return [records[2], records[3]]; + if (execRequest.callCount === 4) return [records[4]]; + return []; }); - it('emitIndividually', async () => { - execRequest = sinon.stub(callJSForceMethod, 'call').returns(duplicateRecords); - const cfg = { - sobject: 'Document', - pageSize: 100, - linkedObjects: [], - emitBehavior: 'emitIndividually', - singlePagePerInterval: false, - }; - const snapshot = {}; - const msg = {}; - const context = getContext(); - await process.call(context, msg, cfg, snapshot); - expect(context.emit.callCount).to.be.equal(28); - expect(context.emit.getCall(27).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(1); - expect(context.emit.getCall(0).lastArg.body).to.deep.equal(duplicateRecords[0]); + + const cfg = { + sobject: 'Document', + pageSize: 2, + emitBehavior: 'emitIndividually', + singlePagePerInterval: false, + }; + const context = getContext(); + await process.call(context, {}, cfg, {}); + + // 1 for COUNT, 3 for data + expect(execRequest.callCount).to.be.equal(4); + // 5 data emits, 1 snapshot emit + expect(context.emit.callCount).to.be.equal(6); + + expect(whereConditions[0]).to.include('LastModifiedDate >= 1970-01-01T00:00:00.000Z'); + expect(whereConditions[1]).to.include(`(LastModifiedDate = ${sameTimestamp} AND Id > '${records[1].Id}')`); + expect(whereConditions[2]).to.include(`(LastModifiedDate = ${sameTimestamp} AND Id > '${records[3].Id}')`); + + // Check final snapshot + expect(context.emit.getCall(5).args[0]).to.equal('snapshot'); + expect(context.emit.getCall(5).args[1]).to.deep.equal({ + lastModificationTime: sameTimestamp, + lastSeenId: '001e', }); + }); + + it('should handle backward compatibility with old snapshot format', async () => { + execRequest.onCall(0).returns({ totalSize: 1 }); + execRequest.onCall(1).returns([duplicateRecords[0]]); + const cfg = { sobject: 'Document' }; + const oldSnapshot = { nextStartTime: '2022-07-01T00:00:00.000Z' }; + const context = getContext(); + + await process.call(context, {}, cfg, oldSnapshot); + + const whereClause = execRequest.getCall(1).args[3].whereCondition; + expect(whereClause).to.include('LastModifiedDate >= 2022-07-01T00:00:00.000Z'); + + expect(context.emit.lastCall.args[0]).to.equal('snapshot'); + expect(context.emit.lastCall.args[1]).to.deep.equal({ + lastModificationTime: duplicateRecords[0].LastModifiedDate, + lastSeenId: duplicateRecords[0].Id, + }); + }); + + it('should emit individually and create correct snapshot', async () => { + execRequest.onFirstCall().returns({ totalSize: duplicateRecords.length }); + execRequest.onSecondCall().returns(duplicateRecords); + + const cfg = { + sobject: 'Document', + pageSize: 100, + emitBehavior: 'emitIndividually', + }; + const context = getContext(); + await process.call(context, {}, cfg, {}); + + expect(execRequest.callCount).to.be.equal(2); + expect(context.emit.callCount).to.be.equal(duplicateRecords.length + 1); + expect(context.emit.getCall(0).args[0]).to.equal('data'); + expect(context.emit.lastCall.args[0]).to.equal('snapshot'); + expect(context.emit.lastCall.args[1]).to.deep.equal({ + lastModificationTime: duplicateRecords[duplicateRecords.length - 1].LastModifiedDate, + lastSeenId: duplicateRecords[duplicateRecords.length - 1].Id, + }); + }); + + it('should work with singlePagePerInterval', async () => { + execRequest.onCall(0).returns({ totalSize: 8 }); + execRequest.onCall(1).returns(duplicateRecords.slice(0, 5)); + execRequest.onCall(2).returns({ totalSize: 3 }); + execRequest.onCall(3).returns(duplicateRecords.slice(5, 8)); + + const cfg = { + sobject: 'Document', + pageSize: 5, + emitBehavior: 'emitIndividually', + singlePagePerInterval: true, + }; + const context = getContext(); + + // First run + await process.call(context, {}, cfg, {}); + expect(execRequest.callCount).to.be.equal(2); // 1 for count, 1 for data + expect(context.emit.callCount).to.be.equal(6); // 5 data, 1 snapshot + const snapshot = context.emit.lastCall.args[1]; + expect(snapshot).to.deep.equal({ + lastModificationTime: duplicateRecords[4].LastModifiedDate, + lastSeenId: duplicateRecords[4].Id, + }); + + // Second run + await process.call(context, {}, cfg, snapshot); + expect(execRequest.callCount).to.be.equal(4); // 2 from previous run, 1 for count, 1 for data + expect(context.emit.callCount).to.be.equal(10); // 5 + 3 data, 2 snapshots total + const finalSnapshot = context.emit.lastCall.args[1]; + expect(finalSnapshot).to.deep.equal({ + lastModificationTime: duplicateRecords[7].LastModifiedDate, + lastSeenId: duplicateRecords[7].Id, + }); + }); - it('emitIndividually with selected fields', async () => { - execRequest = sinon.stub(callJSForceMethod, 'call').returns(duplicateRecords); - const cfg = { - sobject: 'Document', - pageSize: 100, - linkedObjects: [], - selectedFields: ['Id', 'Name'], - emitBehavior: 'emitIndividually', - singlePagePerInterval: false, - }; - const snapshot = {}; - const msg = {}; + describe('validation', () => { + it('should throw an error for pageSize 0', async () => { + const cfg = { pageSize: 0, sobject: 'Document' }; const context = getContext(); - await process.call(context, msg, cfg, snapshot); - expect(execRequest.firstCall.args[3].selectedObjects).to.be.equal('Id,Name,LastModifiedDate'); - expect(context.emit.callCount).to.be.equal(28); - expect(context.emit.getCall(27).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(1); - expect(context.emit.getCall(0).lastArg.body).to.deep.equal(duplicateRecords[0]); + try { + await process.call(context, {}, cfg, {}); + throw new Error('Test failed: Error was not thrown'); + } catch (e) { + expect(e.message).to.include('"Size of Polling Page" must be valid number between 1 and 10000'); + } }); - it('fetchPage sizePage = 10, singlePagePerInterval = false', async () => { - const duplicateRecordsCopy = structuredClone(duplicateRecords); - execRequest = sinon.stub(callJSForceMethod, 'call') - .onCall(0).returns(duplicateRecordsCopy.slice(0, 10)) - .onCall(1).returns(duplicateRecordsCopy.slice(10, 20)) - .onCall(2).returns(duplicateRecordsCopy.slice(20, 30)); - const cfg = { - sobject: 'Document', - pageSize: 10, - linkedObjects: [], - emitBehavior: 'fetchPage', - singlePagePerInterval: false, - }; - const snapshot = {}; - const msg = {}; + it('should throw an error for pageSize "0"', async () => { + const cfg = { pageSize: '0', sobject: 'Document' }; const context = getContext(); - await process.call(context, msg, cfg, snapshot); - expect(context.emit.callCount).to.be.equal(4); - expect(context.emit.getCall(3).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(3); - const resultArray = context.emit.getCall(0).lastArg.body.results; - expect(resultArray).to.deep.equal(duplicateRecords.slice(0, 8)); + try { + await process.call(context, {}, cfg, {}); + throw new Error('Test failed: Error was not thrown'); + } catch (e) { + expect(e.message).to.include('"Size of Polling Page" must be valid number between 1 and 10000'); + } }); - it('fetchPage sizePage = 10, singlePagePerInterval = true', async () => { - execRequest = sinon.stub(callJSForceMethod, 'call') - .onCall(0).returns(duplicateRecords) - .onCall(1).returns(duplicateRecords.slice(8)) - .onCall(2).returns(duplicateRecords.slice(19)); - const cfg = { - sobject: 'Document', - pageSize: 100, - linkedObjects: [], - emitBehavior: 'fetchPage', - singlePagePerInterval: true, - }; - const snapshot = {}; - const msg = {}; + it('should throw an error for negative pageSize', async () => { + const cfg = { pageSize: -1, sobject: 'Document' }; const context = getContext(); - await process.call(context, msg, cfg, snapshot); - expect(context.emit.callCount).to.be.equal(2); - expect(context.emit.getCall(1).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(1); - expect(context.emit.getCall(0).lastArg.body.results).to.deep.equal(duplicateRecords.slice(0, 27)); - const resultArray = context.emit.getCall(0).lastArg.body.results; - const emitedSnapshot = context.emit.getCall(1).lastArg; - await process.call(context, msg, cfg, emitedSnapshot); - expect(context.emit.callCount).to.be.equal(4); - expect(context.emit.getCall(3).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(2); - resultArray.push(...context.emit.getCall(2).lastArg.body.results); - const emitedSnapshot2 = context.emit.getCall(3).lastArg; - await process.call(context, msg, cfg, emitedSnapshot2); - expect(context.emit.callCount).to.be.equal(6); - expect(context.emit.getCall(5).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(3); - resultArray.push(...context.emit.getCall(4).lastArg.body.results); - expect(resultArray).to.deep.equal(duplicateRecords); + try { + await process.call(context, {}, cfg, {}); + throw new Error('Test failed: Error was not thrown'); + } catch (e) { + expect(e.message).to.include('"Size of Polling Page" must be valid number between 1 and 10000'); + } }); - it('fetchPage sizePage = 100, singlePagePerInterval = true', async () => { - execRequest = sinon.stub(callJSForceMethod, 'call') - .onCall(0).returns(duplicateRecords); - const cfg = { - sobject: 'Document', - pageSize: 100, - linkedObjects: [], - emitBehavior: 'fetchPage', - singlePagePerInterval: true, - }; - const snapshot = {}; - const msg = {}; + it('should throw an error for pageSize > MAX_FETCH', async () => { + const cfg = { pageSize: 10001, sobject: 'Document' }; const context = getContext(); - await process.call(context, msg, cfg, snapshot); - expect(context.emit.callCount).to.be.equal(2); - expect(context.emit.getCall(1).firstArg).to.be.equal('snapshot'); - expect(execRequest.callCount).to.be.equal(1); - expect(context.emit.getCall(0).lastArg.body.results).to.deep.equal(duplicateRecords); - const emitedSnapshot = context.emit.getCall(1).lastArg; - expect(emitedSnapshot).to.have.property('nextStartTime'); - expect(emitedSnapshot).to.not.have.property('lastElementId'); + try { + await process.call(context, {}, cfg, {}); + throw new Error('Test failed: Error was not thrown'); + } catch (e) { + expect(e.message).to.include('"Size of Polling Page" must be valid number between 1 and 10000'); + } }); - it('startTime and endTime', async () => { - execRequest = sinon.stub(callJSForceMethod, 'call').returns(duplicateRecords); - const sobject = 'Document'; - const startTime = '2000-01-01T00:00:00.000Z'; - const endTime = '2022-01-01T00:00:00.000Z'; - const cfg = { - sobject, - startTime, - endTime, - pageSize: 300, - linkedObjects: [], - emitBehavior: 'fetchPage', - singlePagePerInterval: false, - }; - const msg = {}; + it('should throw an error for non-numeric pageSize', async () => { + const cfg = { pageSize: 'abc', sobject: 'Document' }; const context = getContext(); - await process.call(context, msg, cfg); - expect(execRequest.callCount).to.be.equal(1); - expect(execRequest.firstCall.args[1]).to.deep.equal(cfg); - expect(execRequest.firstCall.args[3]).to.deep.equal({ - linkedObjects: [], - selectedObjects: '*', - sobject, - maxFetch: cfg.pageSize, - whereCondition: `LastModifiedDate >= ${startTime} AND LastModifiedDate < ${endTime}`, - }); - expect(context.emit.callCount).to.be.equal(2); + try { + await process.call(context, {}, cfg, {}); + throw new Error('Test failed: Error was not thrown'); + } catch (e) { + expect(e.message).to.include('"Size of Polling Page" must be valid number between 1 and 10000'); + } }); }); });