From 85234095736001578df2ab565375ef3edac72ce3 Mon Sep 17 00:00:00 2001 From: Bowei Han Date: Tue, 12 Jul 2022 09:55:51 -0400 Subject: [PATCH] feat: batch API for historical, aggregated, and latest value data (#137) --- .gitignore | 1 + .stylelintignore | 1 + docs/AWSIoTSiteWiseSource.md | 28 + .../iot-time-series-connector.spec.ts | 11 +- .../iot-bar-chart.spec.component.ts | 18 +- .../iot-connector.spec.component.ts | 47 +- .../iot-kpi/iot-kpi.spec.component.ts | 26 +- .../iot-line-chart.spec.component.ts | 28 +- .../iot-scatter-chart.spec.component.ts | 18 +- .../iot-status-grid.spec.component.ts | 24 +- .../iot-status-timeline.spec.component.ts | 27 +- .../mocks/mockGetAggregatedOrRawResponse.ts | 125 ++++ .../src/testing/mocks/siteWiseSDK.ts | 12 +- .../testing/testing-ground/siteWiseQueries.ts | 28 +- .../testing/testing-ground/testing-ground.tsx | 135 +++- packages/core/package.json | 2 +- packages/core/src/__mocks__/iotsitewiseSDK.ts | 26 +- .../data-cache/dataReducer.spec.ts | 124 +++- .../src/data-module/data-cache/dataReducer.ts | 5 +- packages/source-iotsitewise/jest.config.js | 2 +- packages/source-iotsitewise/package.json | 3 +- .../src/__mocks__/assetPropertyValue.ts | 141 ++++ .../src/__mocks__/iotsitewiseSDK.ts | 26 +- packages/source-iotsitewise/src/initialize.ts | 13 +- .../src/time-series-data/client/batch.spec.ts | 149 +++++ .../src/time-series-data/client/batch.ts | 82 +++ .../batchGetAggregatedPropertyDataPoints.ts | 184 ++++++ .../batchGetHistoricalPropertyDataPoints.ts | 177 +++++ .../batchGetLatestPropertyDataPoints.ts | 158 +++++ .../time-series-data/client/client.spec.ts | 613 ++++++++++++++---- .../src/time-series-data/client/client.ts | 132 +++- .../getAggregatedPropertyDataPoints.ts | 12 +- .../getHistoricalPropertyDataPoints.ts | 10 +- .../getLatestPropertyDataPoint.ts | 8 +- .../src/time-series-data/data-source.spec.ts | 437 +++++-------- .../src/time-series-data/data-source.ts | 9 +- .../subscribeToTimeSeriesData.spec.ts | 34 +- .../src/time-series-data/types.ts | 5 + yarn.lock | 520 +++++++++++++-- 39 files changed, 2798 insertions(+), 603 deletions(-) create mode 100644 packages/source-iotsitewise/src/time-series-data/client/batch.spec.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/client/batch.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/client/batchGetAggregatedPropertyDataPoints.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/client/batchGetHistoricalPropertyDataPoints.ts create mode 100644 packages/source-iotsitewise/src/time-series-data/client/batchGetLatestPropertyDataPoints.ts rename packages/source-iotsitewise/src/time-series-data/client/{ => legacy}/getAggregatedPropertyDataPoints.ts (90%) rename packages/source-iotsitewise/src/time-series-data/client/{ => legacy}/getHistoricalPropertyDataPoints.ts (91%) rename packages/source-iotsitewise/src/time-series-data/client/{ => legacy}/getLatestPropertyDataPoint.ts (88%) diff --git a/.gitignore b/.gitignore index ad363f719..c8da6a841 100755 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ __diff_output__ # Cypress screenshots **/cypress/screenshots **/cypress/videos +**/cypress/snapshots/All Specs # Local development hard-coded credentials for use with the AWS SDK. creds.json diff --git a/.stylelintignore b/.stylelintignore index 8b12bbfae..9e455a0da 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -3,3 +3,4 @@ dist/ www/ loader/ node_modules/ +coverage/ diff --git a/docs/AWSIoTSiteWiseSource.md b/docs/AWSIoTSiteWiseSource.md index 85f58cbf7..5cc70f4c6 100644 --- a/docs/AWSIoTSiteWiseSource.md +++ b/docs/AWSIoTSiteWiseSource.md @@ -8,6 +8,8 @@ You can download the AWS IoT SiteWise source from the following location: https: To set up the AWS IoT SiteWise source, follow the instructions in [Getting started with IoT Application Kit](https://github.com/awslabs/iot-app-kit/tree/main/docs/GettingStarted.md). +--- + ## Queries The AWS IoT SiteWise source provides queries that you can use to filter AWS IoT SiteWise data and assets. @@ -34,6 +36,8 @@ query.timeSeriesData({ This query for time series data, can then be provided to any of the IoT App Kit components that support time series data. +--- + ## API ### `timeSeriesData` @@ -140,6 +144,7 @@ const { query } = initialize({ iotsitewiseClient }); ]} /> ``` +--- ### `assetTree` @@ -206,3 +211,26 @@ Type: Boolean Type: Boolean +--- + +## SiteWiseDataSourceSettings + +(Optional) Settings that can be provided when initializing the AWS IoT SiteWise source. + +``` +import { initialize } from '@iot-app-kit/source-iotsitewise'; + +const { IoTSiteWiseClient } = require("@aws-sdk/client-iotsitewise"); + +const iotsitewiseClient = new IoTSiteWiseClient({ region: "REGION" }); + +const { query } = initialize({ iotsitewiseClient, settings: { batchDuration: 100 } }); +``` + +`batchDuration` + +(Optional) Timeframe over which to coalesce time-series data requests before executing a batch request, specified in ms. e.g. a `batchDuration` of 100 will cause the AWS IoT SiteWise source to repeatedly batch all requests that occur within a 100 ms timeframe. + +Type: Number + +The AWS IoT SiteWise source communicates with SiteWise using batch APIs to reduce network overhead. By default, all individual requests for time-series data that occur within a single frame of execution are coalesced and executed in a batch request. This behaviour is scheduled using the [Job and JobQueue](https://262.ecma-international.org/6.0/#sec-jobs-and-job-queues) concepts. Depending on dashboard configuration, widget configuration, latency, and a multitude of other factors, batching on a single frame of execution might not be desirable. diff --git a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts index 0f6264c0e..4297eb624 100644 --- a/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts +++ b/packages/components/src/components/iot-time-series-connector/iot-time-series-connector.spec.ts @@ -1,7 +1,12 @@ import { newSpecPage } from '@stencil/core/testing'; import { MinimalLiveViewport } from '@synchro-charts/core'; import flushPromises from 'flush-promises'; -import { initialize, createMockSiteWiseSDK } from '@iot-app-kit/source-iotsitewise'; +import { + initialize, + createMockSiteWiseSDK, + BATCH_ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_DOUBLE_VALUE, +} from '@iot-app-kit/source-iotsitewise'; import { IotTimeSeriesConnector } from './iot-time-series-connector'; import { update } from '../../testing/update'; import { CustomHTMLElement } from '../../testing/types'; @@ -149,6 +154,8 @@ it('populates the name, unit, and data type from the asset model information fro Promise.resolve(createAssetResponse({ assetId: assetId as string, assetModelId })), describeAssetModel: ({ assetModelId }) => Promise.resolve(createAssetModelResponse({ assetModelId: assetModelId as string, propertyId: propertyId_1 })), + batchGetAssetPropertyValueHistory: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY), + batchGetAssetPropertyValue: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE), }), }); @@ -188,6 +195,8 @@ it('populates the name, unit, and data type from the asset model information fro Promise.resolve(createAssetResponse({ assetId: assetId as string, assetModelId })), describeAssetModel: ({ assetModelId }) => Promise.resolve(createAssetModelResponse({ assetModelId: assetModelId as string, propertyId: propertyId_1 })), + batchGetAssetPropertyValueHistory: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY), + batchGetAssetPropertyValue: jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE), }), }); diff --git a/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts b/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts index 9519310d4..02de4a73b 100644 --- a/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts +++ b/packages/components/src/integration/iot-bar-chart/iot-bar-chart.spec.component.ts @@ -1,5 +1,5 @@ import { renderChart } from '../../testing/renderChart'; -import { mockGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { mockBatchGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { ScaleConfig, ScaleType } from '@synchro-charts/core'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,13 +14,17 @@ describe('bar chart', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/aggregates?*', (req) => { + beforeEach(() => { + cy.intercept('/properties/batch/aggregates', (req) => { + const { startDate, endDate, resolution } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + resolution, }) ); }).as('getAggregates'); diff --git a/packages/components/src/integration/iot-connector/iot-connector.spec.component.ts b/packages/components/src/integration/iot-connector/iot-connector.spec.component.ts index 56c6aeb77..9fe9d24f5 100644 --- a/packages/components/src/integration/iot-connector/iot-connector.spec.component.ts +++ b/packages/components/src/integration/iot-connector/iot-connector.spec.component.ts @@ -1,5 +1,5 @@ import { renderChart, testChartContainerClassNameSelector } from '../../testing/renderChart'; -import { mockGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { mockBatchGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,39 +14,48 @@ describe('handles gestures', () => { const assetModelId = 'some-asset-model-id'; before(() => { - cy.intercept('/properties/history?*', (req) => { - if (new Date(req.query.startDate).getUTCFullYear() === 1899) { + cy.intercept('/properties/batch/history', (req) => { + const { startDate, endDate } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + if (new Date(startDateInMs).getUTCFullYear() === 1899) { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(new Date(req.query.endDate).getTime() - SECOND_IN_MS), - endDate: new Date(req.query.endDate), + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(new Date(endDateInMs).getTime() - SECOND_IN_MS), + endDate: new Date(endDateInMs), }) ); } else { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + entryId: '1-0', }) ); } }); - cy.intercept('/properties/aggregates?*', (req) => { - if (new Date(req.query.startDate).getUTCFullYear() === 1899) { + cy.intercept('/properties/batch/aggregates', (req) => { + const { startDate, endDate, resolution } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + if (new Date(startDateInMs).getUTCFullYear() === 1899) { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(new Date(req.query.endDate).getTime() - 60 * SECOND_IN_MS), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(new Date(endDateInMs).getTime() - 60 * SECOND_IN_MS), + endDate: new Date(endDateInMs), + resolution, }) ); } else { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + resolution, }) ); } diff --git a/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts b/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts index 4234880f5..316cbcb67 100644 --- a/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts +++ b/packages/components/src/integration/iot-kpi/iot-kpi.spec.component.ts @@ -1,5 +1,8 @@ import { renderChart } from '../../testing/renderChart'; -import { mockLatestValueResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { + mockBatchLatestValueResponse, + mockBatchGetAggregatedOrRawResponse, +} from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -13,9 +16,22 @@ describe('kpi', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/latest?*', (req) => { - req.reply(mockLatestValueResponse()); + beforeEach(() => { + cy.intercept('/properties/batch/history', (req) => { + const { startDate, endDate } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + req.reply( + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + }) + ); + }).as('getHistory'); + + cy.intercept('/properties/batch/latest', (req) => { + req.reply(mockBatchLatestValueResponse()); }).as('getAggregates'); cy.intercept(`/assets/${assetId}`, (req) => { @@ -30,7 +46,7 @@ describe('kpi', () => { it('renders', () => { renderChart({ chartType: 'iot-kpi', settings: { resolution: '0' }, viewport: { duration: '1m' } }); - cy.wait(['@getAggregates', '@getAssetSummary', '@getAssetModels']); + cy.wait(['@getAggregates', '@getAssetSummary', '@getAssetModels', '@getHistory']); cy.matchImageSnapshot(snapshotOptions); }); diff --git a/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts b/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts index 697d12778..c372ef45a 100644 --- a/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts +++ b/packages/components/src/integration/iot-line-chart/iot-line-chart.spec.component.ts @@ -1,5 +1,5 @@ import { renderChart } from '../../testing/renderChart'; -import { mockGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { mockBatchGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { ScaleConfig, ScaleType } from '@synchro-charts/core'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,22 +14,26 @@ describe('line chart', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/aggregates?*', (req) => { - if (new Date(req.query.startDate).getUTCFullYear() === 1899) { + beforeEach(() => { + cy.intercept('/properties/batch/aggregates', (req) => { + const { startDate, endDate, resolution } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + if (new Date(startDateInMs).getUTCFullYear() === 1899) { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(new Date(req.query.endDate).getTime() - 60 * SECOND_IN_MS), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(new Date(endDateInMs).getTime() - 60 * SECOND_IN_MS), + endDate: new Date(endDateInMs), + resolution, }) ); } else { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + resolution, }) ); } diff --git a/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts b/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts index e5643a166..32a66df11 100644 --- a/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts +++ b/packages/components/src/integration/iot-scatter-chart/iot-scatter-chart.spec.component.ts @@ -1,5 +1,5 @@ import { renderChart } from '../../testing/renderChart'; -import { mockGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { mockBatchGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { ScaleConfig, ScaleType } from '@synchro-charts/core'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,13 +14,17 @@ describe('scatter chart', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/aggregates?*', (req) => { + beforeEach(() => { + cy.intercept('/properties/batch/aggregates', (req) => { + const { startDate, endDate, resolution } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + resolution, }) ); }).as('getAggregates'); diff --git a/packages/components/src/integration/iot-status-grid/iot-status-grid.spec.component.ts b/packages/components/src/integration/iot-status-grid/iot-status-grid.spec.component.ts index 92b6609a7..06319a554 100644 --- a/packages/components/src/integration/iot-status-grid/iot-status-grid.spec.component.ts +++ b/packages/components/src/integration/iot-status-grid/iot-status-grid.spec.component.ts @@ -1,5 +1,8 @@ import { renderChart } from '../../testing/renderChart'; -import { mockLatestValueResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { + mockBatchLatestValueResponse, + mockBatchGetAggregatedOrRawResponse, +} from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { COMPARISON_OPERATOR } from '@synchro-charts/core'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,9 +17,22 @@ describe('status grid', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/latest?*', (req) => { - req.reply(mockLatestValueResponse()); + beforeEach(() => { + cy.intercept('/properties/batch/history', (req) => { + const { startDate, endDate } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + req.reply( + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + }) + ); + }); + + cy.intercept('/properties/batch/latest', (req) => { + req.reply(mockBatchLatestValueResponse()); }).as('getAggregates'); cy.intercept(`/assets/${assetId}`, (req) => { diff --git a/packages/components/src/integration/iot-status-timeline/iot-status-timeline.spec.component.ts b/packages/components/src/integration/iot-status-timeline/iot-status-timeline.spec.component.ts index e3d57cf9e..621c6bd58 100644 --- a/packages/components/src/integration/iot-status-timeline/iot-status-timeline.spec.component.ts +++ b/packages/components/src/integration/iot-status-timeline/iot-status-timeline.spec.component.ts @@ -1,5 +1,5 @@ import { renderChart } from '../../testing/renderChart'; -import { mockGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; +import { mockBatchGetAggregatedOrRawResponse } from '../../testing/mocks/mockGetAggregatedOrRawResponse'; import { mockGetAssetSummary } from '../../testing/mocks/mockGetAssetSummaries'; import { COMPARISON_OPERATOR } from '@synchro-charts/core'; import { mockGetAssetModelSummary } from '../../testing/mocks/mockGetAssetModelSummary'; @@ -14,22 +14,25 @@ describe('status timeline', () => { const assetId = 'some-asset-id'; const assetModelId = 'some-asset-model-id'; - before(() => { - cy.intercept('/properties/history?*', (req) => { - if (new Date(req.query.startDate).getUTCFullYear() === 1899) { + beforeEach(() => { + cy.intercept('/properties/batch/history', (req) => { + const { startDate, endDate } = req.body.entries[0]; + const startDateInMs = startDate * SECOND_IN_MS; + const endDateInMs = endDate * SECOND_IN_MS; + + if (new Date(startDateInMs).getUTCFullYear() === 1899) { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(new Date(req.query.endDate).getTime() - SECOND_IN_MS), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(new Date(endDateInMs).getTime() - SECOND_IN_MS), + endDate: new Date(endDateInMs), }) ); } else { req.reply( - mockGetAggregatedOrRawResponse({ - startDate: new Date(req.query.startDate), - endDate: new Date(req.query.endDate), - resolution: req.query.resolution as string, + mockBatchGetAggregatedOrRawResponse({ + startDate: new Date(startDateInMs), + endDate: new Date(endDateInMs), + entryId: '1-0', }) ); } diff --git a/packages/components/src/testing/mocks/mockGetAggregatedOrRawResponse.ts b/packages/components/src/testing/mocks/mockGetAggregatedOrRawResponse.ts index ed13eaad8..e54b55718 100644 --- a/packages/components/src/testing/mocks/mockGetAggregatedOrRawResponse.ts +++ b/packages/components/src/testing/mocks/mockGetAggregatedOrRawResponse.ts @@ -4,6 +4,9 @@ import { GetAssetPropertyAggregatesResponse, GetAssetPropertyValueHistoryResponse, GetAssetPropertyValueResponse, + BatchGetAssetPropertyValueHistoryResponse, + BatchGetAssetPropertyAggregatesResponse, + BatchGetAssetPropertyValueResponse, } from '@aws-sdk/client-iotsitewise'; import { RAW_DATA, MINUTE_AGGREGATED_DATA, HOUR_AGGREGATED_DATA, DAY_AGGREGATED_DATA } from './data'; import { MINUTE_IN_MS, HOUR_IN_MS, DAY_IN_MS, SECOND_IN_MS } from '@iot-app-kit/core/src/common/time'; @@ -20,6 +23,21 @@ export const mockLatestValueResponse = (): { body: GetAssetPropertyValueResponse }; }; +export const mockBatchLatestValueResponse = (): { body: BatchGetAssetPropertyValueResponse } => { + return { + body: { + successEntries: [ + { + entryId: '0-0', + assetPropertyValue: RAW_DATA[RAW_DATA.length - 1], + }, + ], + errorEntries: [], + skippedEntries: [], + }, + }; +}; + /** * Returns exactly what the parsed JSON response from the SDK returns. * @@ -97,3 +115,110 @@ export const mockGetAggregatedOrRawResponse = ({ }; } }; + +/** + * Returns exactly what the parsed JSON response from the SDK returns for batch calls + * + * There's slight deviations from the specified types: + * 1. The timestamps are converted from number to date for the times that don't have a nanosecond component, so we must 'cast' them to allow the types to pass + * 2. The `undefined` is turned into a null, so we must cast those. + */ +export const mockBatchGetAggregatedOrRawResponse = ({ + startDate, + endDate, + resolution, + entryId = '0-0', +}: { + startDate: Date; + endDate: Date; + resolution?: string; + entryId?: string; +}): { body: BatchGetAssetPropertyValueHistoryResponse | BatchGetAssetPropertyAggregatesResponse } => { + const startTimestampInSeconds = Math.round(startDate.getTime()) / 1000; + const endTimestampInSeconds = Math.round(endDate.getTime()) / 1000; + + if (resolution === '1m') { + const data: AggregatedValue[] = []; + for (let timestamp = startTimestampInSeconds; timestamp <= endTimestampInSeconds; timestamp += MINUTE_IN_S) { + data.push({ + ...MINUTE_AGGREGATED_DATA[timestamp % MINUTE_AGGREGATED_DATA.length], + timestamp: timestamp as unknown as Date, + }); + } + + return { + body: { + successEntries: [ + { + entryId, + aggregatedValues: data, + }, + ], + errorEntries: [], + skippedEntries: [], + }, + }; + } else if (resolution === '1h') { + const data: AggregatedValue[] = []; + for (let timestamp = startTimestampInSeconds; timestamp <= endTimestampInSeconds; timestamp += HOUR_IN_S) { + data.push({ + ...HOUR_AGGREGATED_DATA[timestamp % HOUR_AGGREGATED_DATA.length], + timestamp: timestamp as unknown as Date, + }); + } + + return { + body: { + successEntries: [ + { + entryId, + aggregatedValues: data, + }, + ], + errorEntries: [], + skippedEntries: [], + }, + }; + } else if (resolution === '1d') { + const data: AggregatedValue[] = []; + for (let timestamp = startTimestampInSeconds; timestamp <= endTimestampInSeconds; timestamp += DAY_IN_S) { + data.push({ + ...DAY_AGGREGATED_DATA[timestamp % DAY_AGGREGATED_DATA.length], + timestamp: timestamp as unknown as Date, + }); + } + + return { + body: { + successEntries: [ + { + entryId, + aggregatedValues: data, + }, + ], + errorEntries: [], + skippedEntries: [], + }, + }; + } else { + const data: AssetPropertyValue[] = []; + for (let timeInSeconds = startTimestampInSeconds; timeInSeconds <= endTimestampInSeconds; timeInSeconds++) { + data.push({ + ...RAW_DATA[timeInSeconds % RAW_DATA.length], + timestamp: { offsetInNanos: 0, timeInSeconds }, + }); + } + return { + body: { + successEntries: [ + { + entryId, + assetPropertyValueHistory: data, + }, + ], + errorEntries: [], + skippedEntries: [], + }, + }; + } +}; diff --git a/packages/components/src/testing/mocks/siteWiseSDK.ts b/packages/components/src/testing/mocks/siteWiseSDK.ts index 7ffd172da..3dbfd123e 100644 --- a/packages/components/src/testing/mocks/siteWiseSDK.ts +++ b/packages/components/src/testing/mocks/siteWiseSDK.ts @@ -2,14 +2,18 @@ import { createMockSiteWiseSDK, createAssetResponse, createAssetModelResponse, - ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_AGGREGATES, + BATCH_ASSET_PROPERTY_DOUBLE_VALUE, } from '@iot-app-kit/source-iotsitewise'; const PROPERTY_ID = 'some-property-id'; const ASSET_MODEL_ID = 'some-asset-model-id'; const PROPERTY_NAME = 'some-property-name'; -const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); +const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); +const batchGetAssetPropertyAggregates = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_AGGREGATES); +const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); const describeAsset = jest .fn() .mockImplementation(({ assetId }) => @@ -28,5 +32,7 @@ const describeAssetModel = jest.fn().mockImplementation(({ assetModelId }) => export const mockSiteWiseSDK = createMockSiteWiseSDK({ describeAsset, describeAssetModel, - getAssetPropertyValueHistory, + batchGetAssetPropertyValueHistory, + batchGetAssetPropertyAggregates, + batchGetAssetPropertyValue, }); diff --git a/packages/components/src/testing/testing-ground/siteWiseQueries.ts b/packages/components/src/testing/testing-ground/siteWiseQueries.ts index edc19c3ec..1ec32b90d 100644 --- a/packages/components/src/testing/testing-ground/siteWiseQueries.ts +++ b/packages/components/src/testing/testing-ground/siteWiseQueries.ts @@ -1,18 +1,30 @@ -const STRING_ASSET_ID = 'f2f74fa8-625a-435f-b89c-d27b2d84f45b'; +const STRING_ASSET_ID = 'fa94ab3e-d02f-4c50-88e1-f017c9069c4d'; -export const DEMO_TURBINE_ASSET_1 = '08654543-acb0-403d-96d4-eee30b89d7b4'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_1 = 'e07aed33-334a-41bf-9589-db1e6b2655ca'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_2 = 'a94f4a39-4fd1-4c5b-a466-5e1c7a8c0a53'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_3 = 'f167c24f-3e35-42b4-b493-3b13fe4fbf79'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_4 = 'd8937b65-5f03-4e40-93ac-c5513420ade7'; +export const DEMO_TURBINE_ASSET_1 = 'fa94ab3e-d02f-4c50-88e1-f017c9069c4d'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_1 = 'f63861af-251e-4d0b-a32e-4d2089d35b1c'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_2 = 'ea55054e-55d0-4eda-8359-2cbc19d52a3d'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_3 = '18faca18-a8e5-4e48-9153-462c54980869'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_4 = '95648016-38d0-45e7-8f53-872404b9b471'; + +export const DEMO_TURBINE_ASSET_2 = 'e6fb533c-4919-4981-a71f-7764ccc10867'; +export const DEMO_TURBINE_ASSET_2_PROPERTY_1 = 'f63861af-251e-4d0b-a32e-4d2089d35b1c'; +export const DEMO_TURBINE_ASSET_2_PROPERTY_2 = 'ea55054e-55d0-4eda-8359-2cbc19d52a3d'; +export const DEMO_TURBINE_ASSET_2_PROPERTY_3 = '18faca18-a8e5-4e48-9153-462c54980869'; +export const DEMO_TURBINE_ASSET_2_PROPERTY_4 = '95648016-38d0-45e7-8f53-872404b9b471'; + +export const DEMO_TURBINE_ASSET_3 = 'bc86bf78-c506-460c-8602-0c534356892a'; +export const DEMO_TURBINE_ASSET_3_PROPERTY_1 = 'f63861af-251e-4d0b-a32e-4d2089d35b1c'; +export const DEMO_TURBINE_ASSET_3_PROPERTY_2 = 'ea55054e-55d0-4eda-8359-2cbc19d52a3d'; +export const DEMO_TURBINE_ASSET_3_PROPERTY_3 = '18faca18-a8e5-4e48-9153-462c54980869'; +export const DEMO_TURBINE_ASSET_3_PROPERTY_4 = '95648016-38d0-45e7-8f53-872404b9b471'; export const ASSET_DETAILS_QUERY = { assetId: STRING_ASSET_ID, }; const AGGREGATED_DATA_ASSET = STRING_ASSET_ID; -const AGGREGATED_DATA_PROPERTY = 'd0dc79be-0dc2-418c-ac23-26f33cdb4b8b'; -const AGGREGATED_DATA_PROPERTY_2 = '69607dc2-5fbe-416d-aac2-0382018626e4'; +const AGGREGATED_DATA_PROPERTY = 'b1616ab4-7526-4c0a-85e2-a137cf57d668'; +const AGGREGATED_DATA_PROPERTY_2 = '729479fb-8720-4a70-bd55-2a9f9eaaa32e4'; export const AGGREGATED_DATA_QUERY = { assets: [ diff --git a/packages/components/src/testing/testing-ground/testing-ground.tsx b/packages/components/src/testing/testing-ground/testing-ground.tsx index d167dfdb7..5f2293996 100755 --- a/packages/components/src/testing/testing-ground/testing-ground.tsx +++ b/packages/components/src/testing/testing-ground/testing-ground.tsx @@ -5,6 +5,18 @@ import { DEMO_TURBINE_ASSET_1, DEMO_TURBINE_ASSET_1_PROPERTY_1, DEMO_TURBINE_ASSET_1_PROPERTY_2, + DEMO_TURBINE_ASSET_1_PROPERTY_3, + DEMO_TURBINE_ASSET_1_PROPERTY_4, + DEMO_TURBINE_ASSET_2, + DEMO_TURBINE_ASSET_2_PROPERTY_1, + DEMO_TURBINE_ASSET_2_PROPERTY_2, + DEMO_TURBINE_ASSET_2_PROPERTY_3, + DEMO_TURBINE_ASSET_2_PROPERTY_4, + DEMO_TURBINE_ASSET_3, + DEMO_TURBINE_ASSET_3_PROPERTY_1, + DEMO_TURBINE_ASSET_3_PROPERTY_2, + DEMO_TURBINE_ASSET_3_PROPERTY_3, + DEMO_TURBINE_ASSET_3_PROPERTY_4, } from './siteWiseQueries'; import { getEnvCredentials } from './getEnvCredentials'; @@ -26,28 +38,14 @@ export class TestingGround { private query: SiteWiseQuery; componentWillLoad() { - const { query } = initialize({ awsCredentials: getEnvCredentials(), awsRegion: 'us-west-2' }); + const { query } = initialize({ + awsCredentials: getEnvCredentials(), + awsRegion: 'us-west-2', + settings: { batchDuration: undefined, legacyAPI: false }, + }); this.query = query; } - private changeResolution = (ev: Event) => { - const resolution = (ev.target as HTMLSelectElement)?.value; - - if (resolution === 'auto') { - this.resolution = DEFAULT_RESOLUTION_MAPPING; - } else if (resolution === '0') { - this.resolution = {}; - } else { - this.resolution = resolution; - } - }; - - private changeDuration = (ev: Event) => { - const duration = `${(ev.target as HTMLSelectElement)?.value}m`; - - this.viewport = { duration }; - }; - render() { return (
@@ -56,32 +54,111 @@ export class TestingGround {

- - + Promise.reject(new Error('Mock method not override.')); @@ -29,6 +35,9 @@ export const createMockSiteWiseSDK = ({ getAssetPropertyAggregates = nonOverriddenMock, getAssetPropertyValueHistory = nonOverriddenMock, getInterpolatedAssetPropertyValues = nonOverriddenMock, + batchGetAssetPropertyValueHistory = nonOverriddenMock, + batchGetAssetPropertyAggregates = nonOverriddenMock, + batchGetAssetPropertyValue = nonOverriddenMock, }: { listAssets?: (input: ListAssetsCommandInput) => Promise; listAssociatedAssets?: (input: ListAssociatedAssetsCommandInput) => Promise; @@ -44,6 +53,15 @@ export const createMockSiteWiseSDK = ({ getInterpolatedAssetPropertyValues?: ( input: GetInterpolatedAssetPropertyValuesCommandInput ) => Promise; + batchGetAssetPropertyValueHistory?: ( + input: BatchGetAssetPropertyValueHistoryCommandInput + ) => Promise; + batchGetAssetPropertyAggregates?: ( + input: BatchGetAssetPropertyAggregatesCommandInput + ) => Promise; + batchGetAssetPropertyValue?: ( + input: BatchGetAssetPropertyValueCommandInput + ) => Promise; } = {}) => ({ send: (command: { input: any }) => { @@ -68,6 +86,12 @@ export const createMockSiteWiseSDK = ({ return getAssetPropertyValueHistory(command.input); case 'GetInterpolatedAssetPropertyValuesCommand': return getInterpolatedAssetPropertyValues(command.input); + case 'BatchGetAssetPropertyValueHistoryCommand': + return batchGetAssetPropertyValueHistory(command.input); + case 'BatchGetAssetPropertyAggregatesCommand': + return batchGetAssetPropertyAggregates(command.input); + case 'BatchGetAssetPropertyValueCommand': + return batchGetAssetPropertyValue(command.input); default: throw new Error( `missing mock implementation for command name ${commandName}. Add a new command within the mock SiteWise SDK.` diff --git a/packages/core/src/data-module/data-cache/dataReducer.spec.ts b/packages/core/src/data-module/data-cache/dataReducer.spec.ts index 194202af7..59b5ffc8c 100755 --- a/packages/core/src/data-module/data-cache/dataReducer.spec.ts +++ b/packages/core/src/data-module/data-cache/dataReducer.spec.ts @@ -482,6 +482,70 @@ it('sets the data with the correct cache intervals when a success action occurs ); }); +it('sets the data with the correct cache intervals when a success action occurs with fetchMostRecentBeforeEnd', () => { + const ID = 'my-id'; + const RESOLUTION = SECOND_IN_MS; + + const INITIAL_STATE = { + [ID]: { + [RESOLUTION]: { + id: ID, + resolution: RESOLUTION, + isLoading: true, + isRefreshing: true, + requestHistory: [], + dataCache: EMPTY_CACHE, + requestCache: EMPTY_CACHE, + }, + }, + }; + + const newDataPoints = [{ x: DATE_BEFORE.getTime(), y: 100 }]; + + const DATA: DataStream = { + id: ID, + name: 'some name', + resolution: RESOLUTION, + aggregates: { + [RESOLUTION]: newDataPoints, + }, + data: [], + dataType: DataType.NUMBER, + }; + const newState = dataReducer( + INITIAL_STATE, + onSuccessAction(ID, DATA, FIRST_DATE, LAST_DATE, { + id: ID, + resolution: '1s', + start: FIRST_DATE, + end: LAST_DATE, + fetchMostRecentBeforeEnd: true, + }) + ); + expect(newState?.[ID]?.[RESOLUTION]).toEqual( + expect.objectContaining({ + id: ID, + resolution: RESOLUTION, + error: undefined, + isLoading: false, + requestHistory: [ + expect.objectContaining({ + end: expect.any(Date), + requestedAt: expect.any(Date), + start: expect.any(Date), + }), + ], + dataCache: { + intervals: [[DATE_BEFORE.getTime(), LAST_DATE.getTime()]], + items: [newDataPoints], + }, + requestCache: expect.objectContaining({ + intervals: [[DATE_BEFORE.getTime(), LAST_DATE.getTime()]], + }), + }) + ); +}); + it('sets the data with the correct cache intervals when a success action occurs with fetchMostRecentBeforeStart if no data is returned', () => { const ID = 'my-id'; const RESOLUTION = SECOND_IN_MS; @@ -541,6 +605,65 @@ it('sets the data with the correct cache intervals when a success action occurs ); }); +it('sets the data with the correct cache intervals when a success action occurs with fetchMostRecentBeforeEnd if no data is returned', () => { + const ID = 'my-id'; + const RESOLUTION = SECOND_IN_MS; + + const INITIAL_STATE = { + [ID]: { + [RESOLUTION]: { + id: ID, + resolution: RESOLUTION, + isLoading: true, + isRefreshing: true, + requestHistory: [], + dataCache: EMPTY_CACHE, + requestCache: EMPTY_CACHE, + }, + }, + }; + + const DATA: DataStream = { + id: ID, + name: 'some name', + resolution: RESOLUTION, + data: [], + dataType: DataType.NUMBER, + }; + const newState = dataReducer( + INITIAL_STATE, + onSuccessAction(ID, DATA, FIRST_DATE, LAST_DATE, { + id: ID, + resolution: '1s', + start: FIRST_DATE, + end: LAST_DATE, + fetchMostRecentBeforeEnd: true, + }) + ); + expect(newState?.[ID]?.[RESOLUTION]).toEqual( + expect.objectContaining({ + id: ID, + resolution: RESOLUTION, + error: undefined, + isLoading: false, + requestHistory: [ + expect.objectContaining({ + end: expect.any(Date), + requestedAt: expect.any(Date), + start: expect.any(Date), + }), + ], + dataCache: { + intervals: [[FIRST_DATE.getTime(), LAST_DATE.getTime()]], + items: [[]], + }, + requestCache: expect.objectContaining({ + intervals: [[FIRST_DATE.getTime(), LAST_DATE.getTime()]], + }), + }) + ); +}); + it('merges data into existing data cache', () => { const ID = 'my-id'; @@ -614,7 +737,6 @@ it('merges data into existing data cache', () => { resolution: '1s', start: START_DATE_1, end: END_DATE_1, - fetchMostRecentBeforeEnd: true, }) ); diff --git a/packages/core/src/data-module/data-cache/dataReducer.ts b/packages/core/src/data-module/data-cache/dataReducer.ts index 5920177fd..1222db58e 100755 --- a/packages/core/src/data-module/data-cache/dataReducer.ts +++ b/packages/core/src/data-module/data-cache/dataReducer.ts @@ -82,7 +82,10 @@ export const dataReducer: Reducer = ( // start the interval from the returned data point to avoid over-caching // if there is no data point it's fine to cache the entire interval - if (requestInformation.fetchMostRecentBeforeStart && sortedData.length > 0) { + if ( + (requestInformation.fetchMostRecentBeforeStart || requestInformation.fetchMostRecentBeforeEnd) && + sortedData.length > 0 + ) { intervalStart = new Date(sortedData[0].x); } diff --git a/packages/source-iotsitewise/jest.config.js b/packages/source-iotsitewise/jest.config.js index 6cf8ff5d8..86eed58a7 100644 --- a/packages/source-iotsitewise/jest.config.js +++ b/packages/source-iotsitewise/jest.config.js @@ -4,7 +4,7 @@ module.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['jest-extended/all'], collectCoverageFrom: ['src/**/*.{ts,tsx}'], - coveragePathIgnorePatterns: ['/src/__mocks__'], + coveragePathIgnorePatterns: ['/src/__mocks__', 'src/time-series-data/client/legacy'], testPathIgnorePatterns: ['/dist'], coverageReporters: ['text-summary', 'cobertura', 'html', 'json', 'json-summary'], moduleNameMapper: { diff --git a/packages/source-iotsitewise/package.json b/packages/source-iotsitewise/package.json index 4c97afe3c..b2be470c8 100644 --- a/packages/source-iotsitewise/package.json +++ b/packages/source-iotsitewise/package.json @@ -37,10 +37,11 @@ "pack": "yarn pack" }, "dependencies": { - "@aws-sdk/client-iotsitewise": "^3.39.0", + "@aws-sdk/client-iotsitewise": "^3.87.0", "@iot-app-kit/core": "^1.5.0", "@rollup/plugin-typescript": "^8.3.0", "@synchro-charts/core": "^5.0.0", + "dataloader": "^2.1.0", "flush-promises": "^1.0.2", "rxjs": "^7.4.0", "typescript": "4.4.4" diff --git a/packages/source-iotsitewise/src/__mocks__/assetPropertyValue.ts b/packages/source-iotsitewise/src/__mocks__/assetPropertyValue.ts index f0b899db4..71ec5e643 100644 --- a/packages/source-iotsitewise/src/__mocks__/assetPropertyValue.ts +++ b/packages/source-iotsitewise/src/__mocks__/assetPropertyValue.ts @@ -3,6 +3,9 @@ import { GetAssetPropertyAggregatesResponse, GetAssetPropertyValueHistoryResponse, GetAssetPropertyValueResponse, + BatchGetAssetPropertyValueHistoryResponse, + BatchGetAssetPropertyAggregatesResponse, + BatchGetAssetPropertyValueResponse, Quality, } from '@aws-sdk/client-iotsitewise'; @@ -48,6 +51,90 @@ export const ASSET_PROPERTY_VALUE_HISTORY: GetAssetPropertyValueHistoryResponse ], }; +export const BATCH_ASSET_PROPERTY_VALUE_HISTORY: BatchGetAssetPropertyValueHistoryResponse = { + successEntries: [ + { + entryId: '0-0', + assetPropertyValueHistory: [ + { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + { + value: { + doubleValue: 12.01, + }, + timestamp: { + timeInSeconds: 2000, + offsetInNanos: 0, + }, + }, + ], + }, + { + entryId: '0-1', + assetPropertyValueHistory: [ + { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + ], + }, + { + entryId: '1-0', + assetPropertyValueHistory: [ + { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + ], + }, + { + entryId: '1-1', + assetPropertyValueHistory: [ + { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + ], + }, + ], + errorEntries: [], + skippedEntries: [], +}; + +export const BATCH_ASSET_PROPERTY_ERROR_ENTRY = { + entryId: '0-0', + errorMessage: 'assetId 1 not found', + errorCode: '404', +}; + +export const BATCH_ASSET_PROPERTY_ERROR: BatchGetAssetPropertyValueHistoryResponse = { + successEntries: [], + errorEntries: [BATCH_ASSET_PROPERTY_ERROR_ENTRY], + skippedEntries: [], +}; + export const AGGREGATE_VALUES: GetAssetPropertyAggregatesResponse = { aggregatedValues: [ { @@ -71,6 +158,60 @@ export const AGGREGATE_VALUES: GetAssetPropertyAggregatesResponse = { ], }; +export const BATCH_ASSET_PROPERTY_AGGREGATES: BatchGetAssetPropertyAggregatesResponse = { + successEntries: [ + { + entryId: '0-0', + ...AGGREGATE_VALUES, + }, + { + entryId: '0-1', + ...AGGREGATE_VALUES, + }, + { + entryId: '1-0', + ...AGGREGATE_VALUES, + }, + { + entryId: '1-1', + ...AGGREGATE_VALUES, + }, + ], + errorEntries: [], + skippedEntries: [], +}; + +export const BATCH_ASSET_PROPERTY_DOUBLE_VALUE: BatchGetAssetPropertyValueResponse = { + successEntries: [ + { + entryId: '0-0', + assetPropertyValue: { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + }, + { + entryId: '0-1', + assetPropertyValue: { + value: { + doubleValue: 10.123, + }, + timestamp: { + timeInSeconds: 1000, + offsetInNanos: 99000004, + }, + }, + }, + ], + errorEntries: [], + skippedEntries: [], +}; + export const ASSET_PROPERTY_DOUBLE_VALUE: GetAssetPropertyValueResponse = { propertyValue: { value: { diff --git a/packages/source-iotsitewise/src/__mocks__/iotsitewiseSDK.ts b/packages/source-iotsitewise/src/__mocks__/iotsitewiseSDK.ts index cf2d8503b..1af909fa0 100644 --- a/packages/source-iotsitewise/src/__mocks__/iotsitewiseSDK.ts +++ b/packages/source-iotsitewise/src/__mocks__/iotsitewiseSDK.ts @@ -11,11 +11,17 @@ import { GetAssetPropertyValueResponse, GetInterpolatedAssetPropertyValuesCommandInput, GetInterpolatedAssetPropertyValuesResponse, + BatchGetAssetPropertyValueHistoryCommandInput, + BatchGetAssetPropertyValueHistoryResponse, + BatchGetAssetPropertyAggregatesCommandInput, + BatchGetAssetPropertyAggregatesResponse, + BatchGetAssetPropertyValueCommandInput, + BatchGetAssetPropertyValueResponse, IoTSiteWiseClient, ListAssetsCommandInput, ListAssociatedAssetsCommandInput, ListAssociatedAssetsResponse, - ListAssetsResponse + ListAssetsResponse, } from '@aws-sdk/client-iotsitewise'; const nonOverriddenMock = () => Promise.reject(new Error('Mock method not override.')); @@ -29,6 +35,9 @@ export const createMockSiteWiseSDK = ({ getAssetPropertyAggregates = nonOverriddenMock, getAssetPropertyValueHistory = nonOverriddenMock, getInterpolatedAssetPropertyValues = nonOverriddenMock, + batchGetAssetPropertyValueHistory = nonOverriddenMock, + batchGetAssetPropertyAggregates = nonOverriddenMock, + batchGetAssetPropertyValue = nonOverriddenMock, }: { listAssets?: (input: ListAssetsCommandInput) => Promise; listAssociatedAssets?: (input: ListAssociatedAssetsCommandInput) => Promise; @@ -44,6 +53,15 @@ export const createMockSiteWiseSDK = ({ getInterpolatedAssetPropertyValues?: ( input: GetInterpolatedAssetPropertyValuesCommandInput ) => Promise; + batchGetAssetPropertyValueHistory?: ( + input: BatchGetAssetPropertyValueHistoryCommandInput + ) => Promise; + batchGetAssetPropertyAggregates?: ( + input: BatchGetAssetPropertyAggregatesCommandInput + ) => Promise; + batchGetAssetPropertyValue?: ( + input: BatchGetAssetPropertyValueCommandInput + ) => Promise; } = {}) => ({ send: (command: { input: any }) => { @@ -68,6 +86,12 @@ export const createMockSiteWiseSDK = ({ return getAssetPropertyValueHistory(command.input); case 'GetInterpolatedAssetPropertyValuesCommand': return getInterpolatedAssetPropertyValues(command.input); + case 'BatchGetAssetPropertyValueHistoryCommand': + return batchGetAssetPropertyValueHistory(command.input); + case 'BatchGetAssetPropertyAggregatesCommand': + return batchGetAssetPropertyAggregates(command.input); + case 'BatchGetAssetPropertyValueCommand': + return batchGetAssetPropertyValue(command.input); default: throw new Error( `missing mock implementation for command name ${commandName}. Add a new command within the mock SiteWise SDK.` diff --git a/packages/source-iotsitewise/src/initialize.ts b/packages/source-iotsitewise/src/initialize.ts index 37f6a6854..1b9dff427 100644 --- a/packages/source-iotsitewise/src/initialize.ts +++ b/packages/source-iotsitewise/src/initialize.ts @@ -1,6 +1,6 @@ import { SiteWiseTimeSeriesDataProvider } from './time-series-data/provider'; import { IotAppKitDataModule, TreeQuery, TimeQuery, TimeSeriesData, TimeSeriesDataRequest } from '@iot-app-kit/core'; -import { SiteWiseAssetQuery } from './time-series-data/types'; +import { SiteWiseAssetQuery, SiteWiseDataSourceSettings } from './time-series-data/types'; import { BranchReference, RootedSiteWiseAssetTreeQueryArguments, @@ -18,7 +18,7 @@ import { Credentials, Provider as AWSCredentialsProvider } from '@aws-sdk/types' import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; import { assetSession } from './sessions'; -type IoTAppKitInitInputs = +type SiteWiseDataSourceInitInputs = ( | { registerDataSources?: boolean; iotSiteWiseClient: IoTSiteWiseClient; @@ -27,7 +27,10 @@ type IoTAppKitInitInputs = registerDataSources?: boolean; awsCredentials: Credentials | AWSCredentialsProvider; awsRegion: string; - }; + } +) & { + settings?: SiteWiseDataSourceSettings; +}; export type SiteWiseQuery = { timeSeriesData: (query: SiteWiseAssetQuery) => TimeQuery; @@ -43,7 +46,7 @@ export type SiteWiseQuery = { * @param awsCredentials - https://www.npmjs.com/package/@aws-sdk/credential-providers * @param awsRegion - Region for AWS based data sources to point towards, i.e. us-east-1 */ -export const initialize = (input: IoTAppKitInitInputs) => { +export const initialize = (input: SiteWiseDataSourceInitInputs) => { const siteWiseTimeSeriesModule = new IotAppKitDataModule(); const siteWiseSdk = 'iotSiteWiseClient' in input ? input.iotSiteWiseClient : sitewiseSdk(input.awsCredentials, input.awsRegion); @@ -53,7 +56,7 @@ export const initialize = (input: IoTAppKitInitInputs) => { if (input.registerDataSources !== false) { /** Automatically registered data sources */ - siteWiseTimeSeriesModule.registerDataSource(createDataSource(siteWiseSdk)); + siteWiseTimeSeriesModule.registerDataSource(createDataSource(siteWiseSdk, input.settings)); } return { diff --git a/packages/source-iotsitewise/src/time-series-data/client/batch.spec.ts b/packages/source-iotsitewise/src/time-series-data/client/batch.spec.ts new file mode 100644 index 000000000..bcf6c3ed3 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/client/batch.spec.ts @@ -0,0 +1,149 @@ +import { + createEntryBatches, + calculateNextBatchSize, + shouldFetchNextBatch, + MAX_BATCH_RESULTS, + MAX_BATCH_ENTRIES, + NO_LIMIT_BATCH, +} from './batch'; + +describe('createEntryBatches', () => { + it('buckets entries by maxResults for a given batch', () => { + const batches = createEntryBatches([ + { + id: '1', + maxResults: undefined, + }, + { + id: '2', + maxResults: 2000, + }, + { + id: '3', + maxResults: 2000, + }, + { + id: '4', + maxResults: 10, + }, + { + id: '5', + maxResults: 2000, + }, + ]); + + expect(batches).toEqual( + expect.arrayContaining([ + [ + [ + { + id: '1', + maxResults: undefined, + }, + ], + NO_LIMIT_BATCH, + ], + [ + [ + { + id: '2', + maxResults: 2000, + }, + { + id: '3', + maxResults: 2000, + }, + { + id: '5', + maxResults: 2000, + }, + ], + 6000, + ], + [ + [ + { + id: '4', + maxResults: 10, + }, + ], + 10, + ], + ]) + ); + }); + + it('chunks batches that exceed max entry size (16)', () => { + const entrySize = 2000; + + const entries = [ + ...[...Array(MAX_BATCH_ENTRIES * 3)].map((args, index) => ({ + id: String(index), + maxResults: entrySize, + })), + { + id: 'abc', + maxResults: 10, + }, + ]; + + const batches = createEntryBatches(entries); + + expect(batches).toEqual( + expect.arrayContaining([ + [entries.slice(0, MAX_BATCH_ENTRIES), MAX_BATCH_ENTRIES * entrySize], + [entries.slice(MAX_BATCH_ENTRIES, 2 * MAX_BATCH_ENTRIES), MAX_BATCH_ENTRIES * entrySize], + [entries.slice(MAX_BATCH_ENTRIES * 2, MAX_BATCH_ENTRIES * 3), MAX_BATCH_ENTRIES * entrySize], + [[entries[MAX_BATCH_ENTRIES * 3]], 10], + ]) + ); + }); + + it('handles empty input', () => { + const batches = createEntryBatches([]); + expect(batches).toEqual([]); + }); +}); + +describe('calculateNextBatchSize', () => { + it('returns the correct max batch size for no limit batches', () => { + expect(calculateNextBatchSize({ maxResults: NO_LIMIT_BATCH, dataPointsFetched: 0 })).toBe(MAX_BATCH_RESULTS); + expect(calculateNextBatchSize({ maxResults: NO_LIMIT_BATCH, dataPointsFetched: 100000 })).toBe(MAX_BATCH_RESULTS); + }); + + it('returns the correct max batch size when specified and need to fetch more than MAX_BATCH_SIZE', () => { + expect(calculateNextBatchSize({ maxResults: MAX_BATCH_RESULTS * 3, dataPointsFetched: 0 })).toBe(MAX_BATCH_RESULTS); + }); + + it('returns the correct max batch size when specified and need to fetch less than MAX_BATCH SIZE', () => { + expect(calculateNextBatchSize({ maxResults: MAX_BATCH_RESULTS / 2, dataPointsFetched: 0 })).toBe( + MAX_BATCH_RESULTS / 2 + ); + expect(calculateNextBatchSize({ maxResults: MAX_BATCH_RESULTS, dataPointsFetched: MAX_BATCH_RESULTS / 2 })).toBe( + MAX_BATCH_RESULTS / 2 + ); + }); +}); + +describe('shouldFetchNextBatch', () => { + it('returns true if next token exists and batch has no limit', () => { + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: NO_LIMIT_BATCH, dataPointsFetched: 0 })).toBe(true); + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: NO_LIMIT_BATCH, dataPointsFetched: 500000 })).toBe( + true + ); + }); + + it('returns true if next token exists and there is still data that needs to be fetched', () => { + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: 3000, dataPointsFetched: 0 })).toBe(true); + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: 10000, dataPointsFetched: 9999 })).toBe(true); + }); + + it('returns false if next token exists but data points have already been fetched', () => { + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: 3000, dataPointsFetched: 3000 })).toBe(false); + expect(shouldFetchNextBatch({ nextToken: '123', maxResults: 0, dataPointsFetched: 0 })).toBe(false); + }); + + it('returns false if next token does not exist', () => { + expect(shouldFetchNextBatch({ nextToken: undefined, maxResults: 3000, dataPointsFetched: 0 })).toBe(false); + }); +}); diff --git a/packages/source-iotsitewise/src/time-series-data/client/batch.ts b/packages/source-iotsitewise/src/time-series-data/client/batch.ts new file mode 100644 index 000000000..28e7ec3e7 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/client/batch.ts @@ -0,0 +1,82 @@ +// current maximum batch size when using batch APIs +export const MAX_BATCH_RESULTS = 4000; + +// current batch API entry limit +export const MAX_BATCH_ENTRIES = 16; + +// use -1 to represent a batch with no max result limit +export const NO_LIMIT_BATCH = -1; + +/** + * bucket entries by maxResults, chunk buckets if required. + * entries[] => [[entries, -1], [entries, 1000], [entries, 16]] + * + * @param entries + * @returns buckets: [BatchHistoricalEntry[], number | undefined][] + */ +export const createEntryBatches = (entries: T[]): [T[], number][] => { + const buckets: { [key: number]: T[] } = {}; + + entries.forEach((entry) => { + const maxEntryResults = entry.maxResults || NO_LIMIT_BATCH; + + if (buckets[maxEntryResults]) { + buckets[maxEntryResults] = buckets[maxEntryResults].concat([entry]); + } else { + buckets[maxEntryResults] = [entry]; + } + }); + + // chunk buckets that are larger than MAX_BATCH_ENTRIES + return Object.keys(buckets) + .map((key) => { + const maxEntryResults = Number(key); + const bucket = buckets[maxEntryResults]; + + return chunkBatch(bucket).map((chunk): [T[], number] => [ + chunk, + maxEntryResults === NO_LIMIT_BATCH ? NO_LIMIT_BATCH : chunk.length * maxEntryResults, + ]); + }) + .flat(); +}; + +/** + * calculate the required size of the next batch + */ +export const calculateNextBatchSize = ({ + maxResults, + dataPointsFetched, +}: { + maxResults: number; + dataPointsFetched: number; +}) => (maxResults === NO_LIMIT_BATCH ? MAX_BATCH_RESULTS : Math.min(maxResults - dataPointsFetched, MAX_BATCH_RESULTS)); + +/** + * check if batch still needs to be paginated. + */ +export const shouldFetchNextBatch = ({ + nextToken, + maxResults, + dataPointsFetched, +}: { + nextToken: string | undefined; + maxResults: number; + dataPointsFetched?: number; +}) => + !!nextToken && + (maxResults === NO_LIMIT_BATCH || + (dataPointsFetched !== null && dataPointsFetched !== undefined && dataPointsFetched < maxResults)); + +/** + * chunk batches by MAX_BATCH_ENTRIES + */ +const chunkBatch = (batch: T[]): T[][] => { + const chunks = []; + + for (let i = 0; i < batch.length; i += MAX_BATCH_ENTRIES) { + chunks.push(batch.slice(i, i + MAX_BATCH_ENTRIES)); + } + + return chunks; +}; diff --git a/packages/source-iotsitewise/src/time-series-data/client/batchGetAggregatedPropertyDataPoints.ts b/packages/source-iotsitewise/src/time-series-data/client/batchGetAggregatedPropertyDataPoints.ts new file mode 100644 index 000000000..00ea89192 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/client/batchGetAggregatedPropertyDataPoints.ts @@ -0,0 +1,184 @@ +import { + AggregateType, + BatchGetAssetPropertyAggregatesCommand, + BatchGetAssetPropertyAggregatesErrorEntry, + BatchGetAssetPropertyAggregatesSuccessEntry, + IoTSiteWiseClient, + TimeOrdering, +} from '@aws-sdk/client-iotsitewise'; +import { aggregateToDataPoint } from '../util/toDataPoint'; +import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange, parseDuration } from '@iot-app-kit/core'; +import { toSiteWiseAssetProperty } from '../util/dataStreamId'; +import { isDefined } from '../../common/predicates'; +import { AggregatedPropertyParams } from './client'; +import { createEntryBatches, calculateNextBatchSize, shouldFetchNextBatch } from './batch'; +import { RESOLUTION_TO_MS_MAPPING } from '../util/resolution'; + +type BatchAggregatedEntry = { + requestInformation: RequestInformationAndRange; + aggregateTypes: AggregateType[]; + maxResults?: number; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; + requestStart: Date; + requestEnd: Date; +}; + +type BatchEntryCallbackCache = { + [key: string]: { + onError: (entry: BatchGetAssetPropertyAggregatesErrorEntry) => void; + onSuccess: (entry: BatchGetAssetPropertyAggregatesSuccessEntry) => void; + }; +}; + +const sendRequest = ({ + client, + batch, + maxResults, + requestIndex, // used to create and regenerate (for paginating) a unique entryId + nextToken: prevToken, + dataPointsFetched = 0, // track number of data points fetched so far +}: { + client: IoTSiteWiseClient; + batch: BatchAggregatedEntry[]; + maxResults: number; + requestIndex: number; + nextToken?: string; + dataPointsFetched?: number; +}) => { + // callback cache makes it convenient to capture request data in a closure. + // the cache exposes methods that only require batch response entry as an argument. + const callbackCache: BatchEntryCallbackCache = {}; + + const batchSize = calculateNextBatchSize({ maxResults, dataPointsFetched }); + + client + .send( + new BatchGetAssetPropertyAggregatesCommand({ + entries: batch.map((entry, entryIndex) => { + const { requestInformation, aggregateTypes, onError, onSuccess, requestStart, requestEnd } = entry; + const { id, resolution } = requestInformation; + + // use 2D array indices as entryIDs to guarantee uniqueness + // entryId is used to map batch entries with the appropriate callback + const entryId = String(`${requestIndex}-${entryIndex}`); + + // save request entry data in functional closure. + callbackCache[entryId] = { + onError: ({ errorMessage: msg = 'batch aggregate error', errorCode: status }) => { + onError({ + id, + resolution: parseDuration(resolution), + error: { msg, status }, + }); + }, + onSuccess: ({ aggregatedValues }) => { + if (aggregatedValues) { + onSuccess( + [ + dataStreamFromSiteWise({ + ...toSiteWiseAssetProperty(id), + dataPoints: aggregatedValues + .map((aggregatedValue) => aggregateToDataPoint(aggregatedValue)) + .filter(isDefined), + resolution: RESOLUTION_TO_MS_MAPPING[resolution], + }), + ], + requestInformation, + requestStart, + requestEnd + ); + } + }, + }; + + // BatchGetAssetPropertyValueAggregatesEntry + return { + ...toSiteWiseAssetProperty(requestInformation.id), + aggregateTypes, + resolution, + startDate: requestStart, + endDate: requestEnd, + entryId, + timeOrdering: TimeOrdering.DESCENDING, + }; + }), + maxResults: batchSize, + nextToken: prevToken, + }) + ) + .then((response) => { + const { errorEntries, successEntries, nextToken } = response; + + // execute the correct callback for each entry + // empty entries and entries that don't exist in the cache are ignored. + // TODO: implement retries for retry-able batch errors + errorEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onError(entry)); + successEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onSuccess(entry)); + + // increment number of data points fetched + dataPointsFetched += batchSize; + + if (shouldFetchNextBatch({ nextToken, maxResults, dataPointsFetched })) { + sendRequest({ + client, + batch, + maxResults, + requestIndex, + nextToken, + dataPointsFetched, + }); + } + }); +}; + +const batchGetAggregatedPropertyDataPointsForProperty = ({ + client, + entries, +}: { + client: IoTSiteWiseClient; + entries: BatchAggregatedEntry[]; +}) => + createEntryBatches(entries) + .filter((batch) => batch.length > 0) // filter out empty batches + .map(([batch, maxResults], requestIndex) => sendRequest({ client, batch, maxResults, requestIndex })); + +export const batchGetAggregatedPropertyDataPoints = ({ + params, + client, +}: { + params: AggregatedPropertyParams[]; + client: IoTSiteWiseClient; +}) => { + const entries: BatchAggregatedEntry[] = []; + + // fan out params into individual entries, handling fetchMostRecentBeforeStart + params.forEach(({ requestInformations, maxResults, onSuccess, onError, aggregateTypes }) => { + requestInformations + .filter(({ resolution }) => resolution !== '0') + .forEach((requestInformation) => { + const { fetchMostRecentBeforeStart, start, end } = requestInformation; + + entries.push({ + requestInformation, + aggregateTypes, + maxResults: fetchMostRecentBeforeStart ? 1 : maxResults, + onSuccess, + onError, + requestStart: fetchMostRecentBeforeStart ? new Date(0, 0, 0) : start, + requestEnd: fetchMostRecentBeforeStart ? start : end, + }); + }); + }); + + // sort entries to ensure earliest data is fetched first because batch API has a property limit + entries.sort((a, b) => b.requestInformation.start.getTime() - a.requestInformation.start.getTime()); + + if (entries.length > 0) { + batchGetAggregatedPropertyDataPointsForProperty({ + entries, + client, + }); + } +}; diff --git a/packages/source-iotsitewise/src/time-series-data/client/batchGetHistoricalPropertyDataPoints.ts b/packages/source-iotsitewise/src/time-series-data/client/batchGetHistoricalPropertyDataPoints.ts new file mode 100644 index 000000000..357e4c68f --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/client/batchGetHistoricalPropertyDataPoints.ts @@ -0,0 +1,177 @@ +import { + BatchGetAssetPropertyValueHistoryCommand, + BatchGetAssetPropertyValueHistoryErrorEntry, + BatchGetAssetPropertyValueHistorySuccessEntry, + IoTSiteWiseClient, + TimeOrdering, +} from '@aws-sdk/client-iotsitewise'; +import { toDataPoint } from '../util/toDataPoint'; +import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; +import { toSiteWiseAssetProperty } from '../util/dataStreamId'; +import { isDefined } from '../../common/predicates'; +import { HistoricalPropertyParams } from './client'; +import { createEntryBatches, calculateNextBatchSize, shouldFetchNextBatch } from './batch'; + +type BatchHistoricalEntry = { + requestInformation: RequestInformationAndRange; + maxResults?: number; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; + requestStart: Date; + requestEnd: Date; +}; + +type BatchEntryCallbackCache = { + [key: string]: { + onError: (entry: BatchGetAssetPropertyValueHistoryErrorEntry) => void; + onSuccess: (entry: BatchGetAssetPropertyValueHistorySuccessEntry) => void; + }; +}; + +const sendRequest = ({ + client, + batch, + maxResults, + requestIndex, // used to create and regenerate (for paginating) a unique entryId + nextToken: prevToken, + dataPointsFetched = 0, // track number of data points fetched so far +}: { + client: IoTSiteWiseClient; + batch: BatchHistoricalEntry[]; + maxResults: number; + requestIndex: number; + nextToken?: string; + dataPointsFetched?: number; +}) => { + // callback cache makes it convenient to capture request data in a closure. + // the cache exposes methods that only require batch response entry as an argument. + const callbackCache: BatchEntryCallbackCache = {}; + + const batchSize = calculateNextBatchSize({ maxResults, dataPointsFetched }); + + client + .send( + new BatchGetAssetPropertyValueHistoryCommand({ + entries: batch.map((entry, entryIndex) => { + const { requestInformation, onError, onSuccess, requestStart, requestEnd } = entry; + const { id } = requestInformation; + + // use 2D array indices as entryIDs to guarantee uniqueness + // entryId is used to map batch entries with the appropriate callback + const entryId = String(`${requestIndex}-${entryIndex}`); + + // save request entry data in functional closure. + callbackCache[entryId] = { + onError: ({ errorMessage: msg = 'batch historical error', errorCode: status }) => { + onError({ + id, + resolution: 0, + error: { msg, status }, + }); + }, + onSuccess: ({ assetPropertyValueHistory }) => { + if (assetPropertyValueHistory) { + onSuccess( + [ + dataStreamFromSiteWise({ + ...toSiteWiseAssetProperty(id), + dataPoints: assetPropertyValueHistory + .map((assetPropertyValue) => toDataPoint(assetPropertyValue)) + .filter(isDefined), + }), + ], + requestInformation, + requestStart, + requestEnd + ); + } + }, + }; + + // BatchGetAssetPropertyValueHistoryEntry + return { + ...toSiteWiseAssetProperty(requestInformation.id), + startDate: requestStart, + endDate: requestEnd, + entryId, + timeOrdering: TimeOrdering.DESCENDING, + }; + }), + maxResults: batchSize, + nextToken: prevToken, + }) + ) + .then((response) => { + const { errorEntries, successEntries, nextToken } = response; + + // execute the correct callback for each entry + // empty entries and entries that don't exist in the cache are ignored. + // TODO: implement retries for retry-able batch errors + errorEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onError(entry)); + successEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onSuccess(entry)); + + // increment number of data points fetched + dataPointsFetched += batchSize; + + if (shouldFetchNextBatch({ nextToken, maxResults, dataPointsFetched })) { + sendRequest({ + client, + batch, + maxResults, + requestIndex, + nextToken, + dataPointsFetched, + }); + } + }); +}; + +const batchGetHistoricalPropertyDataPointsForProperty = ({ + client, + entries, +}: { + client: IoTSiteWiseClient; + entries: BatchHistoricalEntry[]; +}) => + createEntryBatches(entries) + .filter((batch) => batch.length > 0) // filter out empty batches + .map(([batch, maxResults], requestIndex) => sendRequest({ client, batch, maxResults, requestIndex })); + +export const batchGetHistoricalPropertyDataPoints = ({ + params, + client, +}: { + params: HistoricalPropertyParams[]; + client: IoTSiteWiseClient; +}) => { + const entries: BatchHistoricalEntry[] = []; + + // fan out params into individual entries, handling fetchMostRecentBeforeStart + params.forEach(({ requestInformations, maxResults, onSuccess, onError }) => { + requestInformations + .filter(({ resolution }) => resolution === '0') + .forEach((requestInformation) => { + const { fetchMostRecentBeforeStart, start, end } = requestInformation; + + entries.push({ + requestInformation, + maxResults: fetchMostRecentBeforeStart ? 1 : maxResults, + onSuccess, + onError, + requestStart: fetchMostRecentBeforeStart ? new Date(0, 0, 0) : start, + requestEnd: fetchMostRecentBeforeStart ? start : end, + }); + }); + }); + + // sort entries to ensure earliest data is fetched first because batch API has a property limit + entries.sort((a, b) => b.requestInformation.start.getTime() - a.requestInformation.start.getTime()); + + if (entries.length > 0) { + batchGetHistoricalPropertyDataPointsForProperty({ + entries, + client, + }); + } +}; diff --git a/packages/source-iotsitewise/src/time-series-data/client/batchGetLatestPropertyDataPoints.ts b/packages/source-iotsitewise/src/time-series-data/client/batchGetLatestPropertyDataPoints.ts new file mode 100644 index 000000000..d732070a4 --- /dev/null +++ b/packages/source-iotsitewise/src/time-series-data/client/batchGetLatestPropertyDataPoints.ts @@ -0,0 +1,158 @@ +import { + BatchGetAssetPropertyValueCommand, + BatchGetAssetPropertyValueErrorEntry, + BatchGetAssetPropertyValueSuccessEntry, + IoTSiteWiseClient, +} from '@aws-sdk/client-iotsitewise'; +import { toDataPoint } from '../util/toDataPoint'; +import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; +import { toSiteWiseAssetProperty } from '../util/dataStreamId'; +import { isDefined } from '../../common/predicates'; +import { LatestPropertyParams } from './client'; +import { createEntryBatches, shouldFetchNextBatch, NO_LIMIT_BATCH } from './batch'; + +type BatchLatestEntry = { + requestInformation: RequestInformationAndRange; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; + requestStart: Date; + requestEnd: Date; + maxResults?: number; +}; + +type BatchEntryCallbackCache = { + [key: string]: { + onError: (entry: BatchGetAssetPropertyValueErrorEntry) => void; + onSuccess: (entry: BatchGetAssetPropertyValueSuccessEntry) => void; + }; +}; + +/** + * This API currently does not paginate and nextToken will always be null. However, once + * Hybrid Query (hot/cold) is supported it's possible that the API will not return + * all entries in a single API request. Supporting nextToken future-proofs this implementation. + */ +const sendRequest = ({ + client, + batch, + requestIndex, // used to create and regenerate (for paginating) a unique entryId + nextToken: prevToken, +}: { + client: IoTSiteWiseClient; + batch: BatchLatestEntry[]; + requestIndex: number; + nextToken?: string; +}) => { + // callback cache makes it convenient to capture request data in a closure. + // the cache exposes methods that only require batch response entry as an argument. + const callbackCache: BatchEntryCallbackCache = {}; + + client + .send( + new BatchGetAssetPropertyValueCommand({ + entries: batch.map((entry, entryIndex) => { + const { requestInformation, onError, onSuccess, requestStart, requestEnd } = entry; + const { id } = requestInformation; + + // use 2D array indices as entryIDs to guarantee uniqueness + // entryId is used to map batch entries with the appropriate callback + const entryId = String(`${requestIndex}-${entryIndex}`); + + // save request entry data in functional closure. + callbackCache[entryId] = { + onError: ({ errorMessage: msg = 'batch latest value error', errorCode: status }) => { + onError({ + id, + resolution: 0, // currently supports raw data only + error: { msg, status }, + }); + }, + onSuccess: ({ assetPropertyValue }) => { + if (assetPropertyValue) { + const dataStream = dataStreamFromSiteWise({ + ...toSiteWiseAssetProperty(id), + dataPoints: [toDataPoint(assetPropertyValue)].filter(isDefined), + }); + + onSuccess([dataStream], requestInformation, requestStart, requestEnd); + } + }, + }; + + // BatchGetAssetPropertyValueEntry + return { + ...toSiteWiseAssetProperty(requestInformation.id), + entryId, + }; + }), + nextToken: prevToken, + }) + ) + .then((response) => { + const { errorEntries, successEntries, nextToken } = response; + + // execute the correct callback for each entry + // empty entries and entries that don't exist in the cache are ignored. + // TODO: implement retries for retry-able batch errors + errorEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onError(entry)); + successEntries?.forEach((entry) => entry.entryId && callbackCache[entry.entryId]?.onSuccess(entry)); + + if (shouldFetchNextBatch({ nextToken, maxResults: NO_LIMIT_BATCH })) { + sendRequest({ + client, + batch, + requestIndex, + nextToken, + }); + } + }); +}; + +const batchGetLatestPropertyDataPointsForProperty = ({ + client, + entries, +}: { + client: IoTSiteWiseClient; + entries: BatchLatestEntry[]; +}) => + createEntryBatches(entries) + .filter((batch) => batch.length > 0) // filter out empty batches + .map(([batch], requestIndex) => sendRequest({ client, batch, requestIndex })); + +export const batchGetLatestPropertyDataPoints = ({ + params, + client, +}: { + params: LatestPropertyParams[]; + client: IoTSiteWiseClient; +}) => { + const entries: BatchLatestEntry[] = []; + + // fan out params into individual entries, handling fetchMostRecentBeforeStart + params.forEach(({ requestInformations, onSuccess, onError }) => { + requestInformations + .filter(({ resolution, fetchMostRecentBeforeEnd }) => resolution === '0' && fetchMostRecentBeforeEnd) + .forEach((requestInformation) => { + const { end } = requestInformation; + + entries.push({ + requestInformation, + onSuccess, + onError, + requestStart: new Date(0, 0, 0), // caching will be adjusted based on the returned data point + requestEnd: end, + }); + }); + }); + + // sort entries to ensure earliest data is fetched first because batch API has a property limit + entries.sort((a, b) => b.requestInformation.start.getTime() - a.requestInformation.start.getTime()); + + if (entries.length > 0) { + batchGetLatestPropertyDataPointsForProperty({ + entries, + client, + }); + } +}; diff --git a/packages/source-iotsitewise/src/time-series-data/client/client.spec.ts b/packages/source-iotsitewise/src/time-series-data/client/client.spec.ts index 24e6243d7..72ca91841 100644 --- a/packages/source-iotsitewise/src/time-series-data/client/client.spec.ts +++ b/packages/source-iotsitewise/src/time-series-data/client/client.spec.ts @@ -1,13 +1,17 @@ -import { AggregateType, ResourceNotFoundException } from '@aws-sdk/client-iotsitewise'; +import { AggregateType } from '@aws-sdk/client-iotsitewise'; import { SiteWiseClient } from './client'; import { createMockSiteWiseSDK } from '../../__mocks__/iotsitewiseSDK'; import { - ASSET_PROPERTY_DOUBLE_VALUE, - ASSET_PROPERTY_VALUE_HISTORY, - AGGREGATE_VALUES, + BATCH_ASSET_PROPERTY_DOUBLE_VALUE, + BATCH_ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_ERROR, + BATCH_ASSET_PROPERTY_ERROR_ENTRY, + BATCH_ASSET_PROPERTY_AGGREGATES, } from '../../__mocks__/assetPropertyValue'; import { toId } from '../util/dataStreamId'; import { HOUR_IN_MS } from '@iot-app-kit/core'; +import { MAX_BATCH_RESULTS } from './batch'; +import flushPromises from 'flush-promises'; it('initializes', () => { expect(() => new SiteWiseClient(createMockSiteWiseSDK({}))).not.toThrowError(); @@ -15,21 +19,14 @@ it('initializes', () => { describe('getHistoricalPropertyDataPoints', () => { it('calls onError on failure', async () => { - const ERR: Partial = { - name: 'ResourceNotFoundException', - message: 'assetId 1 not found', - $metadata: { - httpStatusCode: 404, - }, - }; - const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(ERR); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_ERROR); const assetId = 'some-asset-id'; const propertyId = 'some-property-id'; const onSuccess = jest.fn(); const onError = jest.fn(); - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyValueHistory })); + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory })); const startDate = new Date(2000, 0, 0); const endDate = new Date(2001, 0, 0); @@ -49,49 +46,93 @@ describe('getHistoricalPropertyDataPoints', () => { expect(onError).toBeCalledWith( expect.objectContaining({ error: { - msg: ERR.message, - type: ERR.name, - status: ERR.$metadata?.httpStatusCode, + msg: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorMessage, + status: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorCode, }, }) ); }); - it('returns data point on success', async () => { - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); - const assetId = 'some-asset-id'; - const propertyId = 'some-property-id'; + it('batches and paginates', async () => { + const batchGetAssetPropertyValueHistory = jest + .fn() + .mockResolvedValue({ ...BATCH_ASSET_PROPERTY_VALUE_HISTORY, nextToken: 'nextToken' }); + const assetId1 = 'some-asset-id-1'; + const propertyId1 = 'some-property-id-1'; + + const assetId2 = 'some-asset-id-2'; + const propertyId2 = 'some-property-id-2'; const onSuccess = jest.fn(); const onError = jest.fn(); - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyValueHistory })); + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory })); const startDate = new Date(2000, 0, 0); const endDate = new Date(2001, 0, 0); - const requestInformations = [ - { - id: toId({ assetId, propertyId }), - start: startDate, - end: endDate, - resolution: '0', - fetchFromStartToEnd: true, - }, - ]; + const requestInformation1 = { + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + start: startDate, + end: endDate, + resolution: '0', + fetchFromStartToEnd: true, + }; - await client.getHistoricalPropertyDataPoints({ requestInformations, onSuccess, onError }); + const requestInformation2 = { + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + start: startDate, + end: endDate, + resolution: '0', + fetchFromStartToEnd: true, + }; - expect(getAssetPropertyValueHistory).toBeCalledWith( - expect.objectContaining({ assetId, propertyId, startDate, endDate }) - ); + // batches requests that are sent on a single frame + client.getHistoricalPropertyDataPoints({ + requestInformations: [requestInformation1], + onSuccess, + onError, + maxResults: MAX_BATCH_RESULTS, // ensure pagination happens exactly once + }); + client.getHistoricalPropertyDataPoints({ + requestInformations: [requestInformation2], + onSuccess, + onError, + maxResults: MAX_BATCH_RESULTS, // ensure pagination happens exactly once + }); + + await flushPromises(); + + // process the batch and paginate once + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(2); + + const batchHistoryParams = [ + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: assetId1, + propertyId: propertyId1, + startDate, + endDate, + }), + expect.objectContaining({ + assetId: assetId2, + propertyId: propertyId2, + startDate, + endDate, + }), + ]), + }), + ]; + + expect(batchGetAssetPropertyValueHistory.mock.calls).toEqual([batchHistoryParams, batchHistoryParams]); expect(onError).not.toBeCalled(); - expect(onSuccess).toBeCalledWith( - [ + const onSuccessParams1 = [ + expect.arrayContaining([ expect.objectContaining({ - id: toId({ assetId, propertyId }), + id: toId({ assetId: assetId1, propertyId: propertyId1 }), data: [ { x: 1000099, @@ -103,112 +144,213 @@ describe('getHistoricalPropertyDataPoints', () => { }, ], }), - ], + ]), expect.objectContaining({ - id: toId({ assetId, propertyId }), + id: toId({ assetId: assetId1, propertyId: propertyId1 }), start: startDate, end: endDate, resolution: '0', fetchFromStartToEnd: true, }), startDate, - endDate - ); + endDate, + ]; + + const onSuccessParams2 = [ + expect.arrayContaining([ + expect.objectContaining({ + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + data: [ + { + x: 1000099, + y: 10.123, + }, + ], + }), + ]), + expect.objectContaining({ + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + start: startDate, + end: endDate, + resolution: '0', + fetchFromStartToEnd: true, + }), + startDate, + endDate, + ]; + + // call onSuccess for each entry in each batch + expect(onSuccess).toBeCalledTimes(4); + expect(onSuccess.mock.calls).toEqual([onSuccessParams1, onSuccessParams2, onSuccessParams1, onSuccessParams2]); }); }); describe('getLatestPropertyDataPoint', () => { - it.skip('returns data point on success', async () => { - const getAssetPropertyValue = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); + it('calls onError when error occurs', async () => { + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_ERROR); const assetId = 'some-asset-id'; const propertyId = 'some-property-id'; + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValue })); + const onSuccess = jest.fn(); const onError = jest.fn(); - const start = new Date(1000099); - const end = new Date(); - const requestInformations = [ { id: toId({ assetId, propertyId }), - start, - end, + start: new Date(), + end: new Date(), resolution: '0', fetchMostRecentBeforeEnd: true, }, ]; - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyValue })); - await client.getLatestPropertyDataPoint({ onSuccess, onError, requestInformations }); - expect(getAssetPropertyValue).toBeCalledWith({ assetId, propertyId }); + + expect(onError).toBeCalledWith( + expect.objectContaining({ + error: { + msg: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorMessage, + status: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorCode, + }, + }) + ); + }); + + it('batches', async () => { + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); + const assetId1 = 'some-asset-id-1'; + const propertyId1 = 'some-property-id-1'; + + const assetId2 = 'some-asset-id-2'; + const propertyId2 = 'some-property-id-2'; + + const onSuccess = jest.fn(); + const onError = jest.fn(); + + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValue })); + + const startDate = new Date(2000, 0, 0); + const endDate = new Date(2001, 0, 0); + const resolution = '0'; + + const requestInformation1 = { + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + start: startDate, + end: endDate, + resolution, + fetchMostRecentBeforeEnd: true, + }; + + const requestInformation2 = { + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + start: startDate, + end: endDate, + resolution, + fetchMostRecentBeforeEnd: true, + }; + + // batches requests that are sent on a single frame + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation1], + onSuccess, + onError, + }); + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation2], + onSuccess, + onError, + }); + + await flushPromises(); + + // process the batch and paginate once + expect(batchGetAssetPropertyValue).toBeCalledTimes(1); + + const batchLatestParams = [ + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: assetId1, + propertyId: propertyId1, + }), + expect.objectContaining({ + assetId: assetId2, + propertyId: propertyId2, + }), + ]), + }), + ]; + + expect(batchGetAssetPropertyValue.mock.calls).toEqual([batchLatestParams]); expect(onError).not.toBeCalled(); - expect(onSuccess).toBeCalledWith( - [ + const onSuccessParams1 = [ + expect.arrayContaining([ expect.objectContaining({ - id: toId({ assetId, propertyId }), + id: toId({ assetId: assetId1, propertyId: propertyId1 }), data: [ { - y: ASSET_PROPERTY_DOUBLE_VALUE.propertyValue?.value?.doubleValue, x: 1000099, + y: 10.123, }, ], + resolution: 0, }), - ], + ]), expect.objectContaining({ - id: toId({ assetId, propertyId }), - start, - end, - resolution: '0', + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + start: startDate, + end: endDate, + resolution, fetchMostRecentBeforeEnd: true, }), - start, - end - ); - }); - - it('calls onError when error occurs', async () => { - const ERR = new Error('some scary error'); - const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR); - const assetId = 'some-asset-id'; - const propertyId = 'some-property-id'; - - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyValue })); - - const onSuccess = jest.fn(); - const onError = jest.fn(); + new Date(0, 0, 0), + endDate, + ]; - const requestInformations = [ - { - id: toId({ assetId, propertyId }), - start: new Date(), - end: new Date(), - resolution: '0', + const onSuccessParams2 = [ + expect.arrayContaining([ + expect.objectContaining({ + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + data: [ + { + x: 1000099, + y: 10.123, + }, + ], + resolution: 0, + }), + ]), + expect.objectContaining({ + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + start: startDate, + end: endDate, + resolution, fetchMostRecentBeforeEnd: true, - }, + }), + new Date(0, 0, 0), + endDate, ]; - await client.getLatestPropertyDataPoint({ onSuccess, onError, requestInformations }); - - expect(onSuccess).not.toBeCalled(); - expect(onError).toBeCalled(); + // call onSuccess for each entry in the batch + expect(onSuccess).toBeCalledTimes(2); + expect(onSuccess.mock.calls).toEqual([onSuccessParams1, onSuccessParams2]); }); }); describe('getAggregatedPropertyDataPoints', () => { it('calls onError on failure', async () => { - const ERR = new Error('some error'); - const getAssetPropertyAggregates = jest.fn().mockRejectedValue(ERR); + const batchGetAssetPropertyAggregates = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_ERROR); const assetId = 'some-asset-id'; const propertyId = 'some-property-id'; const onSuccess = jest.fn(); const onError = jest.fn(); - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyAggregates })); + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyAggregates })); const startDate = new Date(2000, 0, 0); const endDate = new Date(2001, 0, 0); @@ -225,59 +367,137 @@ describe('getAggregatedPropertyDataPoints', () => { }, ]; - await client.getAggregatedPropertyDataPoints({ - requestInformations, - onSuccess, - onError, - aggregateTypes, - }); + await client.getAggregatedPropertyDataPoints({ requestInformations, onSuccess, onError, aggregateTypes }); - expect(onError).toBeCalled(); + expect(onError).toBeCalledWith( + expect.objectContaining({ + error: { + msg: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorMessage, + status: BATCH_ASSET_PROPERTY_ERROR_ENTRY.errorCode, + }, + }) + ); }); - it('returns data point on success', async () => { - const assetId = 'some-asset-id'; - const propertyId = 'some-property-id'; + it('batches and paginates', async () => { + const batchGetAssetPropertyAggregates = jest + .fn() + .mockResolvedValue({ ...BATCH_ASSET_PROPERTY_AGGREGATES, nextToken: 'nextToken' }); + const assetId1 = 'some-asset-id-1'; + const propertyId1 = 'some-property-id-1'; + + const assetId2 = 'some-asset-id-2'; + const propertyId2 = 'some-property-id-2'; const onSuccess = jest.fn(); const onError = jest.fn(); - const getAssetPropertyAggregates = jest.fn().mockResolvedValue(AGGREGATE_VALUES); - const client = new SiteWiseClient(createMockSiteWiseSDK({ getAssetPropertyAggregates })); + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyAggregates })); const startDate = new Date(2000, 0, 0); const endDate = new Date(2001, 0, 0); const resolution = '1h'; const aggregateTypes = [AggregateType.AVERAGE]; - const requestInformations = [ - { - id: toId({ assetId, propertyId }), - start: startDate, - end: endDate, - resolution, - fetchFromStartToEnd: true, - }, - ]; + const requestInformation1 = { + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + start: startDate, + end: endDate, + resolution, + fetchFromStartToEnd: true, + }; - await client.getAggregatedPropertyDataPoints({ - requestInformations, + const requestInformation2 = { + id: toId({ assetId: assetId2, propertyId: propertyId2 }), + start: startDate, + end: endDate, + resolution, + fetchFromStartToEnd: true, + }; + + // batches requests that are sent on a single frame + client.getAggregatedPropertyDataPoints({ + requestInformations: [requestInformation1], onSuccess, onError, aggregateTypes, + maxResults: MAX_BATCH_RESULTS, // ensure pagination happens exactly once + }); + client.getAggregatedPropertyDataPoints({ + requestInformations: [requestInformation2], + onSuccess, + onError, + aggregateTypes, + maxResults: MAX_BATCH_RESULTS, // ensure pagination happens exactly once }); - expect(getAssetPropertyAggregates).toBeCalledWith( - expect.objectContaining({ assetId, propertyId, startDate, endDate, resolution, aggregateTypes }) - ); + await flushPromises(); + + // process the batch and paginate once + expect(batchGetAssetPropertyAggregates).toBeCalledTimes(2); + + const batchHistoryParams = [ + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: assetId1, + propertyId: propertyId1, + startDate, + endDate, + }), + expect.objectContaining({ + assetId: assetId2, + propertyId: propertyId2, + startDate, + endDate, + }), + ]), + }), + ]; + + expect(batchGetAssetPropertyAggregates.mock.calls).toEqual([batchHistoryParams, batchHistoryParams]); expect(onError).not.toBeCalled(); - expect(onSuccess).toBeCalledWith( - [ + const onSuccessParams1 = [ + expect.arrayContaining([ expect.objectContaining({ - id: toId({ assetId, propertyId }), + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + aggregates: { + [HOUR_IN_MS]: [ + { + x: 946602000000, + y: 5, + }, + { + x: 946605600000, + y: 7, + }, + { + x: 946609200000, + y: 10, + }, + ], + }, data: [], + resolution: HOUR_IN_MS, + }), + ]), + expect.objectContaining({ + id: toId({ assetId: assetId1, propertyId: propertyId1 }), + start: startDate, + end: endDate, + resolution, + fetchFromStartToEnd: true, + }), + startDate, + endDate, + ]; + + const onSuccessParams2 = [ + expect.arrayContaining([ + expect.objectContaining({ + id: toId({ assetId: assetId2, propertyId: propertyId2 }), aggregates: { [HOUR_IN_MS]: [ { @@ -294,17 +514,166 @@ describe('getAggregatedPropertyDataPoints', () => { }, ], }, + data: [], + resolution: HOUR_IN_MS, }), - ], + ]), expect.objectContaining({ - id: toId({ assetId, propertyId }), + id: toId({ assetId: assetId2, propertyId: propertyId2 }), start: startDate, end: endDate, resolution, fetchFromStartToEnd: true, }), startDate, - endDate - ); + endDate, + ]; + + // call onSuccess for each entry in each batch + expect(onSuccess).toBeCalledTimes(4); + expect(onSuccess.mock.calls).toEqual([onSuccessParams1, onSuccessParams2, onSuccessParams1, onSuccessParams2]); + }); +}); + +describe('batch duration', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('batches requests over a single frame', async () => { + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); + const assetId = 'some-asset-id'; + const propertyId = 'some-property-id'; + + const onSuccess = jest.fn(); + const onError = jest.fn(); + + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValue })); + + const startDate = new Date(2000, 0, 0); + const endDate = new Date(2001, 0, 0); + const resolution = '0'; + + const requestInformation = { + id: toId({ assetId, propertyId }), + start: startDate, + end: endDate, + resolution, + fetchMostRecentBeforeEnd: true, + }; + + // single frame + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); // clear promise queue + jest.advanceTimersByTime(0); // ensure latest requests are enqueued + + // process the batch + expect(batchGetAssetPropertyValue).toBeCalledTimes(1); + + // now split into two frames + batchGetAssetPropertyValue.mockClear(); + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); // clear promise queue + jest.advanceTimersByTime(0); // ensure latest requests are enqueued + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); // clear promise queue + jest.advanceTimersByTime(0); // ensure latest requests are enqueued + + expect(batchGetAssetPropertyValue).toBeCalledTimes(2); + }); + + it('batches requests over a specified duration', async () => { + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); + const assetId = 'some-asset-id'; + const propertyId = 'some-property-id'; + + const onSuccess = jest.fn(); + const onError = jest.fn(); + + const client = new SiteWiseClient(createMockSiteWiseSDK({ batchGetAssetPropertyValue }), { batchDuration: 100 }); + + const startDate = new Date(2000, 0, 0); + const endDate = new Date(2001, 0, 0); + const resolution = '0'; + + const requestInformation = { + id: toId({ assetId, propertyId }), + start: startDate, + end: endDate, + resolution, + fetchMostRecentBeforeEnd: true, + }; + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); // clear promise queue + jest.advanceTimersByTime(50); // ensure latest requests are enqueued but not outside of batch window + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); // clear promise queue + jest.advanceTimersByTime(100); // ensure latest requests are enqueued and outside of batch window + + await flushPromises(); + + // process the batch and paginate once + expect(batchGetAssetPropertyValue).toBeCalledTimes(1); + + // now split into two separate batch windows + batchGetAssetPropertyValue.mockClear(); + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); + jest.advanceTimersByTime(150); // ensure latest requests are enqueued and outside of batch window + + client.getLatestPropertyDataPoint({ + requestInformations: [requestInformation], + onSuccess, + onError, + }); + + await flushPromises(); + jest.advanceTimersByTime(150); // ensure latest requests are enqueued and outside of batch window + + expect(batchGetAssetPropertyValue).toBeCalledTimes(2); }); }); diff --git a/packages/source-iotsitewise/src/time-series-data/client/client.ts b/packages/source-iotsitewise/src/time-series-data/client/client.ts index a4a2c178c..f65880bab 100644 --- a/packages/source-iotsitewise/src/time-series-data/client/client.ts +++ b/packages/source-iotsitewise/src/time-series-data/client/client.ts @@ -1,46 +1,116 @@ +import DataLoader from 'dataloader'; import { IoTSiteWiseClient, AggregateType } from '@aws-sdk/client-iotsitewise'; -import { getLatestPropertyDataPoint } from './getLatestPropertyDataPoint'; -import { getHistoricalPropertyDataPoints } from './getHistoricalPropertyDataPoints'; -import { getAggregatedPropertyDataPoints } from './getAggregatedPropertyDataPoints'; +import { batchGetHistoricalPropertyDataPoints } from './batchGetHistoricalPropertyDataPoints'; import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; +import { batchGetAggregatedPropertyDataPoints } from './batchGetAggregatedPropertyDataPoints'; +import { batchGetLatestPropertyDataPoints } from './batchGetLatestPropertyDataPoints'; +import { SiteWiseDataSourceSettings } from '../types'; +import { getHistoricalPropertyDataPoints } from './legacy/getHistoricalPropertyDataPoints'; +import { getAggregatedPropertyDataPoints } from './legacy/getAggregatedPropertyDataPoints'; +import { getLatestPropertyDataPoint } from './legacy/getLatestPropertyDataPoint'; + +export type LatestPropertyParams = { + requestInformations: RequestInformationAndRange[]; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; +}; + +export type HistoricalPropertyParams = { + requestInformations: RequestInformationAndRange[]; + maxResults?: number; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; +}; + +export type AggregatedPropertyParams = { + requestInformations: RequestInformationAndRange[]; + aggregateTypes: AggregateType[]; + maxResults?: number; + onError: ErrorCallback; + onSuccess: OnSuccessCallback; +}; export class SiteWiseClient { private siteWiseSdk: IoTSiteWiseClient; + private settings: SiteWiseDataSourceSettings; + + private latestPropertyDataLoader: DataLoader; + private historicalPropertyDataLoader: DataLoader; + private aggregatedPropertyDataLoader: DataLoader; - constructor(siteWiseSdk: IoTSiteWiseClient) { + constructor(siteWiseSdk: IoTSiteWiseClient, settings: SiteWiseDataSourceSettings = {}) { this.siteWiseSdk = siteWiseSdk; + this.settings = settings; + this.instantiateDataLoaders(); + } + + /** + * Instantiate batch data loaders for latest, historical, and aggregated data. + * by default, data loaders will schedule batches for each frame of execution which ensures + * no additional latency when capturing many related requests in a single batch. + */ + private instantiateDataLoaders() { + this.latestPropertyDataLoader = new DataLoader( + async (keys) => { + batchGetLatestPropertyDataPoints({ params: keys.flat(), client: this.siteWiseSdk }); + return keys.map(() => undefined); // values are updated in data cache and don't need to be rebroadcast + }, + { + batchScheduleFn: this.settings.batchDuration + ? (callback) => setTimeout(callback, this.settings.batchDuration) + : undefined, + } + ); + + this.historicalPropertyDataLoader = new DataLoader( + async (keys) => { + batchGetHistoricalPropertyDataPoints({ params: keys.flat(), client: this.siteWiseSdk }); + return keys.map(() => undefined); + }, + { + batchScheduleFn: this.settings.batchDuration + ? (callback) => setTimeout(callback, this.settings.batchDuration) + : undefined, + } + ); + + this.aggregatedPropertyDataLoader = new DataLoader( + async (keys) => { + batchGetAggregatedPropertyDataPoints({ params: keys.flat(), client: this.siteWiseSdk }); + return keys.map(() => undefined); + }, + { + batchScheduleFn: this.settings.batchDuration + ? (callback) => setTimeout(callback, this.settings.batchDuration) + : undefined, + } + ); } - getLatestPropertyDataPoint(options: { - requestInformations: RequestInformationAndRange[]; - onSuccess: OnSuccessCallback; - onError: ErrorCallback; - }): Promise { - return getLatestPropertyDataPoint({ client: this.siteWiseSdk, ...options }); + getLatestPropertyDataPoint(params: LatestPropertyParams): Promise { + if (this.settings.legacyAPI) { + return getLatestPropertyDataPoint({ client: this.siteWiseSdk, ...params }); + } + return this.latestPropertyDataLoader.load(params); } - getHistoricalPropertyDataPoints(options: { - requestInformations: RequestInformationAndRange[]; - maxResults?: number; - onError: ErrorCallback; - onSuccess: OnSuccessCallback; - }): Promise { - return getHistoricalPropertyDataPoints({ - client: this.siteWiseSdk, - ...options, - }); + getHistoricalPropertyDataPoints(params: HistoricalPropertyParams): Promise { + if (this.settings.legacyAPI) { + return getHistoricalPropertyDataPoints({ + client: this.siteWiseSdk, + ...params, + }); + } + return this.historicalPropertyDataLoader.load(params); } - getAggregatedPropertyDataPoints(options: { - requestInformations: RequestInformationAndRange[]; - aggregateTypes: AggregateType[]; - maxResults?: number; - onError: ErrorCallback; - onSuccess: OnSuccessCallback; - }): Promise { - return getAggregatedPropertyDataPoints({ - client: this.siteWiseSdk, - ...options, - }); + getAggregatedPropertyDataPoints(params: AggregatedPropertyParams): Promise { + if (this.settings.legacyAPI) { + return getAggregatedPropertyDataPoints({ + client: this.siteWiseSdk, + ...params, + }); + } + return this.aggregatedPropertyDataLoader.load(params); } } diff --git a/packages/source-iotsitewise/src/time-series-data/client/getAggregatedPropertyDataPoints.ts b/packages/source-iotsitewise/src/time-series-data/client/legacy/getAggregatedPropertyDataPoints.ts similarity index 90% rename from packages/source-iotsitewise/src/time-series-data/client/getAggregatedPropertyDataPoints.ts rename to packages/source-iotsitewise/src/time-series-data/client/legacy/getAggregatedPropertyDataPoints.ts index 50095b91f..8c1899082 100644 --- a/packages/source-iotsitewise/src/time-series-data/client/getAggregatedPropertyDataPoints.ts +++ b/packages/source-iotsitewise/src/time-series-data/client/legacy/getAggregatedPropertyDataPoints.ts @@ -4,13 +4,13 @@ import { TimeOrdering, AggregateType, } from '@aws-sdk/client-iotsitewise'; -import { AssetId, AssetPropertyId } from '../types'; -import { aggregateToDataPoint } from '../util/toDataPoint'; -import { RESOLUTION_TO_MS_MAPPING } from '../util/resolution'; -import { toId, toSiteWiseAssetProperty } from '../util/dataStreamId'; +import { AssetId, AssetPropertyId } from '../../types'; +import { aggregateToDataPoint } from '../../util/toDataPoint'; +import { RESOLUTION_TO_MS_MAPPING } from '../../util/resolution'; +import { toId, toSiteWiseAssetProperty } from '../../util/dataStreamId'; import { parseDuration, OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; -import { isDefined } from '../../common/predicates'; -import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { isDefined } from '../../../common/predicates'; +import { dataStreamFromSiteWise } from '../../dataStreamFromSiteWise'; const getAggregatedPropertyDataPointsForProperty = ({ requestInformation, diff --git a/packages/source-iotsitewise/src/time-series-data/client/getHistoricalPropertyDataPoints.ts b/packages/source-iotsitewise/src/time-series-data/client/legacy/getHistoricalPropertyDataPoints.ts similarity index 91% rename from packages/source-iotsitewise/src/time-series-data/client/getHistoricalPropertyDataPoints.ts rename to packages/source-iotsitewise/src/time-series-data/client/legacy/getHistoricalPropertyDataPoints.ts index 57dbac67b..feb15323c 100644 --- a/packages/source-iotsitewise/src/time-series-data/client/getHistoricalPropertyDataPoints.ts +++ b/packages/source-iotsitewise/src/time-series-data/client/legacy/getHistoricalPropertyDataPoints.ts @@ -1,10 +1,10 @@ import { GetAssetPropertyValueHistoryCommand, IoTSiteWiseClient, TimeOrdering } from '@aws-sdk/client-iotsitewise'; -import { AssetId, AssetPropertyId } from '../types'; -import { toDataPoint } from '../util/toDataPoint'; -import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { AssetId, AssetPropertyId } from '../../types'; +import { toDataPoint } from '../../util/toDataPoint'; +import { dataStreamFromSiteWise } from '../../dataStreamFromSiteWise'; import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; -import { toId, toSiteWiseAssetProperty } from '../util/dataStreamId'; -import { isDefined } from '../../common/predicates'; +import { toId, toSiteWiseAssetProperty } from '../../util/dataStreamId'; +import { isDefined } from '../../../common/predicates'; const getHistoricalPropertyDataPointsForProperty = ({ requestInformation, diff --git a/packages/source-iotsitewise/src/time-series-data/client/getLatestPropertyDataPoint.ts b/packages/source-iotsitewise/src/time-series-data/client/legacy/getLatestPropertyDataPoint.ts similarity index 88% rename from packages/source-iotsitewise/src/time-series-data/client/getLatestPropertyDataPoint.ts rename to packages/source-iotsitewise/src/time-series-data/client/legacy/getLatestPropertyDataPoint.ts index 0e10bc3d9..52ed0a82a 100644 --- a/packages/source-iotsitewise/src/time-series-data/client/getLatestPropertyDataPoint.ts +++ b/packages/source-iotsitewise/src/time-series-data/client/legacy/getLatestPropertyDataPoint.ts @@ -1,9 +1,9 @@ import { GetAssetPropertyValueCommand, IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; -import { toDataPoint } from '../util/toDataPoint'; -import { dataStreamFromSiteWise } from '../dataStreamFromSiteWise'; +import { toDataPoint } from '../../util/toDataPoint'; +import { dataStreamFromSiteWise } from '../../dataStreamFromSiteWise'; import { OnSuccessCallback, ErrorCallback, RequestInformationAndRange } from '@iot-app-kit/core'; -import { toId, toSiteWiseAssetProperty } from '../util/dataStreamId'; -import { isDefined } from '../../common/predicates'; +import { toId, toSiteWiseAssetProperty } from '../../util/dataStreamId'; +import { isDefined } from '../../../common/predicates'; export const getLatestPropertyDataPoint = async ({ onSuccess, diff --git a/packages/source-iotsitewise/src/time-series-data/data-source.spec.ts b/packages/source-iotsitewise/src/time-series-data/data-source.spec.ts index 5b34eb9b7..62ad793e7 100644 --- a/packages/source-iotsitewise/src/time-series-data/data-source.spec.ts +++ b/packages/source-iotsitewise/src/time-series-data/data-source.spec.ts @@ -1,12 +1,15 @@ import flushPromises from 'flush-promises'; -import { IoTSiteWiseClient, ResourceNotFoundException } from '@aws-sdk/client-iotsitewise'; +import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; import { createDataSource, SITEWISE_DATA_SOURCE } from './data-source'; import { MINUTE_IN_MS, HOUR_IN_MS, MONTH_IN_MS, IotAppKitDataModule, TimeSeriesDataRequest } from '@iot-app-kit/core'; import { SiteWiseDataStreamQuery } from './types'; import { - ASSET_PROPERTY_DOUBLE_VALUE, AGGREGATE_VALUES, ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_VALUE_HISTORY, + BATCH_ASSET_PROPERTY_AGGREGATES, + BATCH_ASSET_PROPERTY_DOUBLE_VALUE, + BATCH_ASSET_PROPERTY_ERROR, } from '../__mocks__/assetPropertyValue'; import { createMockSiteWiseSDK } from '../__mocks__/iotsitewiseSDK'; import { toId } from './util/dataStreamId'; @@ -52,15 +55,15 @@ const HISTORICAL_REQUEST: TimeSeriesDataRequest = { describe('initiateRequest', () => { it('does not call SDK when query contains no assets', () => { - const getAssetPropertyValue = jest.fn(); - const getAssetPropertyAggregates = jest.fn(); - const getAssetPropertyValueHistory = jest.fn(); + const batchGetAssetPropertyValue = jest.fn(); + const batchGetAssetPropertyAggregates = jest.fn(); + const batchGetAssetPropertyValueHistory = jest.fn(); const getInterpolatedAssetPropertyValues = jest.fn(); const mockSDK = createMockSiteWiseSDK({ - getAssetPropertyValue, - getAssetPropertyValueHistory, - getAssetPropertyAggregates, + batchGetAssetPropertyValue, + batchGetAssetPropertyValueHistory, + batchGetAssetPropertyAggregates, getInterpolatedAssetPropertyValues, }); @@ -79,141 +82,18 @@ describe('initiateRequest', () => { [] ); - expect(getAssetPropertyAggregates).not.toBeCalled(); - expect(getAssetPropertyValue).not.toBeCalled(); - expect(getAssetPropertyValueHistory).not.toBeCalled(); + expect(batchGetAssetPropertyAggregates).not.toBeCalled(); + expect(batchGetAssetPropertyValue).not.toBeCalled(); + expect(batchGetAssetPropertyValueHistory).not.toBeCalled(); expect(getInterpolatedAssetPropertyValues).not.toBeCalled(); }); describe('fetch latest before end', () => { - describe('on error', () => { - it.skip('calls `onError` callback', async () => { - const ERR: Partial = { - name: 'ResourceNotFoundException', - message: 'assetId 1 not found', - $metadata: { - httpStatusCode: 404, - }, - }; - const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR); - - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); + it('gets latest value for multiple properties', async () => { + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); - const dataSource = createDataSource(mockSDK); - - const ASSET_1 = 'asset-1'; - const PROPERTY_1 = 'prop-1'; - - const query: SiteWiseDataStreamQuery = { - source: SITEWISE_DATA_SOURCE, - assets: [{ assetId: ASSET_1, properties: [{ propertyId: PROPERTY_1 }] }], - }; - - const onError = jest.fn(); - const onSuccess = jest.fn(); - - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: LAST_MINUTE_REQUEST, - }, - [ - { - id: toId({ assetId: ASSET_1, propertyId: PROPERTY_1 }), - start: new Date(), - end: new Date(), - resolution: '0', - fetchMostRecentBeforeEnd: true, - }, - ] - ); - - await flushPromises(); - - expect(onSuccess).not.toBeCalled(); - expect(onError).toBeCalledWith({ - id: toId({ assetId: ASSET_1, propertyId: PROPERTY_1 }), - resolution: '0', - error: { - msg: ERR.message, - type: ERR.name, - status: ERR.$metadata?.httpStatusCode, - }, - }); - }); - }); - - it.skip('gets latest value when provided with a duration and `fetchLatestBeforeEnd` is true', async () => { - const getAssetPropertyValue = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); - const getAssetPropertyAggregates = jest.fn(); - const getAssetPropertyValueHistory = jest.fn(); - const getInterpolatedAssetPropertyValues = jest.fn(); - - const mockSDK = createMockSiteWiseSDK({ - getAssetPropertyValue, - getAssetPropertyValueHistory, - getAssetPropertyAggregates, - getInterpolatedAssetPropertyValues, - }); - - const dataSource = createDataSource(mockSDK); - - const query: SiteWiseDataStreamQuery = { - source: SITEWISE_DATA_SOURCE, - assets: [{ assetId: 'some-asset-id', properties: [{ propertyId: 'some-property-id' }] }], - }; - - const onError = jest.fn(); - const onSuccess = jest.fn(); - - dataSource.initiateRequest( - { - onError, - onSuccess, - query, - request: LAST_MINUTE_REQUEST, - }, - [ - { - id: toId({ assetId: 'some-asset-id', propertyId: 'some-property-id' }), - start: new Date(), - end: new Date(), - resolution: '0', - fetchMostRecentBeforeEnd: true, - }, - ] - ); - - await flushPromises(); - - expect(getAssetPropertyAggregates).not.toBeCalled(); - expect(getAssetPropertyValueHistory).not.toBeCalled(); - expect(getInterpolatedAssetPropertyValues).not.toBeCalled(); - - expect(getAssetPropertyValue).toBeCalledTimes(1); - expect(getAssetPropertyValue).toBeCalledWith({ - assetId: query.assets[0].assetId, - propertyId: query.assets[0].properties[0].propertyId, - }); - - expect(onError).not.toBeCalled(); - - expect(onSuccess).toBeCalledTimes(1); - expect(onSuccess).toBeCalledWith([ - expect.objectContaining({ - id: toId({ assetId: 'some-asset-id', propertyId: 'some-property-id' }), - data: [{ x: 1000099, y: 10.123 }], - resolution: '0', - }), - ]); - }); - - it('gets latest value for multiple properties', () => { - const getAssetPropertyValue = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); - - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory, batchGetAssetPropertyValue }); const dataSource = createDataSource(mockSDK); @@ -251,23 +131,48 @@ describe('initiateRequest', () => { ] ); - expect(getAssetPropertyValue).toBeCalledTimes(2); + await flushPromises(); - expect(getAssetPropertyValue).toBeCalledWith({ - assetId: ASSET_ID, - propertyId: PROPERTY_1, - }); + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValue).toBeCalledWith({ - assetId: ASSET_ID, - propertyId: PROPERTY_2, - }); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + }), + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + }), + ]), + }) + ); + + expect(batchGetAssetPropertyValue).toBeCalledTimes(1); + + expect(batchGetAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + }), + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + }), + ]), + }) + ); }); - it('gets latest value for multiple assets', () => { - const getAssetPropertyValue = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); + it('gets latest value for multiple assets', async () => { + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_DOUBLE_VALUE); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory, batchGetAssetPropertyValue }); const dataSource = createDataSource(mockSDK); @@ -309,25 +214,49 @@ describe('initiateRequest', () => { ] ); - expect(getAssetPropertyValue).toBeCalledTimes(2); + await flushPromises(); + + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_1, + propertyId: PROPERTY_1, + }), + expect.objectContaining({ + assetId: ASSET_2, + propertyId: PROPERTY_2, + }), + ]), + }) + ); - expect(getAssetPropertyValue).toBeCalledWith({ - assetId: ASSET_1, - propertyId: PROPERTY_1, - }); + expect(batchGetAssetPropertyValue).toBeCalledTimes(1); - expect(getAssetPropertyValue).toBeCalledWith({ - assetId: ASSET_2, - propertyId: PROPERTY_2, - }); + expect(batchGetAssetPropertyValue).toBeCalledWith( + expect.objectContaining({ + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_1, + propertyId: PROPERTY_1, + }), + expect.objectContaining({ + assetId: ASSET_2, + propertyId: PROPERTY_2, + }), + ]), + }) + ); }); }); describe('fetch latest before start', () => { - it('gets latest value before start for multiple properties', () => { - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); + it('gets latest value before start for multiple properties', async () => { + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValueHistory }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory }); const dataSource = createDataSource(mockSDK); @@ -365,31 +294,34 @@ describe('initiateRequest', () => { ] ); - expect(getAssetPropertyValueHistory).toBeCalledTimes(2); + await flushPromises(); - expect(getAssetPropertyValueHistory).toBeCalledWith( - expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_1, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, - }) - ); + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValueHistory).toBeCalledWith( + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_2, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_1, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + ]), }) ); }); - it('gets latest value before start for multiple assets', () => { - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); + it('gets latest value before start for multiple assets', async () => { + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValueHistory }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory }); const dataSource = createDataSource(mockSDK); @@ -431,31 +363,34 @@ describe('initiateRequest', () => { ] ); - expect(getAssetPropertyValueHistory).toBeCalledTimes(2); + await flushPromises(); - expect(getAssetPropertyValueHistory).toBeCalledWith( - expect.objectContaining({ - assetId: ASSET_1, - propertyId: PROPERTY_1, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, - }) - ); + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValueHistory).toBeCalledWith( + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( expect.objectContaining({ - assetId: ASSET_2, - propertyId: PROPERTY_2, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_1, + propertyId: PROPERTY_1, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + expect.objectContaining({ + assetId: ASSET_2, + propertyId: PROPERTY_2, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + ]), }) ); }); - it('gets latest value before start for aggregates', () => { - const getAssetPropertyAggregates = jest.fn().mockResolvedValue(ASSET_PROPERTY_DOUBLE_VALUE); + it('gets latest value before start for aggregates', async () => { + const batchGetAssetPropertyAggregates = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_AGGREGATES); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyAggregates }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyAggregates }); const dataSource = createDataSource(mockSDK); @@ -493,23 +428,26 @@ describe('initiateRequest', () => { ] ); - expect(getAssetPropertyAggregates).toBeCalledTimes(2); + await flushPromises(); - expect(getAssetPropertyAggregates).toBeCalledWith( - expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_1, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, - }) - ); + expect(batchGetAssetPropertyAggregates).toBeCalledTimes(1); - expect(getAssetPropertyAggregates).toBeCalledWith( + expect(batchGetAssetPropertyAggregates).toBeCalledWith( expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_2, - startDate: new Date(0, 0, 0), - endDate: historicalRequestStart, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_1, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_2, + startDate: new Date(0, 0, 0), + endDate: historicalRequestStart, + }), + ]), }) ); }); @@ -517,15 +455,15 @@ describe('initiateRequest', () => { }); it('requests raw data if specified per asset property', async () => { - const getAssetPropertyValue = jest.fn(); - const getAssetPropertyAggregates = jest.fn(); - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValue = jest.fn(); + const batchGetAssetPropertyAggregates = jest.fn(); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); const getInterpolatedAssetPropertyValues = jest.fn(); const mockSDK = createMockSiteWiseSDK({ - getAssetPropertyValue, - getAssetPropertyValueHistory, - getAssetPropertyAggregates, + batchGetAssetPropertyValue, + batchGetAssetPropertyValueHistory, + batchGetAssetPropertyAggregates, getInterpolatedAssetPropertyValues, }); @@ -575,16 +513,20 @@ it('requests raw data if specified per asset property', async () => { await flushPromises(); - expect(getAssetPropertyValue).not.toBeCalled(); + expect(batchGetAssetPropertyValue).not.toBeCalled(); expect(getInterpolatedAssetPropertyValues).not.toBeCalled(); - expect(getAssetPropertyAggregates).not.toBeCalled(); + expect(batchGetAssetPropertyAggregates).not.toBeCalled(); - expect(getAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValueHistory).toBeCalledWith( + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( expect.objectContaining({ - assetId: query.assets[0].assetId, - propertyId: query.assets[0].properties[0].propertyId, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: query.assets[0].assetId, + propertyId: query.assets[0].properties[0].propertyId, + }), + ]), }) ); @@ -615,21 +557,14 @@ it('requests raw data if specified per asset property', async () => { ); }); -describe.skip('e2e through data-module', () => { +describe('e2e through data-module', () => { describe('fetching range of historical data', () => { it('reports error occurred on request initiation', async () => { const dataModule = new IotAppKitDataModule(); - const ERR: Partial = { - name: 'ResourceNotFoundException', - message: 'assetId 1 not found', - $metadata: { - httpStatusCode: 404, - }, - }; - const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(ERR); + const batchGetAssetPropertyAggregates = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_ERROR); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValueHistory }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyAggregates }); const dataSource = createDataSource(mockSDK); dataModule.registerDataSource(dataSource); @@ -658,14 +593,7 @@ describe.skip('e2e through data-module', () => { expect.objectContaining({ dataStreams: [ expect.objectContaining({ - id: toId({ assetId, propertyId }), - error: { - msg: ERR.message, - type: ERR.name, - status: ERR.$metadata?.httpStatusCode, - }, - isLoading: false, - isRefreshing: false, + error: { msg: 'assetId 1 not found', status: '404' }, }), ], }) @@ -679,16 +607,10 @@ describe.skip('e2e through data-module', () => { it('reports error occurred on request initiation', async () => { const dataModule = new IotAppKitDataModule(); - const ERR: Partial = { - name: 'ResourceNotFoundException', - message: 'assetId 1 not found', - $metadata: { - httpStatusCode: 404, - }, - }; - const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValue = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_ERROR); - const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); + const mockSDK = createMockSiteWiseSDK({ batchGetAssetPropertyValueHistory, batchGetAssetPropertyValue }); const dataSource = createDataSource(mockSDK); dataModule.registerDataSource(dataSource); @@ -706,8 +628,8 @@ describe.skip('e2e through data-module', () => { } as SiteWiseDataStreamQuery, ], request: { - viewport: { start: new Date(2000, 0, 0), end: new Date() }, - settings: { fetchMostRecentBeforeEnd: true }, + viewport: { start: new Date(2000, 0, 0), end: new Date(2000, 0, 0, 1) }, + settings: { fetchMostRecentBeforeEnd: true, resolution: '0' }, }, }, timeSeriesCallback @@ -715,19 +637,12 @@ describe.skip('e2e through data-module', () => { await flushPromises(); - expect(timeSeriesCallback).toBeCalledTimes(2); - expect(timeSeriesCallback).toHaveBeenLastCalledWith( + expect(timeSeriesCallback).toBeCalledTimes(3); + expect(timeSeriesCallback).toHaveBeenCalledWith( expect.objectContaining({ dataStreams: [ expect.objectContaining({ - id: toId({ assetId, propertyId }), - error: { - msg: ERR.message, - type: ERR.name, - status: ERR.$metadata?.httpStatusCode, - }, - isLoading: false, - isRefreshing: false, + error: { msg: 'assetId 1 not found', status: '404' }, }), ], }) diff --git a/packages/source-iotsitewise/src/time-series-data/data-source.ts b/packages/source-iotsitewise/src/time-series-data/data-source.ts index b5d8719df..402784d47 100644 --- a/packages/source-iotsitewise/src/time-series-data/data-source.ts +++ b/packages/source-iotsitewise/src/time-series-data/data-source.ts @@ -1,5 +1,5 @@ import { IoTSiteWiseClient, AggregateType } from '@aws-sdk/client-iotsitewise'; -import { SiteWiseDataStreamQuery } from './types'; +import { SiteWiseDataSourceSettings, SiteWiseDataStreamQuery } from './types'; import { SiteWiseClient } from './client/client'; import { toId } from './util/dataStreamId'; import { @@ -60,8 +60,11 @@ export const determineResolution = ({ } }; -export const createDataSource = (siteWise: IoTSiteWiseClient): DataSource => { - const client = new SiteWiseClient(siteWise); +export const createDataSource = ( + siteWise: IoTSiteWiseClient, + settings?: SiteWiseDataSourceSettings +): DataSource => { + const client = new SiteWiseClient(siteWise, settings); return { name: SITEWISE_DATA_SOURCE, initiateRequest: ({ onSuccess, onError }, requestInformations) => diff --git a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts index 484652913..dea0a258f 100644 --- a/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts +++ b/packages/source-iotsitewise/src/time-series-data/subscribeToTimeSeriesData.spec.ts @@ -7,7 +7,7 @@ import flushPromises from 'flush-promises'; import { createDataSource } from './data-source'; import { createAssetModelResponse, createAssetResponse } from '../__mocks__/asset'; import { toId } from './util/dataStreamId'; -import { ASSET_PROPERTY_VALUE_HISTORY } from '../__mocks__/assetPropertyValue'; +import { BATCH_ASSET_PROPERTY_VALUE_HISTORY } from '../__mocks__/assetPropertyValue'; import { SiteWiseAssetDataSource, SiteWiseAssetModule } from '../asset-modules'; const initializeSubscribeToTimeSeriesData = (client: IoTSiteWiseClient) => { @@ -56,7 +56,7 @@ it('provides time series data from iotsitewise', async () => { const ASSET_MODEL_ID = 'some-asset-model-id'; const PROPERTY_NAME = 'some-property-name'; - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); const describeAsset = jest .fn() .mockImplementation(({ assetId }) => @@ -76,7 +76,7 @@ it('provides time series data from iotsitewise', async () => { createMockSiteWiseSDK({ describeAsset, describeAssetModel, - getAssetPropertyValueHistory, + batchGetAssetPropertyValueHistory, }) ); @@ -110,11 +110,15 @@ it('provides time series data from iotsitewise', async () => { expect(describeAssetModel).toBeCalledWith({ assetModelId: ASSET_MODEL_ID }); // fetches historical data - expect(getAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValueHistory).toBeCalledWith( + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_ID, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_ID, + }), + ]), }) ); @@ -145,7 +149,7 @@ it('provides timeseries data from iotsitewise when subscription is updated', asy const ASSET_MODEL_ID = 'some-asset-model-id'; const PROPERTY_NAME = 'some-property-name'; - const getAssetPropertyValueHistory = jest.fn().mockResolvedValue(ASSET_PROPERTY_VALUE_HISTORY); + const batchGetAssetPropertyValueHistory = jest.fn().mockResolvedValue(BATCH_ASSET_PROPERTY_VALUE_HISTORY); const describeAsset = jest .fn() .mockImplementation(({ assetId }) => @@ -165,7 +169,7 @@ it('provides timeseries data from iotsitewise when subscription is updated', asy createMockSiteWiseSDK({ describeAsset, describeAssetModel, - getAssetPropertyValueHistory, + batchGetAssetPropertyValueHistory, }) ); @@ -205,11 +209,15 @@ it('provides timeseries data from iotsitewise when subscription is updated', asy expect(describeAssetModel).toBeCalledWith({ assetModelId: ASSET_MODEL_ID }); // fetches historical data - expect(getAssetPropertyValueHistory).toBeCalledTimes(1); - expect(getAssetPropertyValueHistory).toBeCalledWith( + expect(batchGetAssetPropertyValueHistory).toBeCalledTimes(1); + expect(batchGetAssetPropertyValueHistory).toBeCalledWith( expect.objectContaining({ - assetId: ASSET_ID, - propertyId: PROPERTY_ID, + entries: expect.arrayContaining([ + expect.objectContaining({ + assetId: ASSET_ID, + propertyId: PROPERTY_ID, + }), + ]), }) ); diff --git a/packages/source-iotsitewise/src/time-series-data/types.ts b/packages/source-iotsitewise/src/time-series-data/types.ts index 682738ab3..f74de6c20 100644 --- a/packages/source-iotsitewise/src/time-series-data/types.ts +++ b/packages/source-iotsitewise/src/time-series-data/types.ts @@ -27,3 +27,8 @@ export type SiteWiseAssetQuery = { export type SiteWiseAssetDataStreamQuery = DataStreamQuery & SiteWiseAssetQuery; export type SiteWiseDataStreamQuery = SiteWiseAssetDataStreamQuery; + +export type SiteWiseDataSourceSettings = { + batchDuration?: number; + legacyAPI?: boolean; +}; diff --git a/yarn.lock b/yarn.lock index 83cc28690..3a7f6c320 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,6 +81,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/abort-controller@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.78.0.tgz#f2b0f8d63954afe51136254f389a18dd24a8f6f3" + integrity sha512-iz1YLwM2feJUj/y97yO4XmDeTxs+yZ1XJwQgoawKuc8IDBKUutnJNCHL5jL04WUKU7Nrlq+Hr2fCTScFh2z9zg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/client-cognito-identity@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.67.0.tgz#fea86530eef63b9004c0113aff0ec91ae4ecd483" @@ -120,44 +128,44 @@ "@aws-sdk/util-utf8-node" "3.55.0" tslib "^2.3.1" -"@aws-sdk/client-iotsitewise@^3.39.0": - version "3.67.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-iotsitewise/-/client-iotsitewise-3.67.0.tgz#53aa9762c412bfdc3767c11a86f623623f2f5150" - integrity sha512-6UEsA1XioAdteg8zZKXS8xH6pnS8sZRuCkeVzu8x6E9kocja13xNg1VQPw62U3xm5uEv3bdRW3a/vNWBy51v7w== +"@aws-sdk/client-iotsitewise@^3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-iotsitewise/-/client-iotsitewise-3.87.0.tgz#768c32da2de8547d2a48559654c52606539438a7" + integrity sha512-OoprJQ86fzJnXAkhLnqADMlhZJ2sjNbaDeLXi7Hoiqnjsq+joG3rkFfMfKqKIyXVHBjTxl2jywJYPung3q2M5A== dependencies: "@aws-crypto/sha256-browser" "2.0.0" "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.67.0" - "@aws-sdk/config-resolver" "3.58.0" - "@aws-sdk/credential-provider-node" "3.67.0" - "@aws-sdk/fetch-http-handler" "3.58.0" - "@aws-sdk/hash-node" "3.55.0" - "@aws-sdk/invalid-dependency" "3.55.0" - "@aws-sdk/middleware-content-length" "3.58.0" - "@aws-sdk/middleware-host-header" "3.58.0" - "@aws-sdk/middleware-logger" "3.55.0" - "@aws-sdk/middleware-retry" "3.58.0" - "@aws-sdk/middleware-serde" "3.55.0" - "@aws-sdk/middleware-signing" "3.58.0" - "@aws-sdk/middleware-stack" "3.55.0" - "@aws-sdk/middleware-user-agent" "3.58.0" - "@aws-sdk/node-config-provider" "3.58.0" - "@aws-sdk/node-http-handler" "3.58.0" - "@aws-sdk/protocol-http" "3.58.0" - "@aws-sdk/smithy-client" "3.55.0" - "@aws-sdk/types" "3.55.0" - "@aws-sdk/url-parser" "3.55.0" + "@aws-sdk/client-sts" "3.87.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" "@aws-sdk/util-base64-browser" "3.58.0" "@aws-sdk/util-base64-node" "3.55.0" "@aws-sdk/util-body-length-browser" "3.55.0" "@aws-sdk/util-body-length-node" "3.55.0" - "@aws-sdk/util-defaults-mode-browser" "3.55.0" - "@aws-sdk/util-defaults-mode-node" "3.58.0" - "@aws-sdk/util-user-agent-browser" "3.58.0" - "@aws-sdk/util-user-agent-node" "3.58.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" "@aws-sdk/util-utf8-browser" "3.55.0" "@aws-sdk/util-utf8-node" "3.55.0" - "@aws-sdk/util-waiter" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" tslib "^2.3.1" uuid "^8.3.2" @@ -197,6 +205,42 @@ "@aws-sdk/util-utf8-node" "3.55.0" tslib "^2.3.1" +"@aws-sdk/client-sso@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.85.0.tgz#4e5cf2b9e9898ff23c0aed1af0bac8d46ceed229" + integrity sha512-JMW0NzFpo99oE6O9M/kgLela73p4vmhe/5TIcdrqUvP9XUV9nANl5nSXh3rqLz0ubmliedz9kdYYhwMC3ntoXg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + "@aws-sdk/client-sts@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.67.0.tgz#d45ac0b270eac749b74477458d6980e77303bb1b" @@ -238,6 +282,47 @@ fast-xml-parser "3.19.0" tslib "^2.3.1" +"@aws-sdk/client-sts@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.87.0.tgz#65c18dce2ba8312a8cb4289b29bc1f507db97e92" + integrity sha512-JGI5rzSq8T7IVlfDJ8ltGl8nyVEtwvqXrYR87DwTjeE4HP+/oBdWdbO0oBL1TJMGjzZcENyVYvmaSAkobenkTg== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.87.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-sts" "3.78.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.85.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.85.0" + "@aws-sdk/util-defaults-mode-node" "3.85.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + "@aws-sdk/config-resolver@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.58.0.tgz#c990541276ecdc76acf25f68f58cdb0d0d7eb07e" @@ -249,6 +334,17 @@ "@aws-sdk/util-middleware" "3.55.0" tslib "^2.3.1" +"@aws-sdk/config-resolver@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.80.0.tgz#a804aba4d4767402ab15640757c8c8bb2254eec1" + integrity sha512-vFruNKlmhsaC8yjnHmasi1WW/7EELlEuFTj4mqcqNqR4dfraf0maVvpqF1VSR8EstpFMsGYI5dmoWAnnG4PcLQ== + dependencies: + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-config-provider" "3.55.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-cognito-identity@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.67.0.tgz#3d16f3e082714da898d84d87e48f5e9b5cd5d8e7" @@ -268,6 +364,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-env@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.78.0.tgz#e3013073bab0db313b0505d790aa79a35bd582d9" + integrity sha512-K41VTIzVHm2RyIwtBER8Hte3huUBXdV1WKO+i7olYVgLFmaqcZUNrlyoGDRqZcQ/u4AbxTzBU9jeMIbIfzMOWg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-imds@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.58.0.tgz#89d3963895f5e6150b74b5ba2010158d8576b95e" @@ -279,6 +384,17 @@ "@aws-sdk/url-parser" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-imds@3.81.0": + version "3.81.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.81.0.tgz#1ffd1219b7fd19eec4d4d4b5b06bda66e3bc210e" + integrity sha512-BHopP+gaovTYj+4tSrwCk8NNCR48gE9CWmpIOLkP9ell0gOL81Qh7aCEiIK0BZBZkccv1s16cYq1MSZZGS7PEQ== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-ini@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.67.0.tgz#94107021cc1869d98a5a068b95495fb673b9a22a" @@ -293,6 +409,20 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-ini@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.85.0.tgz#ecbd1d9f3afbcb054b241ae5ced0bc5db6b2a053" + integrity sha512-KgzLGq+w8OrSLutwdYUw0POeLinGQKcqvQJ9702eoeXCwZMnEHwKqU61bn8QKMX/tuYVCNV4I1enI7MmYPW8Lw== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-sso" "3.85.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-node@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.67.0.tgz#282bc00800a6e6f753d64ac2f66615d7a3545309" @@ -309,6 +439,22 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-node@3.87.0": + version "3.87.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.87.0.tgz#700e328ac21219cac521e119d06dead873c5ada1" + integrity sha512-yL9W5nX00grNNsGj2df1y7hQ0F77UA7+2toPOVqYPIDhFtIUA97AVYiBEFQz1mO9OAhUfCGgxuFF4pyqFoMcHQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-ini" "3.85.0" + "@aws-sdk/credential-provider-process" "3.80.0" + "@aws-sdk/credential-provider-sso" "3.85.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-process@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.58.0.tgz#ff6db03266428bb2074e9b32db8021efa1af6570" @@ -319,6 +465,16 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-process@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.80.0.tgz#625577774278f845fe5bd0f311ed53973ec92ede" + integrity sha512-3Ro+kMMyLUJHefOhGc5pOO/ibGcJi8bkj0z/Jtqd5I2Sm1qi7avoztST67/k48KMW1OqPnD/FUqxz5T8B2d+FQ== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-sso@3.67.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.67.0.tgz#6541a93ba6cbf36dd18db97f9f4a2beaa1df2a62" @@ -330,6 +486,17 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-sso@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.85.0.tgz#05f0d4b004d0a6ff799c09f0923ae4d4c55f2c9a" + integrity sha512-uE238BgJ/AftPDlBGDlV0XdiNWnUZxFmUmLxgbr19/6jHaCuBr//T6rP+Bc0BjcHkvQCvTdFoCjs17R3Quy3cw== + dependencies: + "@aws-sdk/client-sso" "3.85.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-provider-web-identity@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.55.0.tgz#21aebe5b4ad7a5b4abaf8df9aabfba0994ece357" @@ -339,6 +506,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/credential-provider-web-identity@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.78.0.tgz#61cc6c5c065de3d8d34b7633899e3bbfa9a24c9d" + integrity sha512-9/IvqHdJaVqMEABA8xZE3t5YF1S2PepfckVu0Ws9YUglj6oO+2QyVX6aRgMF1xph6781+Yc31TDh8/3eaDja7w== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/credential-providers@^3.39.0": version "3.67.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.67.0.tgz#4cd977d1d9ab51b46a869d6cef74fe562114876a" @@ -370,6 +546,17 @@ "@aws-sdk/util-base64-browser" "3.58.0" tslib "^2.3.1" +"@aws-sdk/fetch-http-handler@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.78.0.tgz#9cd4a02eaf015b4a5a18552e8c9e8fbfce7219a3" + integrity sha512-cR6r2h2kJ1DNEZSXC6GknQB7OKmy+s9ZNV+g3AsNqkrUmNNOaHpFoSn+m6SC3qaclcGd0eQBpqzSu/TDn23Ihw== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + tslib "^2.3.1" + "@aws-sdk/hash-node@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.55.0.tgz#ea58e9b6f2147c59ad4e41e83bd6864df59b331e" @@ -379,6 +566,15 @@ "@aws-sdk/util-buffer-from" "3.55.0" tslib "^2.3.1" +"@aws-sdk/hash-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.78.0.tgz#d03f804a685bc1cea9df3eabf499b2a7659d01fd" + integrity sha512-ev48yXaqZVtMeuKy52LUZPHCyKvkKQ9uiUebqkA+zFxIk+eN8SMPFHmsififIHWuS6ZkXBUSctjH9wmLebH60A== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + "@aws-sdk/invalid-dependency@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.55.0.tgz#5406c80e4be534700b92b61c21a74efd754c9492" @@ -387,6 +583,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/invalid-dependency@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.78.0.tgz#c4e30871d69894dbf3450023319385110ce95c81" + integrity sha512-zUo+PbeRMN/Mzj6y+6p9qqk/znuFetT1gmpOcZGL9Rp2T+b9WJWd+daq5ktsL10sVCzIt2UvneJRz6b+aU+bfw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/is-array-buffer@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz#c46122c5636f01d5895e5256a587768c3425ea7a" @@ -403,6 +607,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-content-length@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.78.0.tgz#57d46be61d1176d4c5fce7ba4b0682798c170208" + integrity sha512-5MpKt6lB9TdFy25/AGrpOjPY0iDHZAKpEHc+jSOJBXLl6xunXA7qHdiYaVqkWodLxy70nIckGNHqQ3drabidkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-host-header@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.58.0.tgz#c7fe87ed16306e328e780bbed282dbf31d605236" @@ -412,6 +625,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-host-header@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.78.0.tgz#9130d176c2839bc658aff01bf2a36fee705f0e86" + integrity sha512-1zL8uaDWGmH50c8B8jjz75e0ePj6/3QeZEhjJgTgL6DTdiqvRt32p3t+XWHW+yDI14fZZUYeTklAaLVxqFrHqQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-logger@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.55.0.tgz#83adc985a3a98493519384565e0c1a06552b8704" @@ -420,6 +642,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-logger@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.78.0.tgz#758b84711213b2e78afe0df20bc2d4d70a856da1" + integrity sha512-GBhwxNjhCJUIeQQDaGasX/C23Jay77al2vRyGwmxf8no0DdFsa4J1Ik6/2hhIqkqko+WM4SpCnpZrY4MtnxNvA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-retry@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.58.0.tgz#967518e5b9e55546dcb5de0dfe5784df71807d72" @@ -432,6 +662,18 @@ tslib "^2.3.1" uuid "^8.3.2" +"@aws-sdk/middleware-retry@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.80.0.tgz#d62ebd68ded78bdaf0a8b07bb4cc1c394c99cc8f" + integrity sha512-CTk+tA4+WMUNOcUfR6UQrkhwvPYFpnMsQ1vuHlpLFOGG3nCqywA2hueLMRQmVcDXzP0sGeygce6dzRI9dJB/GA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/service-error-classification" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + uuid "^8.3.2" + "@aws-sdk/middleware-sdk-sts@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.58.0.tgz#5b433a49d2aeb10120805d0f13f6700153d55ec9" @@ -444,6 +686,18 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-sdk-sts@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.78.0.tgz#15d91c421380f748b58bb006e1c398cfdf59b290" + integrity sha512-Lu/kN0J0/Kt0ON1hvwNel+y8yvf35licfIgtedHbBCa/ju8qQ9j+uL9Lla6Y5Tqu29yVaye1JxhiIDhscSwrLA== + dependencies: + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-serde@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.55.0.tgz#326a0696255868a9dfca7c482a616897e9d54fdf" @@ -452,6 +706,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-serde@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.78.0.tgz#d1e1a7b9ac58638b973e533ac4c2ca52f413883c" + integrity sha512-4DPsNOxsl1bxRzfo1WXEZjmD7OEi7qGNpxrDWucVe96Fqj2dH08jR8wxvBIVV1e6bAad07IwdPuCGmivNvwRuQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-signing@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.58.0.tgz#996828122526ec5f95e6e898a6573791db4cd5e1" @@ -463,6 +725,17 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-signing@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.78.0.tgz#2fb41819a9ae0953cf8f428851a57696442469ca" + integrity sha512-OEjJJCNhHHSOprLZ9CzjHIXEKFtPHWP/bG9pMhkV3/6Bmscsgcf8gWHcOnmIrjqX+hT1VALDNpl/RIh0J6/eQw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/middleware-stack@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.55.0.tgz#e99ffb0bdd6861ec3b5a667561dc41dfcb44d36b" @@ -470,6 +743,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/middleware-stack@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.78.0.tgz#e9f42039e500bed23ec74359924ae16e7bf9c77a" + integrity sha512-UoNfRh6eAJN3BJHlG1eb+KeuSe+zARTC2cglroJRyHc2j7GxH2i9FD3IJbj5wvzopJEnQzuY/VCs6STFkqWL1g== + dependencies: + tslib "^2.3.1" + "@aws-sdk/middleware-user-agent@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.58.0.tgz#c60b83f61ed385989e0be5dc80b05a8d5626bbf8" @@ -479,6 +759,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/middleware-user-agent@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.78.0.tgz#e4c7345d26d718de0e84b60ba02b2b08b566fa15" + integrity sha512-wdN5uoq8RxxhLhj0EPeuDSRFuXfUwKeEqRzCKMsYAOC0cAm+PryaP2leo0oTGJ9LUK8REK7zyfFcmtC4oOzlkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/node-config-provider@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.58.0.tgz#1a138c571f6b2608cff49a64f4f2936971734f1e" @@ -489,6 +778,16 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/node-config-provider@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.80.0.tgz#dbb02aa48fb1a0acc3201ca73db5bbf1738895b5" + integrity sha512-vyTOMK04huB7n10ZUv0thd2TE6KlY8livOuLqFTMtj99AJ6vyeB5XBNwKnQtJIt/P7CijYgp8KcFvI9fndOmKg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/node-http-handler@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.58.0.tgz#bb633b51a205181657bfc59b24b7bf1720b7e652" @@ -500,6 +799,17 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/node-http-handler@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.82.0.tgz#e28064815c6c6caf22a16bb7fee4e9e7e73ef3bb" + integrity sha512-yyq/DA/IMzL4fLJhV7zVfP7aUQWPHfOKTCJjWB3KeV5YPiviJtSKb/KyzNi+gQyO7SmsL/8vQbQrf3/s7N/2OA== + dependencies: + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/property-provider@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.55.0.tgz#0eabe5e84d9258c85c2c5e44bcb09379ae9429d2" @@ -508,6 +818,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/property-provider@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.78.0.tgz#f12341fa87da2b54daac95f623bf7ede1754f8ae" + integrity sha512-PZpLvV0hF6lqg3CSN9YmphrB/t5LVJVWGJLB9d9qm7sJs5ksjTYBb5bY91OQ3zit0F4cqBMU8xt2GQ9J6d4DvQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/protocol-http@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.58.0.tgz#170798abcc97884d4beabc4dbbdfe3b41acd2d0a" @@ -516,6 +834,14 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/protocol-http@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.78.0.tgz#8a30db90e3373fe94e2b0007c3cba47b5c9e08bd" + integrity sha512-SQB26MhEK96yDxyXd3UAaxLz1Y/ZvgE4pzv7V3wZiokdEedM0kawHKEn1UQJlqJLEZcQI9QYyysh3rTvHZ3fyg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/querystring-builder@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.55.0.tgz#7d6d4e2c597eb3d636bd3a368b494dac175ba329" @@ -525,6 +851,15 @@ "@aws-sdk/util-uri-escape" "3.55.0" tslib "^2.3.1" +"@aws-sdk/querystring-builder@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.78.0.tgz#29068c4d1fad056e26f848779a31335469cb0038" + integrity sha512-aib6RW1WAaTQDqVgRU1Ku9idkhm90gJKbCxVaGId+as6QHNUqMChEfK2v+0afuKiPNOs5uWmqvOXI9+Gt+UGDg== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + "@aws-sdk/querystring-parser@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.55.0.tgz#ea35642c1b8324dd896d45185f99ad9d6c3af6d2" @@ -533,11 +868,24 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/querystring-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.78.0.tgz#4c76fe15ef2e9bbf4c387c83889d1c25d2c3a614" + integrity sha512-csaH8YTyN+KMNczeK6fBS8l7iJaqcQcKOIbpQFg5upX4Ly5A56HJn4sVQhY1LSgfSk4xRsNfMy5mu6BlsIiaXA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/service-error-classification@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.55.0.tgz#4a85d2d947102c50076bd2af295f62abd74e26ab" integrity sha512-HdjnDyarsa1Avq1MJurkLyEe9c3eRa76dPmK4TmRGgwJ+tInEzGHL0rBW7V8xBK+PDF+fJQ71hvm8jPYmzvBwQ== +"@aws-sdk/service-error-classification@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.78.0.tgz#8d3ac1064e39c180d9b764bb838c7f9de5615281" + integrity sha512-x7Lx8KWctJa01q4Q72Zb4ol9L/era3vy2daASu8l2paHHxsAPBE0PThkvLdUSLZSzlHSVdh3YHESIsT++VsK4w== + "@aws-sdk/shared-ini-file-loader@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.58.0.tgz#321f80f34ef3f15ab40b756fb5ee2797812748c7" @@ -545,6 +893,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/shared-ini-file-loader@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.80.0.tgz#e3d1b0532e9a884e52f967717ba2666ca32bbd74" + integrity sha512-3d5EBJjnWWkjLK9skqLLHYbagtFaZZy+3jUTlbTuOKhlOwe8jF7CUM3j6I4JA6yXNcB3w0exDKKHa8w+l+05aA== + dependencies: + tslib "^2.3.1" + "@aws-sdk/signature-v4@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.58.0.tgz#0d81dd317f9bf35bc0de670c0e534d7793f8e170" @@ -557,6 +912,18 @@ "@aws-sdk/util-uri-escape" "3.55.0" tslib "^2.3.1" +"@aws-sdk/signature-v4@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.78.0.tgz#adb735b9604d4bb8e44d16f1baa87618d576013b" + integrity sha512-eePjRYuzKoi3VMr/lgrUEF1ytLeH4fA/NMCykr/uR6NMo4bSJA59KrFLYSM7SlWLRIyB0UvJqygVEvSxFluyDw== + dependencies: + "@aws-sdk/is-array-buffer" "3.55.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-hex-encoding" "3.58.0" + "@aws-sdk/util-middleware" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + "@aws-sdk/smithy-client@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.55.0.tgz#bf1f5a64d1d2374c291338a52f6c75c6d67e8148" @@ -566,11 +933,25 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/smithy-client@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.85.0.tgz#70852daa14fef9af1adfb4411237026cb68943da" + integrity sha512-Ox/yQEAnANzhpJMyrpuxWtF/i3EviavENczT7fo4uwSyZTz/sfSBQNjs/YAG1UeA6uOI3pBP5EaFERV5hr2fRA== + dependencies: + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/types@3.55.0", "@aws-sdk/types@^3.1.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.55.0.tgz#d524d567e2b2722f2d6be83e2417dd6d46ce1490" integrity sha512-wrDZjuy1CVAYxDCbm3bWQIKMGfNs7XXmG0eG4858Ixgqmq2avsIn5TORy8ynBxcXn9aekV/+tGEQ7BBSYzIVNQ== +"@aws-sdk/types@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.78.0.tgz#51dc80b2142ee20821fb9f476bdca6e541021443" + integrity sha512-I9PTlVNSbwhIgMfmDM5as1tqRIkVZunjVmfogb2WVVPp4CaX0Ll01S0FSMSLL9k6tcQLXqh45pFRjrxCl9WKdQ== + "@aws-sdk/url-parser@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.55.0.tgz#03b47a45c591d52c9d00dc40c630b91094991fe7" @@ -580,6 +961,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/url-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.78.0.tgz#8903011fda4b04c1207df099a21eda1304573099" + integrity sha512-iQn2AjECUoJE0Ae9XtgHtGGKvUkvE8hhbktGopdj+zsPBe4WrBN2DgVxlKPPrBonG/YlcL1D7a5EXaujWSlUUw== + dependencies: + "@aws-sdk/querystring-parser" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/util-base64-browser@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.58.0.tgz#e213f91a5d40dd2d048d340f1ab192ca86c1f40c" @@ -634,6 +1024,16 @@ bowser "^2.11.0" tslib "^2.3.1" +"@aws-sdk/util-defaults-mode-browser@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.85.0.tgz#215e99e8815f885ce722668a0e5afbbca69fa964" + integrity sha512-oqK/e2pHuMWrvTJWtDBzylbj232ezlTay5dCq4RQlyi3LPPVBQ08haYD1Mk2ikQ/qa0XvbSD6YVhjpTlvwRNjw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + "@aws-sdk/util-defaults-mode-node@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.58.0.tgz#57bb445172f10b681f34a7d382d420b9053b2122" @@ -646,6 +1046,18 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/util-defaults-mode-node@3.85.0": + version "3.85.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.85.0.tgz#8cd27ea50ddce298ec586d36eb6379ba14d7bfaf" + integrity sha512-KDNl4H8jJJLh6y7I3MSwRKe4plKbFKK8MVkS0+Fce/GJh4EnqxF0HzMMaSeNUcPvO2wHRq2a60+XW+0d7eWo1A== + dependencies: + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/util-hex-encoding@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.58.0.tgz#d999eb19329933a94563881540a06d7ac7f515f5" @@ -667,6 +1079,13 @@ dependencies: tslib "^2.3.1" +"@aws-sdk/util-middleware@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.78.0.tgz#d907a9b8b7878265cd3e3ee15996bc17de41db11" + integrity sha512-Hi3wv2b0VogO4mzyeEaeU5KgIt4qeo0LXU5gS6oRrG0T7s2FyKbMBkJW3YDh/Y8fNwqArZ+/QQFujpP0PIKwkA== + dependencies: + tslib "^2.3.1" + "@aws-sdk/util-uri-escape@3.55.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz#ee57743c628a1c9f942dfe73205ce890ec011916" @@ -683,6 +1102,15 @@ bowser "^2.11.0" tslib "^2.3.1" +"@aws-sdk/util-user-agent-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.78.0.tgz#12509ed9cc77624da0e0c017099565e37a5038d0" + integrity sha512-diGO/Bf4ggBOEnfD7lrrXaaXOwOXGz0bAJ0HhpizwEMlBld5zfDlWXjNpslh+8+u3EHRjPJQ16KGT6mp/Dm+aw== + dependencies: + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + "@aws-sdk/util-user-agent-node@3.58.0": version "3.58.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.58.0.tgz#ea821601b0d2c7d81239ad0de60964f3967f06ac" @@ -692,6 +1120,15 @@ "@aws-sdk/types" "3.55.0" tslib "^2.3.1" +"@aws-sdk/util-user-agent-node@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.80.0.tgz#269ea0f9bfab4f378af759afa9137936081f010a" + integrity sha512-QV26qIXws1m6sZXg65NS+XrQ5NhAzbDVQLtEVE4nC39UN8fuieP6Uet/gZm9mlLI9hllwvcV7EfgBM3GSC7pZg== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + "@aws-sdk/util-utf8-browser@3.55.0", "@aws-sdk/util-utf8-browser@^3.0.0": version "3.55.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.55.0.tgz#a045bf1a93f6e0ff9c846631b168ea55bbb37668" @@ -707,13 +1144,13 @@ "@aws-sdk/util-buffer-from" "3.55.0" tslib "^2.3.1" -"@aws-sdk/util-waiter@3.55.0": - version "3.55.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.55.0.tgz#0e48a8ce98931f99cfbcad750222fd1f0b237fda" - integrity sha512-Do34MKPFSC/+zVN6vY+FZ+0WN61hzga4nPoAC590AOjs8rW6/H6sDN6Gz1KAZbPnuQUZfvsIJjMxN7lblXHJkQ== +"@aws-sdk/util-waiter@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.78.0.tgz#5886f3e06ae6df9a12ef7079a6e75c76921ea4da" + integrity sha512-8pWd0XiNOS8AkWQyac8VNEI+gz/cGWlC2TAE2CJp0rOK5XhvlcNBINai4D6TxQ+9foyJXLOI1b8nuXemekoG8A== dependencies: - "@aws-sdk/abort-controller" "3.55.0" - "@aws-sdk/types" "3.55.0" + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/types" "3.78.0" tslib "^2.3.1" "@awsui/collection-hooks@^1.0.0": @@ -4552,10 +4989,10 @@ resolve-from "^5.0.0" store2 "^2.12.0" -"@synchro-charts/core@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@synchro-charts/core/-/core-4.0.1.tgz#67a0070e6cafa91660764278c01e2ff362459807" - integrity sha512-Y+QmZU+bs+3dzXD6tJNDZh4AeBi19kbLda9psUrxx0+uomzeUIcJ+ntNHA5N1lvaPXMYQS6IbOdgbhR373emWA== +"@synchro-charts/core@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@synchro-charts/core/-/core-5.0.0.tgz#354cae51fbf8de1994de06de135670c70e713a58" + integrity sha512-zJ7vALPFw9tHCccDWUWFcpR3As7QY380CNGFUgS5ZOn49W1F++VySsGL64XsJYGjoU4/rk5/hAA+BXhsn54dtA== dependencies: "@stencil/redux" "^0.1.1" "@types/d3" "^5.16.4" @@ -9590,6 +10027,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dataloader@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.1.0.tgz#c69c538235e85e7ac6c6c444bae8ecabf5de9df7" + integrity sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ== + date-fns@^1.27.2: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"