From 2c0c6baaac9b1a578ded7a329b1a12824ccbdf42 Mon Sep 17 00:00:00 2001 From: Norbert Nader Date: Mon, 14 Feb 2022 13:12:19 -0800 Subject: [PATCH] feat: improve error handling * add status to error callback for time series data * add error handling in asset module * fix skiped tests * refactor subscribeToAssetTree to accept observer instead of next callback --- .../iot-asset-details/iot-asset-details.tsx | 43 +++- .../iot-asset-tree-demo.tsx | 10 +- .../sitewise-resource-explorer.tsx | 31 ++- .../testing/testing-ground/siteWiseQueries.ts | 16 +- .../testing/testing-ground/testing-ground.tsx | 1 + .../core/src/asset-modules/coordinator.ts | 6 +- packages/core/src/asset-modules/mocks.ts | 52 +++- .../assetTreeSession.spec.ts | 240 +++++++++++++----- .../sitewise-asset-tree/assetTreeSession.ts | 66 +++-- .../sitewise-asset-tree/types.ts | 8 +- .../core/src/asset-modules/sitewise/cache.ts | 10 + .../sitewise/requestProcessor.spec.ts | 15 +- .../sitewise/requestProcessor.ts | 64 +++-- .../src/asset-modules/sitewise/session.ts | 29 ++- .../sitewise/siteWiseAssetModule.spec.ts | 11 +- .../core/src/asset-modules/sitewise/types.ts | 32 ++- packages/core/src/common/types.ts | 1 + .../data-module/IotAppKitDataModule.spec.ts | 6 +- .../data-cache/bestStreamStore.spec.ts | 4 +- .../data-cache/caching/caching.spec.ts | 2 +- .../src/data-module/data-cache/dataActions.ts | 7 +- .../data-cache/dataCacheWrapped.spec.ts | 6 +- .../data-cache/dataCacheWrapped.ts | 6 +- .../data-cache/dataReducer.spec.ts | 23 +- .../core/src/data-module/data-cache/types.ts | 3 +- packages/core/src/data-module/types.ts | 13 +- packages/core/src/index.ts | 5 +- .../__mocks__/mockWidgetProperties.ts | 4 +- .../time-series-data/client/client.spec.ts | 20 +- .../client/getAggregatedPropertyDataPoints.ts | 6 +- .../client/getHistoricalPropertyDataPoints.ts | 6 +- .../client/getLatestPropertyDataPoint.ts | 8 +- .../time-series-data/data-source.spec.ts | 50 +++- .../subscribeToTimeSeriesData.ts | 8 + 34 files changed, 590 insertions(+), 222 deletions(-) create mode 100644 packages/core/src/common/types.ts diff --git a/packages/components/src/components/iot-asset-details/iot-asset-details.tsx b/packages/components/src/components/iot-asset-details/iot-asset-details.tsx index 222add48d..b135573ce 100644 --- a/packages/components/src/components/iot-asset-details/iot-asset-details.tsx +++ b/packages/components/src/components/iot-asset-details/iot-asset-details.tsx @@ -22,21 +22,36 @@ export class IotAssetDetails { componentWillLoad() { this.assetSession = getSiteWiseAssetModule().startSession(); - this.assetSession.requestAssetSummary(this.query, (summary: AssetSummary) => { - this.assetSummary = summary; - const assetId = this.assetSummary?.id as string; - const modelQuery: AssetModelQuery = { assetModelId: this.assetSummary.assetModelId as string }; - this.assetSession.requestAssetModel(modelQuery, (assetModel: DescribeAssetModelResponse) => { - this.assetModel = assetModel; - assetModel.assetModelProperties?.forEach((prop) => { - let propQuery: AssetPropertyValueQuery = { assetId: assetId, propertyId: prop.id as string }; - this.assetSession.requestAssetPropertyValue(propQuery, (propValue: AssetPropertyValue) => { - const copy = new Map(this.assetPropertyValues); - copy.set(prop.id as string, this.convertToString(propValue)); - this.assetPropertyValues = copy; - }); + this.assetSession.requestAssetSummary(this.query, { + next: (summary: AssetSummary) => { + this.assetSummary = summary; + const assetId = this.assetSummary?.id as string; + const modelQuery: AssetModelQuery = { assetModelId: this.assetSummary.assetModelId as string }; + this.assetSession.requestAssetModel(modelQuery, { + next: (assetModel: DescribeAssetModelResponse) => { + this.assetModel = assetModel; + assetModel.assetModelProperties?.forEach((prop) => { + let propQuery: AssetPropertyValueQuery = { assetId: assetId, propertyId: prop.id as string }; + this.assetSession.requestAssetPropertyValue(propQuery, { + next: (propValue: AssetPropertyValue) => { + const copy = new Map(this.assetPropertyValues); + copy.set(prop.id as string, this.convertToString(propValue)); + this.assetPropertyValues = copy; + }, + error: (err) => { + // noop + }, + }); + }); + }, + error: (err) => { + // noop + }, }); - }); + }, + error: (err) => { + // noop + }, }); } diff --git a/packages/components/src/components/iot-asset-tree-demo/iot-asset-tree-demo.tsx b/packages/components/src/components/iot-asset-tree-demo/iot-asset-tree-demo.tsx index 96f99cc24..cf4c03d24 100644 --- a/packages/components/src/components/iot-asset-tree-demo/iot-asset-tree-demo.tsx +++ b/packages/components/src/components/iot-asset-tree-demo/iot-asset-tree-demo.tsx @@ -24,10 +24,12 @@ export class IotAssetTreeDemo { let session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeModule(getSiteWiseAssetModule()).startSession( this.query ); - this.subscription = session.subscribe((newTree) => { - this.roots = newTree; - // check the tree for any new unexpanded nodes and expand them: - this.expandNodes(newTree); + this.subscription = session.subscribe({ + next: (newTree) => { + this.roots = newTree; + // check the tree for any new unexpanded nodes and expand them: + this.expandNodes(newTree); + }, }); } diff --git a/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx index ad98ea846..f80aba631 100644 --- a/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx +++ b/packages/components/src/components/iot-resource-explorer/sitewise-resource-explorer.tsx @@ -5,12 +5,12 @@ import { IoTAppKit, SiteWiseAssetTreeNode, SiteWiseAssetTreeQuery, + ErrorDetails, } from '@iot-app-kit/core'; -import { SitewiseAssetResource } from './types'; +import { SitewiseAssetResource, FilterTexts, ColumnDefinition } from './types'; import { EmptyStateProps, ITreeNode, UseTreeCollection } from '@iot-app-kit/related-table'; import { parseSitewiseAssetTree } from './utils'; import { TableProps } from '@awsui/components-react/table'; -import { FilterTexts, ColumnDefinition } from './types'; import { NonCancelableCustomEvent } from '@awsui/components-react'; @Component({ @@ -33,6 +33,8 @@ export class SitewiseResourceExplorer { @State() items: SitewiseAssetResource[] = []; + @State() errors: ErrorDetails[] = []; + defaults = { selectionType: 'single' as TableProps.SelectionType, loadingText: 'loading...', @@ -50,9 +52,14 @@ export class SitewiseResourceExplorer { subscription: AssetTreeSubscription; componentWillLoad() { - this.subscription = this.appKit.subscribeToAssetTree(this.query, (newTree: SiteWiseAssetTreeNode[]) => { - this.items = parseSitewiseAssetTree(newTree); - }); + this.subscription = this.appKit.subscribeToAssetTree(this.query, { + next: (newTree: SiteWiseAssetTreeNode[]) => { + this.items = parseSitewiseAssetTree(newTree); + }, + error: (err: ErrorDetails[]) => { + this.errors = err; + }, + }) as AssetTreeSubscription; } componentWillUnmount() { @@ -87,6 +94,18 @@ export class SitewiseResourceExplorer { if (this.paginationEnabled) { collectionOptions.pagination = { pageSize: 20 }; } + + let empty: EmptyStateProps = this.defaults.empty; + + if (this.empty) { + empty = this.empty; + } + + if (this.errors.length > 0) { + // TODO: Make use of all the errors + empty = { header: 'Error', description: this.errors[this.errors.length - 1]?.msg }; + } + return ( diff --git a/packages/components/src/testing/testing-ground/siteWiseQueries.ts b/packages/components/src/testing/testing-ground/siteWiseQueries.ts index 65f3d28b4..75d5aca9f 100644 --- a/packages/components/src/testing/testing-ground/siteWiseQueries.ts +++ b/packages/components/src/testing/testing-ground/siteWiseQueries.ts @@ -1,12 +1,12 @@ const STRING_ASSET_ID = 'f2f74fa8-625a-435f-b89c-d27b2d84f45b'; const STRING_PROPERTY_ID = '797482e4-692f-45a2-b3db-17979481e9c3'; -export const DEMO_TURBINE_ASSET_1 = 'f2f74fa8-625a-435f-b89c-d27b2d84f45b'; +export const DEMO_TURBINE_ASSET_1 = '00eeb4b1-5017-48d4-9f39-1066f080a822'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_1 = 'd0dc79be-0dc2-418c-ac23-26f33cdb4b8b'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_2 = '69607dc2-5fbe-416d-aac2-0382018626e4'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_3 = '26072fa0-e36e-489d-90b4-1774e7d12ac9'; -export const DEMO_TURBINE_ASSET_1_PROPERTY_4 = '5c2b7401-57e0-4205-97f2-c4348f12da9a'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_1 = '8739b557-3e77-4df9-9862-130b29dee2b1'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_2 = '9701d7ad-c22e-43fd-b040-68bad00317e3'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_3 = 'bded202a-a436-46b8-85c1-21bb5b945f86'; +export const DEMO_TURBINE_ASSET_1_PROPERTY_4 = 'd8937b65-5f03-4e40-93ac-c5513420ade7'; export const STRING_QUERY = { source: 'site-wise', @@ -27,9 +27,9 @@ export const NUMBER_QUERY = { ], }; -const AGGREGATED_DATA_ASSET = 'f2f74fa8-625a-435f-b89c-d27b2d84f45b'; -const AGGREGATED_DATA_PROPERTY = 'd0dc79be-0dc2-418c-ac23-26f33cdb4b8b'; -const AGGREGATED_DATA_PROPERTY_2 = '69607dc2-5fbe-416d-aac2-0382018626e4'; +const AGGREGATED_DATA_ASSET = '099b1330-83ff-4fec-b165-c7186ec8eb23'; +const AGGREGATED_DATA_PROPERTY = '05c5c47f-fd92-4823-828e-09ce63b90569'; +const AGGREGATED_DATA_PROPERTY_2 = '11d2599a-2547-451d-ab79-a47f878dbbe3'; 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 1b838bced..d98c298d1 100755 --- a/packages/components/src/testing/testing-ground/testing-ground.tsx +++ b/packages/components/src/testing/testing-ground/testing-ground.tsx @@ -138,6 +138,7 @@ export class TestingGround { diff --git a/packages/core/src/asset-modules/coordinator.ts b/packages/core/src/asset-modules/coordinator.ts index 97a11f591..c6c22886e 100644 --- a/packages/core/src/asset-modules/coordinator.ts +++ b/packages/core/src/asset-modules/coordinator.ts @@ -1,7 +1,7 @@ import { SiteWiseAssetSession } from './sitewise/session'; -import { SiteWiseAssetTreeCallback, SiteWiseAssetTreeQuery } from './sitewise-asset-tree/types'; +import { SiteWiseAssetTreeObserver, SiteWiseAssetTreeQuery } from './sitewise-asset-tree/types'; import { SiteWiseAssetTreeSession } from './sitewise-asset-tree/assetTreeSession'; export const subscribeToAssetTree = - (assetModuleSession: SiteWiseAssetSession) => (query: SiteWiseAssetTreeQuery, callback: SiteWiseAssetTreeCallback) => - new SiteWiseAssetTreeSession(assetModuleSession, query).subscribe(callback); + (assetModuleSession: SiteWiseAssetSession) => (query: SiteWiseAssetTreeQuery, observer: SiteWiseAssetTreeObserver) => + new SiteWiseAssetTreeSession(assetModuleSession, query).subscribe(observer); diff --git a/packages/core/src/asset-modules/mocks.ts b/packages/core/src/asset-modules/mocks.ts index 0ab044eae..8dec06af5 100644 --- a/packages/core/src/asset-modules/mocks.ts +++ b/packages/core/src/asset-modules/mocks.ts @@ -8,12 +8,14 @@ import { } from './sitewise/types'; import { AssetPropertyValue, AssetSummary, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; import { lastValueFrom, Observable, Subscription } from 'rxjs'; +import { ErrorDetails } from '../common/types'; export class MockSiteWiseAssetsReplayData { public models: Map = new Map(); public hierarchies: Map = new Map(); public properties: Map = new Map(); public assets: Map = new Map(); + public errors: ErrorDetails[] = []; public addAssetModels(newModels: DescribeAssetModelResponse[]) { newModels.forEach((model) => this.models.set(model.assetModelId as string, model)); @@ -33,6 +35,10 @@ export class MockSiteWiseAssetsReplayData { ) { this.hierarchies.set(assetHierarchyQueryKey(query), newHierarchyAssetSummaryList); } + + public addErrors(errors: ErrorDetails[]) { + this.errors = [...this.errors, ...errors]; + } } export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { @@ -44,10 +50,20 @@ export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { private _requestAssetSummary(query: { assetId: string }): Observable { return new Observable((observer) => { - observer.next(this.replayData.assets.get(query.assetId)); + if (this.replayData.errors.length > 0) { + observer.error(this.replayData.errors); + } else { + observer.next(this.replayData.assets.get(query.assetId)); + } }); } - requestAssetSummary(query: { assetId: string }, observer: (assetSummary: AssetSummary) => void): Subscription { + requestAssetSummary( + query: { assetId: string }, + observer: { + next: (assetSummary: AssetSummary) => void; + error?: (err: ErrorDetails[]) => void; + } + ): Subscription { return this._requestAssetSummary(query).subscribe(observer); } fetchAssetSummary(query: { assetId: string }): Promise { @@ -61,7 +77,10 @@ export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { } requestAssetModel( query: { assetModelId: string }, - observer: (assetSummary: DescribeAssetModelResponse) => void + observer: { + next: (assetSummary: DescribeAssetModelResponse) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetModel(query).subscribe(observer); } @@ -76,7 +95,10 @@ export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { } requestAssetPropertyValue( query: { assetId: string; propertyId: string }, - observer: (assetSummary: AssetPropertyValue) => void + observer: { + next: (assetSummary: AssetPropertyValue) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetPropertyValue(query).subscribe(observer); } @@ -89,12 +111,19 @@ export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { assetHierarchyId: string; }): Observable { return new Observable((observer) => { - observer.next(this.replayData.hierarchies.get(assetHierarchyQueryKey(query))); + if (this.replayData.errors.length > 0) { + observer.error(this.replayData.errors); + } else { + observer.next(this.replayData.hierarchies.get(assetHierarchyQueryKey(query))); + } }); } requestAssetHierarchy( query: { assetId?: string | undefined; assetHierarchyId: string }, - observer: (assetSummary: HierarchyAssetSummaryList) => void + observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetHierarchy(query).subscribe(observer); } @@ -107,10 +136,17 @@ export class MockSiteWiseAssetSession implements SiteWiseAssetSessionInterface { _requestRootAssets(): Observable { return new Observable((observer) => { - observer.next(this.replayData.hierarchies.get(assetHierarchyQueryKey({ assetHierarchyId: HIERARCHY_ROOT_ID }))); + if (this.replayData.errors.length > 0) { + observer.error(this.replayData.errors); + } else { + observer.next(this.replayData.hierarchies.get(assetHierarchyQueryKey({ assetHierarchyId: HIERARCHY_ROOT_ID }))); + } }); } - requestRootAssets(observer: (assetSummary: HierarchyAssetSummaryList) => void): Subscription { + requestRootAssets(observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + }): Subscription { return this._requestRootAssets().subscribe(observer); } fetchRootAssets(): Promise { diff --git a/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.spec.ts b/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.spec.ts index b7eed1ab3..c6e3f52ee 100644 --- a/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.spec.ts +++ b/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.spec.ts @@ -43,24 +43,26 @@ describe('root loading functionality', () => { const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { rootAssetId: '', }); - session.subscribe((treeRoot) => { - if (!treeRoot || treeRoot.length == 0) { - return; - } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.hierarchies.size).toEqual(1); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.isExpanded).toBeFalse(); - expect(treeRoot[0]?.hierarchies.get('bananas1234')).toEqual({ - children: [], - id: 'bananas1234', - isExpanded: false, - loadingState: LoadingStateEnum.NOT_LOADED, - name: 'bananas', - }); + session.subscribe({ + next: (treeRoot) => { + if (!treeRoot || treeRoot.length == 0) { + return; + } + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.hierarchies.size).toEqual(1); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.isExpanded).toBeFalse(); + expect(treeRoot[0]?.hierarchies.get('bananas1234')).toEqual({ + children: [], + id: 'bananas1234', + isExpanded: false, + loadingState: LoadingStateEnum.NOT_LOADED, + name: 'bananas', + }); - expect(treeRoot[0]?.properties).toBeEmpty(); - done(); + expect(treeRoot[0]?.properties).toBeEmpty(); + done(); + }, }); }); }); @@ -75,15 +77,17 @@ describe('branch loading functionality', () => { const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { rootAssetId: rootAsset.id, }); - session.subscribe((treeRoot) => { - if (!treeRoot || treeRoot.length == 0) { - return; - } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.hierarchies.size).toEqual(0); - expect(treeRoot[0]?.properties).toBeEmpty(); - done(); + session.subscribe({ + next: (treeRoot) => { + if (!treeRoot || treeRoot.length == 0) { + return; + } + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.hierarchies.size).toEqual(0); + expect(treeRoot[0]?.properties).toBeEmpty(); + done(); + }, }); }); }); @@ -106,16 +110,18 @@ describe('model loading', () => { rootAssetId: '', withModels: true, }); - session.subscribe((treeRoot) => { - if (!treeRoot || treeRoot.length == 0) { - return; - } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.model).toEqual(sampleAssetModel); + session.subscribe({ + next: (treeRoot) => { + if (!treeRoot || treeRoot.length == 0) { + return; + } + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.model).toEqual(sampleAssetModel); - expect(treeRoot[0]?.properties).toBeEmpty(); - done(); + expect(treeRoot[0]?.properties).toBeEmpty(); + done(); + }, }); }); }); @@ -179,16 +185,18 @@ describe('asset property loading', () => { withModels: true, propertyIds: ['propertyNotInModel.id.1234', 'modelNumber.id.1234'], }); - session.subscribe((treeRoot) => { - if (!treeRoot || treeRoot.length == 0) { - return; - } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.model).toEqual(modelWithProperties); - expect(treeRoot[0]?.properties.size).toEqual(1); - expect(treeRoot[0]?.properties.get('modelNumber.id.1234')).toEqual(expectedPropertyValue); - done(); + session.subscribe({ + next: (treeRoot) => { + if (!treeRoot || treeRoot.length == 0) { + return; + } + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.model).toEqual(modelWithProperties); + expect(treeRoot[0]?.properties.size).toEqual(1); + expect(treeRoot[0]?.properties.get('modelNumber.id.1234')).toEqual(expectedPropertyValue); + done(); + }, }); }); }); @@ -223,21 +231,23 @@ describe('expand functionality', () => { rootAssetId: '', }); session.expand(new BranchReference(rootAsset.id, 'bananas1234')); - session.subscribe((treeRoot) => { - if (treeRoot.length == 0) { - return; - } + session.subscribe({ + next: (treeRoot) => { + if (treeRoot.length == 0) { + return; + } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.hierarchies.size).toEqual(1); - expect(treeRoot[0]?.hierarchies.get('bananas1234')).not.toBeUndefined(); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.isExpanded).toBeTrue(); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children.length).toEqual(2); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children[0].asset).toEqual(bananaOne); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children[1].asset).toEqual(bananaTwo); - done(); + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.hierarchies.size).toEqual(1); + expect(treeRoot[0]?.hierarchies.get('bananas1234')).not.toBeUndefined(); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.isExpanded).toBeTrue(); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children.length).toEqual(2); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children[0].asset).toEqual(bananaOne); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children[1].asset).toEqual(bananaTwo); + done(); + }, }); }); @@ -246,17 +256,109 @@ describe('expand functionality', () => { rootAssetId: '', }); session.collapse(new BranchReference(rootAsset.id, 'bananas1234')); - session.subscribe((treeRoot) => { - if (treeRoot.length == 0) { - return; - } + session.subscribe({ + next: (treeRoot) => { + if (treeRoot.length == 0) { + return; + } - expect(treeRoot.length).toEqual(1); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.asset).toEqual(rootAsset); - expect(treeRoot[0]?.hierarchies.size).toEqual(1); - expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children.length).toEqual(0); - done(); + expect(treeRoot.length).toEqual(1); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.asset).toEqual(rootAsset); + expect(treeRoot[0]?.hierarchies.size).toEqual(1); + expect(treeRoot[0]?.hierarchies.get('bananas1234')?.children.length).toEqual(0); + done(); + }, + }); + }); +}); + +describe('error handling', () => { + let replayData = new MockSiteWiseAssetsReplayData(); + + const error = { msg: 'id not found', type: 'ResourceNotFoundException', status: '404' }; + + replayData.addErrors([error]); + + it('it returns the error when requesting root asset fails', (done) => { + const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { + rootAssetId: '', + }); + session.expand(new BranchReference(undefined, HIERARCHY_ROOT_ID)); + session + .subscribe({ + next: (treeRoot) => { + // noop + }, + error: (err) => { + expect(err.length).toEqual(1); + expect(err[0].msg).toEqual(error.msg); + expect(err[0].type).toEqual(error.type); + expect(err[0].status).toEqual(error.status); + done(); + }, + }) + .unsubscribe(); + }); + + it('it returns the error when requesting asset hierarchy fails', (done) => { + const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { + rootAssetId: '', + }); + session.expand(new BranchReference('asset-id', 'hierarchy-id')); + session + .subscribe({ + next: (treeRoot) => { + // noop + }, + error: (err) => { + expect(err.length).toEqual(1); + expect(err[0].msg).toEqual(error.msg); + expect(err[0].type).toEqual(error.type); + expect(err[0].status).toEqual(error.status); + done(); + }, + }) + .unsubscribe(); + }); + + it('it returns the error when requesting asset summary fails', (done) => { + const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { + rootAssetId: 'root-asset-id', + }); + session + .subscribe({ + next: (treeRoot) => { + // noop + }, + error: (err) => { + expect(err.length).toEqual(1); + expect(err[0].msg).toEqual(error.msg); + expect(err[0].type).toEqual(error.type); + expect(err[0].status).toEqual(error.status); + done(); + }, + }) + .unsubscribe(); + }); + + it('it returns the error when requesting asset model fails', (done) => { + const session: SiteWiseAssetTreeSession = new SiteWiseAssetTreeSession(new MockSiteWiseAssetSession(replayData), { + rootAssetId: 'root-asset-id', }); + session + .subscribe({ + next: (treeRoot) => { + // noop + }, + error: (err) => { + expect(err.length).toEqual(1); + expect(err[0].msg).toEqual(error.msg); + expect(err[0].type).toEqual(error.type); + expect(err[0].status).toEqual(error.status); + done(); + }, + }) + .unsubscribe(); }); }); diff --git a/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.ts b/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.ts index c6bd99bfb..ed6e40d42 100644 --- a/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.ts +++ b/packages/core/src/asset-modules/sitewise-asset-tree/assetTreeSession.ts @@ -4,6 +4,7 @@ import { HierarchyGroup, SiteWiseAssetTreeNode, SiteWiseAssetTreeQuery, + SiteWiseAssetTreeObserver, } from './types'; import { BehaviorSubject, debounceTime, Subject, Subscription } from 'rxjs'; import { AssetModelQuery, HIERARCHY_ROOT_ID, LoadingStateEnum, SiteWiseAssetSessionInterface } from '../sitewise/types'; @@ -54,15 +55,23 @@ export class SiteWiseAssetTreeSession { // query starts at the specified root Asset const root = new Branch(); this.branches[this.rootBranchRef.key] = root; - this.assetSession.requestAssetSummary({ assetId: query.rootAssetId }, (assetSummary) => { - this.saveAsset(assetSummary); - root.assetIds.push(assetSummary.id as string); - this.updateTree(); - }); + this.assetSession.requestAssetSummary( + { assetId: query.rootAssetId }, + { + next: (assetSummary) => { + this.saveAsset(assetSummary); + root.assetIds.push(assetSummary.id as string); + this.updateTree(); + }, + error: (err) => { + this.subject.error(err); + }, + } + ); } } - public subscribe(observer: (tree: SiteWiseAssetTreeNode[]) => void): AssetTreeSubscription { + public subscribe(observer: SiteWiseAssetTreeObserver): AssetTreeSubscription { const subscription: Subscription = this.subject.subscribe(observer); return { @@ -85,9 +94,14 @@ export class SiteWiseAssetTreeSession { // if the branch does not exist, or isn't fully loaded, start loading it if (!existingExpanded || existingExpanded.loadingState != LoadingStateEnum.LOADED) { if (branchRef.hierarchyId === HIERARCHY_ROOT_ID) { - this.assetSession.requestRootAssets((results) => { - this.saveExpandedHierarchy(branchRef, results.assets, results.loadingState); - this.updateTree(); + this.assetSession.requestRootAssets({ + next: (results) => { + this.saveExpandedHierarchy(branchRef, results.assets, results.loadingState); + this.updateTree(); + }, + error: (err) => { + this.subject.error(err); + }, }); } else { this.assetSession.requestAssetHierarchy( @@ -95,9 +109,14 @@ export class SiteWiseAssetTreeSession { assetId: branchRef.assetId, assetHierarchyId: branchRef.hierarchyId, }, - (results) => { - this.saveExpandedHierarchy(branchRef, results.assets, results.loadingState); - this.updateTree(); + { + next: (results) => { + this.saveExpandedHierarchy(branchRef, results.assets, results.loadingState); + this.updateTree(); + }, + error: (err) => { + this.subject.error(err); + }, } ); } @@ -112,24 +131,31 @@ export class SiteWiseAssetTreeSession { // load related Asset Model and any of the requested properties that the Model contains if (this.query.withModels || this.query.propertyIds?.length) { - this.assetSession.requestAssetModel( - { assetModelId: assetNode.asset.assetModelId } as AssetModelQuery, - (model) => { + this.assetSession.requestAssetModel({ assetModelId: assetNode.asset.assetModelId } as AssetModelQuery, { + next: (model) => { assetNode.model = model; this.updateTree(); this.query.propertyIds?.forEach((propertyId) => { if (this.containsPropertyId(model, propertyId)) { this.assetSession.requestAssetPropertyValue( { assetId: assetId, propertyId: propertyId }, - (propertyValue) => { - assetNode.properties.set(propertyId, propertyValue); - this.updateTree(); + { + next: (propertyValue) => { + assetNode.properties.set(propertyId, propertyValue); + this.updateTree(); + }, + error: (err) => { + this.subject.error(err); + }, } ); } }); - } - ); + }, + error: (err) => { + this.subject.error(err); + }, + }); } } diff --git a/packages/core/src/asset-modules/sitewise-asset-tree/types.ts b/packages/core/src/asset-modules/sitewise-asset-tree/types.ts index 981845c4a..2557cf186 100644 --- a/packages/core/src/asset-modules/sitewise-asset-tree/types.ts +++ b/packages/core/src/asset-modules/sitewise-asset-tree/types.ts @@ -1,5 +1,6 @@ -import { AssetSummary, DescribeAssetModelResponse, AssetPropertyValue } from '@aws-sdk/client-iotsitewise'; +import { AssetPropertyValue, AssetSummary, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; import { LoadingStateEnum } from '../sitewise/types'; +import { ErrorDetails } from '../../common/types'; export type SiteWiseAssetTreeNode = { asset: AssetSummary; @@ -40,4 +41,7 @@ export class BranchReference { } } -export type SiteWiseAssetTreeCallback = (tree: SiteWiseAssetTreeNode[]) => void; +export type SiteWiseAssetTreeObserver = { + next: (tree: SiteWiseAssetTreeNode[]) => void; + error?: (err: ErrorDetails[]) => void; +}; diff --git a/packages/core/src/asset-modules/sitewise/cache.ts b/packages/core/src/asset-modules/sitewise/cache.ts index d6b0e376d..b45d21a62 100644 --- a/packages/core/src/asset-modules/sitewise/cache.ts +++ b/packages/core/src/asset-modules/sitewise/cache.ts @@ -6,12 +6,22 @@ import { DescribeAssetResponse, } from '@aws-sdk/client-iotsitewise'; import { CachedAssetSummaryBlock, LoadingStateEnum } from './types'; +import { ErrorDetails } from '../../common/types'; export class SiteWiseAssetCache { private assetCache: Record = {}; private assetModelCache: Record = {}; private propertyValueCache: Record = {}; private hierarchyCache: Record = {}; + private errorCache: ErrorDetails[] = []; + + public storeErrors(err: ErrorDetails): void { + this.errorCache.push(err); + } + + public getAllErrors(): ErrorDetails[] { + return this.errorCache; + } private convertToAssetSummary(assetDescription: DescribeAssetResponse): AssetSummary { return { diff --git a/packages/core/src/asset-modules/sitewise/requestProcessor.spec.ts b/packages/core/src/asset-modules/sitewise/requestProcessor.spec.ts index 4f3c586c4..f21b3ac94 100644 --- a/packages/core/src/asset-modules/sitewise/requestProcessor.spec.ts +++ b/packages/core/src/asset-modules/sitewise/requestProcessor.spec.ts @@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; import { HIERARCHY_ROOT_ID, HierarchyAssetSummaryList, LoadingStateEnum } from './types'; import { sampleAssetModel } from '../../iotsitewise/__mocks__/assetModel'; import { sampleAssetSummary } from '../../iotsitewise/__mocks__/asset'; -import { samplePropertyValue } from '../../iotsitewise/__mocks__/assetPropertyValue'; +import { ASSET_PROPERTY_STRING_VALUE } from '../../iotsitewise/__mocks__/assetPropertyValue'; it('initializes', () => { expect(() => { @@ -94,7 +94,7 @@ describe('Request an Asset Model', () => { describe('Request an Asset Property Value', () => { const mockDataSource = createMockSiteWiseAssetDataSource(); let mockGetPropertyValue = jest.fn(); - mockGetPropertyValue.mockReturnValue(Promise.resolve(samplePropertyValue)); + mockGetPropertyValue.mockResolvedValue(ASSET_PROPERTY_STRING_VALUE); mockDataSource.getPropertyValue = mockGetPropertyValue; const requestProcessor: RequestProcessor = new RequestProcessor(mockDataSource, new SiteWiseAssetCache()); @@ -108,11 +108,12 @@ describe('Request an Asset Property Value', () => { ); }); - // NOTE: code needs to be looked at. Once test was fixed, did not pass. - it.skip('waits for the Asset Property Value', (done) => { - observable.subscribe((result) => { - expect(samplePropertyValue).toEqual(result); - done(); + it('waits for the Asset Property Value', (done) => { + observable.subscribe({ + next: (result) => { + expect(ASSET_PROPERTY_STRING_VALUE.propertyValue).toEqual(result); + done(); + }, }); }); }); diff --git a/packages/core/src/asset-modules/sitewise/requestProcessor.ts b/packages/core/src/asset-modules/sitewise/requestProcessor.ts index f6d1cda3e..8ac9dcb06 100644 --- a/packages/core/src/asset-modules/sitewise/requestProcessor.ts +++ b/packages/core/src/asset-modules/sitewise/requestProcessor.ts @@ -68,11 +68,16 @@ export class RequestProcessor { return; } - this.api.describeAsset({ assetId: assetSummaryQuery.assetId }).then((assetSummary) => { - this.cache.storeAssetSummary(assetSummary); - observer.next(this.cache.getAssetSummary(assetSummaryQuery.assetId)); - observer.complete(); - }); + this.api + .describeAsset({ assetId: assetSummaryQuery.assetId }) + .then((assetSummary) => { + this.cache.storeAssetSummary(assetSummary); + observer.next(this.cache.getAssetSummary(assetSummaryQuery.assetId)); + observer.complete(); + }) + .catch((err) => { + observer.error({ msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }); + }); }); } @@ -111,6 +116,9 @@ export class RequestProcessor { ); observer.complete(); } + }) + .catch((err) => { + observer.error({ msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }); }); }); } @@ -128,11 +136,16 @@ export class RequestProcessor { return; } - this.api.describeAssetModel({ assetModelId: assetModelQuery.assetModelId }).then((model) => { - this.cache.storeAssetModel(model); - observer.next(this.cache.getAssetModel(assetModelQuery.assetModelId)); - observer.complete(); - }); + this.api + .describeAssetModel({ assetModelId: assetModelQuery.assetModelId }) + .then((model) => { + this.cache.storeAssetModel(model); + observer.next(this.cache.getAssetModel(assetModelQuery.assetModelId)); + observer.complete(); + }) + .catch((err) => { + observer.error({ msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }); + }); }); } @@ -167,7 +180,12 @@ export class RequestProcessor { nextToken: paginationToken, assetModelId: undefined, }) - .then((result) => observer.next(result)); + .then((result) => { + observer.next(result); + }) + .catch((err) => { + observer.error({ msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }); + }); }); } @@ -186,6 +204,9 @@ export class RequestProcessor { }) .then((result) => { observer.next(result); + }) + .catch((err) => { + observer.error({ msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }); }); }); } @@ -234,13 +255,20 @@ export class RequestProcessor { ); } - observable.subscribe((results) => { - if (results) { - observer.next(results); - } - if (results && results.loadingState === LoadingStateEnum.LOADED) { - observer.complete(); - } + observable.subscribe({ + next: (results) => { + if (results) { + observer.next(results); + } + if (results && results.loadingState === LoadingStateEnum.LOADED) { + observer.complete(); + } + }, + error: (err) => { + this.cache.storeErrors(err); + const errors = this.cache.getAllErrors(); + observer.error(errors); + }, }); return () => { diff --git a/packages/core/src/asset-modules/sitewise/session.ts b/packages/core/src/asset-modules/sitewise/session.ts index 462d65d0b..ae9f117f0 100644 --- a/packages/core/src/asset-modules/sitewise/session.ts +++ b/packages/core/src/asset-modules/sitewise/session.ts @@ -11,6 +11,7 @@ import { import { AssetSummary, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; import { RequestProcessor } from './requestProcessor'; import { AssetPropertyValue } from '@aws-sdk/client-iotsitewise'; +import { ErrorDetails } from '../../common/types'; export class SiteWiseAssetSession implements SiteWiseAssetSessionInterface { private processor: RequestProcessor; @@ -72,30 +73,48 @@ export class SiteWiseAssetSession implements SiteWiseAssetSessionInterface { requestAssetHierarchy( query: AssetHierarchyQuery, - observer: (assetSummary: HierarchyAssetSummaryList) => void + observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetHierarchy(query).subscribe(observer); } requestAssetModel( query: AssetModelQuery, - observer: (assetSummary: DescribeAssetModelResponse) => void + observer: { + next: (assetSummary: DescribeAssetModelResponse) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetModel(query).subscribe(observer); } requestAssetPropertyValue( query: AssetPropertyValueQuery, - observer: (assetSummary: AssetPropertyValue) => void + observer: { + next: (assetSummary: AssetPropertyValue) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription { return this._requestAssetPropertyValue(query).subscribe(observer); } - requestAssetSummary(query: AssetSummaryQuery, observer: (assetSummary: AssetSummary) => void): Subscription { + requestAssetSummary( + query: AssetSummaryQuery, + observer: { + next: (assetSummary: AssetSummary) => void; + error?: (err: ErrorDetails[]) => void; + } + ): Subscription { return this._requestAssetSummary(query).subscribe(observer); } - requestRootAssets(observer: (assetSummary: HierarchyAssetSummaryList) => void): Subscription { + requestRootAssets(observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + }): Subscription { return this._requestRootAssets().subscribe(observer); } diff --git a/packages/core/src/asset-modules/sitewise/siteWiseAssetModule.spec.ts b/packages/core/src/asset-modules/sitewise/siteWiseAssetModule.spec.ts index 9b8ed046b..a0982e95b 100644 --- a/packages/core/src/asset-modules/sitewise/siteWiseAssetModule.spec.ts +++ b/packages/core/src/asset-modules/sitewise/siteWiseAssetModule.spec.ts @@ -50,9 +50,8 @@ describe('fetchAsset', () => { expect(describeAsset).not.toBeCalled(); }); - // intentionally skipped, needs to be looked more into - it.skip('throws error when API returns an error', async () => { - const ERROR = 'SEV2'; + it('throws error when API returns an error', async () => { + const ERROR = { message: 'id not found', name: 'ResourceNotFoundException', $metadata: { httpStatusCode: 404 } }; const describeAsset = jest.fn().mockRejectedValue(ERROR); const module = new SiteWiseAssetModule( @@ -65,7 +64,11 @@ describe('fetchAsset', () => { const session = module.startSession(); - await expect(session.fetchAssetSummary({ assetId: ASSET_SUMMARY.id as string })).rejects.toEqual(ERROR); + await expect(session.fetchAssetSummary({ assetId: ASSET_SUMMARY.id as string })).rejects.toEqual({ + msg: ERROR.message, + type: ERROR.name, + status: ERROR.$metadata.httpStatusCode, + }); }); }); diff --git a/packages/core/src/asset-modules/sitewise/types.ts b/packages/core/src/asset-modules/sitewise/types.ts index bad0536d2..ddb982108 100644 --- a/packages/core/src/asset-modules/sitewise/types.ts +++ b/packages/core/src/asset-modules/sitewise/types.ts @@ -3,6 +3,7 @@ */ import { AssetPropertyValue, AssetSummary, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise'; import { Subscription } from 'rxjs'; +import { ErrorDetails } from '../../common/types'; export type AssetSummaryQuery = { assetId: string; @@ -53,25 +54,46 @@ export interface SiteWiseAssetModuleInterface { export interface SiteWiseAssetSessionInterface { fetchAssetSummary(query: AssetSummaryQuery): Promise; - requestAssetSummary(query: AssetSummaryQuery, observer: (assetSummary: AssetSummary) => void): Subscription; + requestAssetSummary( + query: AssetSummaryQuery, + observer: { + next: (assetSummary: AssetSummary) => void; + error?: (err: ErrorDetails[]) => void; + } + ): Subscription; fetchAssetModel(query: AssetModelQuery): Promise; - requestAssetModel(query: AssetModelQuery, observer: (assetSummary: DescribeAssetModelResponse) => void): Subscription; + requestAssetModel( + query: AssetModelQuery, + observer: { + next: (assetSummary: DescribeAssetModelResponse) => void; + error?: (err: ErrorDetails[]) => void; + } + ): Subscription; fetchAssetPropertyValue(query: AssetPropertyValueQuery): Promise; requestAssetPropertyValue( query: AssetPropertyValueQuery, - observer: (assetSummary: AssetPropertyValue) => void + observer: { + next: (assetSummary: AssetPropertyValue) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription; fetchAssetHierarchy(query: AssetHierarchyQuery): Promise; requestAssetHierarchy( query: AssetHierarchyQuery, - observer: (assetSummary: HierarchyAssetSummaryList) => void + observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + } ): Subscription; fetchRootAssets(): Promise; - requestRootAssets(observer: (assetSummary: HierarchyAssetSummaryList) => void): Subscription; + requestRootAssets(observer: { + next: (assetSummary: HierarchyAssetSummaryList) => void; + error?: (err: ErrorDetails[]) => void; + }): Subscription; close(): void; } diff --git a/packages/core/src/common/types.ts b/packages/core/src/common/types.ts new file mode 100644 index 000000000..4b3c864a4 --- /dev/null +++ b/packages/core/src/common/types.ts @@ -0,0 +1 @@ +export type ErrorDetails = { msg: string; type?: string; status?: string }; diff --git a/packages/core/src/data-module/IotAppKitDataModule.spec.ts b/packages/core/src/data-module/IotAppKitDataModule.spec.ts index b782f24cf..f0d199593 100644 --- a/packages/core/src/data-module/IotAppKitDataModule.spec.ts +++ b/packages/core/src/data-module/IotAppKitDataModule.spec.ts @@ -627,7 +627,7 @@ it('only requests latest value', () => { }); describe('error handling', () => { - const ERR_MSG = 'An error has occurred!'; + const ERR = { msg: 'An error has occurred!', type: 'ResourceNotFoundException', status: '404' }; const CACHE_WITH_ERROR: DataStreamsStore = { [DATA_STREAM_INFO.id]: { @@ -639,7 +639,7 @@ describe('error handling', () => { requestHistory: [], isLoading: false, isRefreshing: false, - error: ERR_MSG, + error: ERR, }, }, }; @@ -683,7 +683,7 @@ describe('error handling', () => { expect(timeSeriesCallback).toBeCalledTimes(1); expect(timeSeriesCallback).toBeCalledWith({ - dataStreams: [expect.objectContaining({ error: ERR_MSG })], + dataStreams: [expect.objectContaining({ error: ERR })], viewport: { start: START, end: END, diff --git a/packages/core/src/data-module/data-cache/bestStreamStore.spec.ts b/packages/core/src/data-module/data-cache/bestStreamStore.spec.ts index 2ecfedb77..d85cbb0b3 100755 --- a/packages/core/src/data-module/data-cache/bestStreamStore.spec.ts +++ b/packages/core/src/data-module/data-cache/bestStreamStore.spec.ts @@ -111,7 +111,7 @@ describe(' get best stream store based', () => { 50: { id: ID, resolution: 50, - error: 'woah an error!', + error: { msg: 'woah an error!', type: 'ResourceNotFoundException', status: '404' }, requestHistory: [], isLoading: false, isRefreshing: false, @@ -138,7 +138,7 @@ describe(' get best stream store based', () => { const ERROR_STORE: DataStreamStore = { id: ID, requestHistory: [], - error: 'woah an error!', + error: { msg: 'woah an error!', type: 'ResourceNotFoundException', status: '404' }, resolution: 0, isLoading: false, isRefreshing: false, diff --git a/packages/core/src/data-module/data-cache/caching/caching.spec.ts b/packages/core/src/data-module/data-cache/caching/caching.spec.ts index 3fbe4cd30..5b43fb2a6 100755 --- a/packages/core/src/data-module/data-cache/caching/caching.spec.ts +++ b/packages/core/src/data-module/data-cache/caching/caching.spec.ts @@ -350,7 +350,7 @@ describe('getDateRangesToRequest', () => { requestHistory: [], isLoading: false, isRefreshing: false, - error: 'errored!', + error: { msg: 'errored!', type: 'ResourceNotFoundException', status: '404' }, dataCache: EMPTY_CACHE, requestCache: createDataPointCache({ start: new Date(1991, 0, 0), diff --git a/packages/core/src/data-module/data-cache/dataActions.ts b/packages/core/src/data-module/data-cache/dataActions.ts index 15d5ae4c1..85415f7d7 100755 --- a/packages/core/src/data-module/data-cache/dataActions.ts +++ b/packages/core/src/data-module/data-cache/dataActions.ts @@ -1,6 +1,7 @@ import { Action, Dispatch } from 'redux'; import { DataStreamId, Resolution } from '@synchro-charts/core'; import { DataStream } from '../types'; +import { ErrorDetails } from '../../common/types'; /** * @@ -48,11 +49,11 @@ export interface ErrorResponse extends Action<'ERROR'> { payload: { id: DataStreamId; resolution: Resolution; - error: string; + error: ErrorDetails; }; } -export const onErrorAction = (id: DataStreamId, resolution: Resolution, error: string): ErrorResponse => ({ +export const onErrorAction = (id: DataStreamId, resolution: Resolution, error: ErrorDetails): ErrorResponse => ({ type: ERROR, payload: { id, @@ -61,7 +62,7 @@ export const onErrorAction = (id: DataStreamId, resolution: Resolution, error: s }, }); -export const onError = (id: DataStreamId, resolution: Resolution, error: string) => (dispatch: Dispatch) => { +export const onError = (id: DataStreamId, resolution: Resolution, error: ErrorDetails) => (dispatch: Dispatch) => { dispatch(onErrorAction(id, resolution, error)); }; diff --git a/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts b/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts index 81ad868ba..50f303e74 100644 --- a/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts +++ b/packages/core/src/data-module/data-cache/dataCacheWrapped.spec.ts @@ -9,7 +9,7 @@ it('initializes', () => { }); describe('shouldRequestDataStream', () => { - const ERR_MSG = 'An error has occurred!'; + const ERR = { msg: 'An error has occurred!', type: 'ResourceNotFoundException', status: '404' }; const CACHE_WITH_ERROR: DataStreamsStore = { [DATA_STREAM_INFO.id]: { @@ -21,7 +21,7 @@ describe('shouldRequestDataStream', () => { requestHistory: [], isLoading: false, isRefreshing: false, - error: ERR_MSG, + error: ERR, }, }, }; @@ -86,7 +86,7 @@ describe('actions', () => { const ID = 'some-id'; const RESOLUTION = SECOND_IN_MS; - const ERROR = 'some error'; + const ERROR = { msg: 'some error', type: 'ResourceNotFoundException', status: '404' }; dataCache.onError({ id: ID, resolution: RESOLUTION, error: ERROR }); const state = dataCache.getState() as any; diff --git a/packages/core/src/data-module/data-cache/dataCacheWrapped.ts b/packages/core/src/data-module/data-cache/dataCacheWrapped.ts index 10829dd5d..c19fe00f6 100644 --- a/packages/core/src/data-module/data-cache/dataCacheWrapped.ts +++ b/packages/core/src/data-module/data-cache/dataCacheWrapped.ts @@ -8,9 +8,9 @@ import { viewportEndDate, viewportStartDate } from '../../common/viewport'; import { getDataStreamStore } from './getDataStreamStore'; import { Observable, map, startWith, pairwise, from } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { DataStreamCallback, RequestInformation } from '../types'; +import { DataStreamCallback, RequestInformation, DataStream } from '../types'; import { toDataStreams } from './toDataStreams'; -import { DataStream } from '../types'; +import { ErrorDetails } from '../../common/types'; type StoreChange = { prevDataCache: DataStreamsStore; currDataCache: DataStreamsStore }; @@ -109,7 +109,7 @@ export class DataCache { ); }; - public onError = ({ id, resolution, error }: { id: string; resolution: Resolution; error: string }): void => { + public onError = ({ id, resolution, error }: { id: string; resolution: Resolution; error: ErrorDetails }): void => { this.dataCache.dispatch(onErrorAction(id, resolution, error)); }; 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 89d317b95..1444028f0 100755 --- a/packages/core/src/data-module/data-cache/dataReducer.spec.ts +++ b/packages/core/src/data-module/data-cache/dataReducer.spec.ts @@ -49,7 +49,10 @@ describe('loading and refreshing status', () => { onRequestAction({ id: ID, resolution: RESOLUTION, first: FIRST_DATE, last: LAST_DATE }) ); - const errorState = dataReducer(requestState, onErrorAction(ID, RESOLUTION, 'some-error')) as any; + const errorState = dataReducer( + requestState, + onErrorAction(ID, RESOLUTION, { msg: 'some-error', type: 'ResourceNotFoundException', status: '404' }) + ) as any; expect(errorState[ID][RESOLUTION]).toEqual( expect.objectContaining({ @@ -159,14 +162,14 @@ describe('on request', () => { it('retains existing error message', () => { const ID = 'some-id'; const RESOLUTION = SECOND_IN_MS; - const ERR_MSG = 'a terrible error'; + const ERR = { msg: 'a terrible error', type: 'ResourceNotFoundException', status: '404' }; const INITIAL_STATE: DataStreamsStore = { [ID]: { [RESOLUTION]: { id: ID, resolution: RESOLUTION, - error: ERR_MSG, + error: ERR, isLoading: false, isRefreshing: false, requestHistory: [], @@ -183,7 +186,7 @@ describe('on request', () => { expect((afterRequestState as any)[ID][RESOLUTION]).toEqual( expect.objectContaining({ - error: ERR_MSG, + error: ERR, }) ); }); @@ -200,7 +203,7 @@ it('returns the state back directly when a non-existent action type is passed in it('sets an error message for a previously loaded state', () => { const ID = 'my-id'; - const ERROR_MESSAGE = 'my-error!'; + const ERROR = { msg: 'my-error!', type: 'ResourceNotFoundException', status: '404' }; const INITIAL_STATE: DataStreamsStore = { [ID]: { 0: { @@ -233,12 +236,12 @@ it('sets an error message for a previously loaded state', () => { }, }, }; - const newState = dataReducer(INITIAL_STATE, onErrorAction(ID, 0, ERROR_MESSAGE)) as any; + const newState = dataReducer(INITIAL_STATE, onErrorAction(ID, 0, ERROR)) as any; expect(newState[ID][0]).toEqual( expect.objectContaining({ isLoading: false, isRefreshing: false, - error: ERROR_MESSAGE, + error: ERROR, }) ); }); @@ -533,8 +536,8 @@ describe('requests to different resolutions', () => { last: NEW_LAST_DATE, }) ); - const ERROR_MSG = 'error!'; - const newState = dataReducer(requestState, onErrorAction(ID, RESOLUTION, ERROR_MSG)) as any; + const ERROR = { msg: 'error!', type: 'ResourceNotFoundException', status: '404' }; + const newState = dataReducer(requestState, onErrorAction(ID, RESOLUTION, ERROR)) as any; // maintained other resolution expect(newState[ID][SECOND_IN_MS]).toBe(INITIAL_STATE[ID][SECOND_IN_MS]); @@ -542,7 +545,7 @@ describe('requests to different resolutions', () => { expect(newState[ID][RESOLUTION]).toEqual({ id: ID, resolution: RESOLUTION, - error: ERROR_MSG, + error: ERROR, isLoading: false, isRefreshing: false, requestHistory: [ diff --git a/packages/core/src/data-module/data-cache/types.ts b/packages/core/src/data-module/data-cache/types.ts index 633e3bc39..a097be916 100755 --- a/packages/core/src/data-module/data-cache/types.ts +++ b/packages/core/src/data-module/data-cache/types.ts @@ -1,5 +1,6 @@ import { DataPoint, Primitive } from '@synchro-charts/core'; import { IntervalStructure } from '../../common/intervalStructure'; +import { ErrorDetails } from '../../common/types'; type TTL = number; export type TTLDurationMapping = { @@ -25,7 +26,7 @@ export type DataStreamStore = { isLoading: boolean; // When data is being requested, whether or not data has been previously requested isRefreshing: boolean; - error?: string; + error?: ErrorDetails; }; export type DataStreamsStore = { diff --git a/packages/core/src/data-module/types.ts b/packages/core/src/data-module/types.ts index 3d8fdcb3b..51404321f 100644 --- a/packages/core/src/data-module/types.ts +++ b/packages/core/src/data-module/types.ts @@ -15,6 +15,7 @@ import { import { RefId, TimeSeriesData } from '../iotsitewise/time-series-data/types'; import { CacheSettings } from './data-cache/types'; import { DataPoint, StreamAssociation } from '@synchro-charts/core/dist/types/utils/dataTypes'; +import { ErrorDetails } from '../common/types'; export type RequestInformation = { id: DataStreamId; @@ -47,7 +48,7 @@ export interface DataStream { associatedStreams?: StreamAssociation[]; isLoading?: boolean; isRefreshing?: boolean; - error?: string; + error?: ErrorDetails; } export type DataSource = { @@ -81,7 +82,15 @@ export type DataStreamQuery = { export type AnyDataStreamQuery = DataStreamQuery & any; -export type ErrorCallback = ({ id, resolution, error }: { id: string; resolution: number; error: string }) => void; +export type ErrorCallback = ({ + id, + resolution, + error, +}: { + id: string; + resolution: number; + error: ErrorDetails; +}) => void; export type SubscriptionUpdate = Partial, 'emit'>>; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f63b2434b..fd7138a25 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,7 @@ import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise/dist-types/IoTSit import { Credentials } from '@aws-sdk/types'; import { Provider as AWSCredentialsProvider } from '@aws-sdk/types/dist-types/util'; import { DataSource, DataStreamQuery } from './data-module/types'; -import { AssetTreeSubscription, SiteWiseAssetTreeCallback, SiteWiseAssetTreeQuery } from './asset-modules'; +import { AssetTreeSubscription, SiteWiseAssetTreeObserver, SiteWiseAssetTreeQuery } from './asset-modules'; import { TimeSeriesDataRequest } from './data-module/data-cache/requestTypes'; export * from './data-module/data-cache/requestTypes'; @@ -15,6 +15,7 @@ export * from './iotsitewise/time-series-data/provider'; export * from './testing'; // @todo: build as a separate bundle export * from './data-module/types'; export * from './iotsitewise/time-series-data/types'; +export * from './common/types'; export type IoTAppKitInitInputs = | { @@ -31,7 +32,7 @@ export interface IoTAppKit { session: (componentId: string) => IoTAppKitComponentSession; registerTimeSeriesDataSource: (dataSource: DataSource) => void; /** @todo: create asset provider */ - subscribeToAssetTree: (query: SiteWiseAssetTreeQuery, callback: SiteWiseAssetTreeCallback) => AssetTreeSubscription; + subscribeToAssetTree: (query: SiteWiseAssetTreeQuery, observer: SiteWiseAssetTreeObserver) => AssetTreeSubscription; } export interface Closeable { diff --git a/packages/core/src/iotsitewise/__mocks__/mockWidgetProperties.ts b/packages/core/src/iotsitewise/__mocks__/mockWidgetProperties.ts index 4257dce08..d5f5b84e0 100755 --- a/packages/core/src/iotsitewise/__mocks__/mockWidgetProperties.ts +++ b/packages/core/src/iotsitewise/__mocks__/mockWidgetProperties.ts @@ -1,6 +1,5 @@ import { COMPARISON_OPERATOR, - DataStream, DataStreamInfo, DataType, StatusIcon, @@ -9,7 +8,8 @@ import { ViewPort, } from '@synchro-charts/core'; import { DAY_IN_MS } from '../../common/time'; -import {toDataStreamId} from "../time-series-data/util/dataStreamId"; +import { toDataStreamId } from '../time-series-data/util/dataStreamId'; +import { DataStream } from '../../data-module/types'; const VIEW_PORT: ViewPort = { start: new Date(2000, 0, 0, 0), diff --git a/packages/core/src/iotsitewise/time-series-data/client/client.spec.ts b/packages/core/src/iotsitewise/time-series-data/client/client.spec.ts index 75533c34f..e6995bf5e 100644 --- a/packages/core/src/iotsitewise/time-series-data/client/client.spec.ts +++ b/packages/core/src/iotsitewise/time-series-data/client/client.spec.ts @@ -1,4 +1,4 @@ -import { AggregateType } from '@aws-sdk/client-iotsitewise'; +import { AggregateType, ResourceNotFoundException } from '@aws-sdk/client-iotsitewise'; import { SiteWiseClient } from './client'; import { createMockSiteWiseSDK } from '../../__mocks__/iotsitewiseSDK'; import { @@ -17,7 +17,13 @@ it('initializes', () => { describe('getHistoricalPropertyDataPoints', () => { it('calls onError on failure', async () => { - const ERR = new Error('some error'); + const ERR: Partial = { + name: 'ResourceNotFoundException', + message: 'assetId 1 not found', + $metadata: { + httpStatusCode: 404, + }, + }; const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(ERR); const assetId = 'some-asset-id'; const propertyId = 'some-property-id'; @@ -45,7 +51,15 @@ describe('getHistoricalPropertyDataPoints', () => { await client.getHistoricalPropertyDataPoints({ query, requestInformations, onSuccess, onError }); - expect(onError).toBeCalled(); + expect(onError).toBeCalledWith( + expect.objectContaining({ + error: { + msg: ERR.message, + type: ERR.name, + status: ERR.$metadata?.httpStatusCode, + }, + }) + ); }); it('returns data point on success', async () => { diff --git a/packages/core/src/iotsitewise/time-series-data/client/getAggregatedPropertyDataPoints.ts b/packages/core/src/iotsitewise/time-series-data/client/getAggregatedPropertyDataPoints.ts index 63a24d83e..d6ae00fae 100644 --- a/packages/core/src/iotsitewise/time-series-data/client/getAggregatedPropertyDataPoints.ts +++ b/packages/core/src/iotsitewise/time-series-data/client/getAggregatedPropertyDataPoints.ts @@ -87,7 +87,11 @@ const getAggregatedPropertyDataPointsForProperty = ({ }) .catch((err) => { const id = toDataStreamId({ assetId, propertyId }); - onError({ id, resolution: parseDuration(resolution), error: err.message }); + onError({ + id, + resolution: parseDuration(resolution), + error: { msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }, + }); }); }; diff --git a/packages/core/src/iotsitewise/time-series-data/client/getHistoricalPropertyDataPoints.ts b/packages/core/src/iotsitewise/time-series-data/client/getHistoricalPropertyDataPoints.ts index 7f3ad3417..fb938e8e9 100644 --- a/packages/core/src/iotsitewise/time-series-data/client/getHistoricalPropertyDataPoints.ts +++ b/packages/core/src/iotsitewise/time-series-data/client/getHistoricalPropertyDataPoints.ts @@ -68,7 +68,11 @@ const getHistoricalPropertyDataPointsForProperty = ({ }) .catch((err) => { const id = toDataStreamId({ assetId, propertyId }); - onError({ id, resolution: 0, error: err.message }); + onError({ + id, + resolution: 0, + error: { msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }, + }); }); }; diff --git a/packages/core/src/iotsitewise/time-series-data/client/getLatestPropertyDataPoint.ts b/packages/core/src/iotsitewise/time-series-data/client/getLatestPropertyDataPoint.ts index 8b39e0292..79c4402f9 100644 --- a/packages/core/src/iotsitewise/time-series-data/client/getLatestPropertyDataPoint.ts +++ b/packages/core/src/iotsitewise/time-series-data/client/getLatestPropertyDataPoint.ts @@ -42,9 +42,13 @@ export const getLatestPropertyDataPoint = async ({ assetId, propertyId, })) - .catch((error) => { + .catch((err) => { const dataStreamId = toDataStreamId({ assetId, propertyId }); - onError({ id: dataStreamId, resolution: 0, error: error.message }); + onError({ + id: dataStreamId, + resolution: 0, + error: { msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }, + }); return undefined; }); } diff --git a/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts b/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts index 1d84af10c..8acc74294 100644 --- a/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts +++ b/packages/core/src/iotsitewise/time-series-data/data-source.spec.ts @@ -1,5 +1,5 @@ import flushPromises from 'flush-promises'; -import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise'; +import { IoTSiteWiseClient, ResourceNotFoundException } from '@aws-sdk/client-iotsitewise'; import { createDataSource, SITEWISE_DATA_SOURCE } from './data-source'; import { MINUTE_IN_MS, HOUR_IN_MS, MONTH_IN_MS } from '../../common/time'; import { SiteWiseDataStreamQuery } from './types'; @@ -76,8 +76,14 @@ describe('initiateRequest', () => { describe('fetch latest before end', () => { describe('on error', () => { it('calls `onError` callback', async () => { - const ERR_MESSAGE = 'some critical error! page oncall immediately'; - const getAssetPropertyValue = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); + const ERR: Partial = { + name: 'ResourceNotFoundException', + message: 'assetId 1 not found', + $metadata: { + httpStatusCode: 404, + }, + }; + const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR); const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); @@ -117,7 +123,11 @@ describe('initiateRequest', () => { expect(onError).toBeCalledWith({ id: toDataStreamId({ assetId: ASSET_1, propertyId: PROPERTY_1 }), resolution: 0, - error: ERR_MESSAGE, + error: { + msg: ERR.message, + type: ERR.name, + status: ERR.$metadata?.httpStatusCode, + }, }); }); }); @@ -385,8 +395,14 @@ describe('e2e through data-module', () => { it('reports error occurred on request initiation', async () => { const dataModule = new IotAppKitDataModule(); - const ERR_MESSAGE = 'some critical error! page oncall immediately'; - const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); + const ERR: Partial = { + name: 'ResourceNotFoundException', + message: 'assetId 1 not found', + $metadata: { + httpStatusCode: 404, + }, + }; + const getAssetPropertyValueHistory = jest.fn().mockRejectedValue(ERR); const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValueHistory }); const dataSource = createDataSource(mockSDK); @@ -418,7 +434,11 @@ describe('e2e through data-module', () => { dataStreams: [ expect.objectContaining({ id: toDataStreamId({ assetId, propertyId }), - error: ERR_MESSAGE, + error: { + msg: ERR.message, + type: ERR.name, + status: ERR.$metadata?.httpStatusCode, + }, isLoading: false, isRefreshing: false, }), @@ -434,8 +454,14 @@ describe('e2e through data-module', () => { it('reports error occurred on request initiation', async () => { const dataModule = new IotAppKitDataModule(); - const ERR_MESSAGE = 'some critical error! page oncall immediately'; - const getAssetPropertyValue = jest.fn().mockRejectedValue(new Error(ERR_MESSAGE)); + const ERR: Partial = { + name: 'ResourceNotFoundException', + message: 'assetId 1 not found', + $metadata: { + httpStatusCode: 404, + }, + }; + const getAssetPropertyValue = jest.fn().mockRejectedValue(ERR); const mockSDK = createMockSiteWiseSDK({ getAssetPropertyValue }); const dataSource = createDataSource(mockSDK); @@ -470,7 +496,11 @@ describe('e2e through data-module', () => { dataStreams: [ expect.objectContaining({ id: toDataStreamId({ assetId, propertyId }), - error: ERR_MESSAGE, + error: { + msg: ERR.message, + type: ERR.name, + status: ERR.$metadata?.httpStatusCode, + }, isLoading: false, isRefreshing: false, }), diff --git a/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts index ecd192894..0224c8296 100644 --- a/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts +++ b/packages/core/src/iotsitewise/time-series-data/subscribeToTimeSeriesData.ts @@ -3,6 +3,7 @@ import { completeDataStreams } from '../../completeDataStreams'; import { SiteWiseDataStreamQuery, TimeSeriesData } from './types'; import { MinimalViewPortConfig } from '@synchro-charts/core'; import { SiteWiseAssetSession } from '../../asset-modules'; +import { ErrorDetails } from '../../common/types'; import { DataModule, DataModuleSubscription, DataStream, SubscriptionUpdate } from '../../data-module/types'; export const subscribeToTimeSeriesData = @@ -14,6 +15,8 @@ export const subscribeToTimeSeriesData = const assetModels: Record = {}; + const errors: Record = {}; + const emit = () => { callback({ dataStreams: completeDataStreams({ dataStreams, assetModels }), @@ -43,6 +46,11 @@ export const subscribeToTimeSeriesData = assetModels[asset.assetId] = assetModelResponse; emit(); } + }) + .catch((err: ErrorDetails) => { + // TODO: Currently these are not used anywhere. Do something with these errors. + errors[asset.assetId] = err; + // emit(); }); }); });