diff --git a/packages/core/src/data-module/data-cache/requestTypes.ts b/packages/core/src/data-module/data-cache/requestTypes.ts index 5957c5a32..6c3573f28 100755 --- a/packages/core/src/data-module/data-cache/requestTypes.ts +++ b/packages/core/src/data-module/data-cache/requestTypes.ts @@ -8,13 +8,16 @@ export type HistoricalViewport = { start: Date; end: Date; group?: string }; export type Viewport = DurationViewport | HistoricalViewport; +export type DataRequest = { + viewport?: Viewport; + settings?: TimeSeriesDataRequestSettings; +}; /** * Request Information utilized by consumers of the widgets to connect the `data-provider` to their data source. */ -export type TimeSeriesDataRequest = { +export interface TimeSeriesDataRequest extends DataRequest { viewport: Viewport; - settings?: TimeSeriesDataRequestSettings; -}; +} export type ResolutionConfig = ResolutionMapping | string; diff --git a/packages/core/src/data-module/types.ts b/packages/core/src/data-module/types.ts index a176f7be3..f954bc395 100644 --- a/packages/core/src/data-module/types.ts +++ b/packages/core/src/data-module/types.ts @@ -11,22 +11,30 @@ export type StreamAssociation = { }; export type Timestamp = number; -export type DataPoint = { - x: Timestamp; + +export type DataPointBase = { y: T; }; +export interface DataPoint extends DataPointBase { + x: Timestamp; +} + export type Resolution = number; export type Primitive = string | number | boolean; export type DataStreamId = string; -export type TimeSeriesData = { +export type DataBase = { + dataStreams: DataStreamBase[]; +}; + +export interface TimeSeriesData extends DataBase { dataStreams: DataStream[]; viewport: Viewport; thresholds: Threshold[]; -}; +} // Reference which can be used to associate styles to the associated results from a query export type RefId = string; @@ -53,12 +61,19 @@ export type ComparisonOperator = 'LT' | 'GT' | 'LTE' | 'GTE' | 'EQ' | 'CONTAINS' export type StatusIconType = 'error' | 'active' | 'normal' | 'acknowledged' | 'snoozed' | 'disabled' | 'latched'; -export interface DataStream { +export interface DataStreamBase { + data: DataPointBase[]; + error?: ErrorDetails; + dataType?: DataType; + // Mechanism to associate some information about the data stream + meta?: Record; +} + +export interface DataStream extends DataStreamBase { id: DataStreamId; data: DataPoint[]; resolution: number; aggregationType?: AggregateType; - dataType?: DataType; refId?: string; name?: string; detailedName?: string; @@ -68,9 +83,6 @@ export interface DataStream { associatedStreams?: StreamAssociation[]; isLoading?: boolean; isRefreshing?: boolean; - error?: ErrorDetails; - // Mechanism to associate some information about the data stream - meta?: Record; } export type DataSource = { diff --git a/packages/scene-composer/src/components/StateManager.spec.tsx b/packages/scene-composer/src/components/StateManager.spec.tsx index 7d9a6f477..2d37cf212 100644 --- a/packages/scene-composer/src/components/StateManager.spec.tsx +++ b/packages/scene-composer/src/components/StateManager.spec.tsx @@ -316,6 +316,10 @@ describe('StateManager', () => { }); expect(mockBuild).toBeCalledTimes(2); + expect(mockBuild).toHaveBeenNthCalledWith(2, 'default', { + viewport, + settings: { refreshRate: undefined, fetchFromStartToEnd: true }, + }); expect(mockCombinedPrvider.subscribe).toBeCalledTimes(1); expect(mockCombinedPrvider.unsubscribe).not.toBeCalled(); @@ -323,7 +327,7 @@ describe('StateManager', () => { await act(async () => { container.update( { }); expect(mockBuild).toBeCalledTimes(3); + expect(mockBuild).toHaveBeenNthCalledWith(3, 'default', { + viewport: { duration: '5m' }, + settings: { refreshRate: 5000, fetchFromStartToEnd: true }, + }); expect(mockCombinedPrvider.subscribe).toBeCalledTimes(2); expect(mockCombinedPrvider.unsubscribe).toBeCalledTimes(1); @@ -342,7 +350,7 @@ describe('StateManager', () => { await act(async () => { container.unmount( = ({ settings: { // only support default settings for now until when customization is needed fetchFromStartToEnd: true, + refreshRate: (viewport as DurationViewport).duration ? 5000 : undefined, }, }), ), diff --git a/packages/scene-composer/src/hooks/useBindingData.ts b/packages/scene-composer/src/hooks/useBindingData.ts index 42abd87d0..2f3c14b17 100644 --- a/packages/scene-composer/src/hooks/useBindingData.ts +++ b/packages/scene-composer/src/hooks/useBindingData.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { TimeSeriesData, Primitive } from '@iot-app-kit/core'; +import { DataBase, TimeSeriesData, Primitive, DurationViewport } from '@iot-app-kit/core'; import { isEmpty } from 'lodash'; import { ITwinMakerEntityDataBindingContext } from '@iot-app-kit/source-iottwinmaker'; @@ -47,12 +47,12 @@ const useBindingData = ( settings: { // only support default settings for now until when customization is needed fetchFromStartToEnd: true, + refreshRate: (viewport as DurationViewport).duration ? 5000 : undefined, }, }); provider.subscribe({ - // TODO: support static data - next: (results: TimeSeriesData[]) => { + next: (results: TimeSeriesData[] | DataBase[]) => { if (isEmpty(results.at(0)?.dataStreams)) { log?.info('No data returned'); return; diff --git a/packages/scene-composer/src/hooks/useBindingQueries.ts b/packages/scene-composer/src/hooks/useBindingQueries.ts index 729d69a25..dee2d602d 100644 --- a/packages/scene-composer/src/hooks/useBindingQueries.ts +++ b/packages/scene-composer/src/hooks/useBindingQueries.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { TimeSeriesDataRequest, Query, TimeSeriesData } from '@iot-app-kit/core'; +import { DataBase, DataRequest, Query, TimeSeriesData } from '@iot-app-kit/core'; import { isEmpty } from 'lodash'; import { useSceneComposerId } from '../common/sceneComposerIdContext'; @@ -17,8 +17,7 @@ import { applyDataBindingTemplate } from '../utils/dataBindingTemplateUtils'; */ const useBindingQueries = ( bindings: IValueDataBinding[] | undefined, -): // TODO: update data type for static data when available -{ queries: (Query | undefined)[] | undefined } => { +): { queries: (Query | undefined)[] | undefined } => { const sceneComposerId = useSceneComposerId(); const valueDataBindingProvider = useStore(sceneComposerId)( (state) => state.getEditorConfig().valueDataBindingProvider, diff --git a/packages/scene-composer/src/interfaces/sceneViewer.ts b/packages/scene-composer/src/interfaces/sceneViewer.ts index c0b62600d..de1a9b2da 100644 --- a/packages/scene-composer/src/interfaces/sceneViewer.ts +++ b/packages/scene-composer/src/interfaces/sceneViewer.ts @@ -1,5 +1,5 @@ import { SceneLoader, TwinMakerSceneMetadataModule } from '@iot-app-kit/source-iottwinmaker'; -import { DataStream, TimeQuery, TimeSeriesData, TimeSeriesDataRequest, Viewport } from '@iot-app-kit/core'; +import { DataStream, TimeSeriesDataQuery, Viewport } from '@iot-app-kit/core'; import { IDataBindingTemplate, ISelectedDataBinding, IValueDataBindingProvider } from './dataBinding'; import { SelectionChangedEventCallback, WidgetClickEventCallback } from './components'; @@ -51,7 +51,7 @@ export interface SceneViewerPropsShared { * * Note: Need to provide a viewport to make it work. */ - queries?: TimeQuery[]; + queries?: TimeSeriesDataQuery[]; /** * Specifies the time range of the dataStreams or the range to trigger the queries. */ diff --git a/packages/scene-composer/src/utils/dataStreamUtils.ts b/packages/scene-composer/src/utils/dataStreamUtils.ts index a30393694..97641bf4c 100644 --- a/packages/scene-composer/src/utils/dataStreamUtils.ts +++ b/packages/scene-composer/src/utils/dataStreamUtils.ts @@ -34,7 +34,9 @@ export const convertDataStreamsToDataInput = (streams: DataStream[], viewport: V labels, // the boolean value y will be a string 'true' or 'false' when data is coming from app kit data source, // but can be actual boolean when from other source - values: stream.data.map(({ y }) => (stream.dataType === 'BOOLEAN' ? y === 'true' || (y as any) === true : y)), + values: stream.data.map(({ y }) => + stream.dataType === 'BOOLEAN' ? y === 'true' || (y as boolean) === true : y, + ), }, ]; diff --git a/packages/source-iottwinmaker/src/__mocks__/iottwinmakerSDK.ts b/packages/source-iottwinmaker/src/__mocks__/iottwinmakerSDK.ts index a14653066..b938d9b27 100644 --- a/packages/source-iottwinmaker/src/__mocks__/iottwinmakerSDK.ts +++ b/packages/source-iottwinmaker/src/__mocks__/iottwinmakerSDK.ts @@ -3,6 +3,8 @@ import { ExecuteQueryCommandOutput, GetEntityCommandInput, GetEntityCommandOutput, + GetPropertyValueCommandInput, + GetPropertyValueCommandOutput, GetPropertyValueHistoryCommandInput, GetPropertyValueHistoryCommandOutput, GetSceneCommandInput, @@ -19,6 +21,7 @@ const nonOverriddenMock = () => Promise.reject(new Error('Mock method not overri export const createMockTwinMakerSDK = ({ getEntity = nonOverriddenMock, getPropertyValueHistory = nonOverriddenMock, + getPropertyValue = nonOverriddenMock, getScene = nonOverriddenMock, listEntities = nonOverriddenMock, updateScene = nonOverriddenMock, @@ -28,6 +31,9 @@ export const createMockTwinMakerSDK = ({ getPropertyValueHistory?: ( input: GetPropertyValueHistoryCommandInput ) => Promise; + getPropertyValue?: ( + input: GetPropertyValueCommandInput + ) => Promise; getScene?: (input: GetSceneCommandInput) => Promise; listEntities?: (input: ListEntitiesCommandInput) => Promise; updateScene?: (input: UpdateSceneCommandInput) => Promise; @@ -44,6 +50,8 @@ export const createMockTwinMakerSDK = ({ return getEntity(command.input); case 'GetPropertyValueHistoryCommand': return getPropertyValueHistory(command.input); + case 'GetPropertyValueCommand': + return getPropertyValue(command.input); case 'GetSceneCommand': return getScene(command.input); case 'ListEntitiesCommand': diff --git a/packages/source-iottwinmaker/src/common/queryTypes.ts b/packages/source-iottwinmaker/src/common/queryTypes.ts new file mode 100644 index 000000000..1fce5ac09 --- /dev/null +++ b/packages/source-iottwinmaker/src/common/queryTypes.ts @@ -0,0 +1,29 @@ +import type { RefId } from '@iot-app-kit/core'; + +export type PropertyQueryInfo = { + propertyName: string; + + refId?: RefId; +}; + +export type TwinMakerBaseQuery = { + properties: PropertyQueryInfo[]; +}; + +// Query for static property value +export interface TwinMakerEntityPropertyValueQuery extends TwinMakerBaseQuery { + entityId: string; + componentName: string; +} + +// Query for time series property value +export type TwinMakerEntityHistoryQuery = TwinMakerEntityPropertyValueQuery; + +// Query for time series property value for a component type +export interface TwinMakerComponentHistoryQuery extends TwinMakerBaseQuery { + componentTypeId: string; +} + +export type TwinMakerHistoryQuery = TwinMakerEntityHistoryQuery | TwinMakerComponentHistoryQuery; +export type TwinMakerPropertyValueQuery = TwinMakerEntityPropertyValueQuery; +export type TwinMakerQuery = TwinMakerHistoryQuery | TwinMakerPropertyValueQuery; diff --git a/packages/source-iottwinmaker/src/common/types.ts b/packages/source-iottwinmaker/src/common/types.ts index 9937290aa..090519945 100644 --- a/packages/source-iottwinmaker/src/common/types.ts +++ b/packages/source-iottwinmaker/src/common/types.ts @@ -1,2 +1 @@ -// TODO: Use the Primitive type used by other app kit datasource -export declare type Primitive = string | number | boolean; +export type { Primitive } from '@iot-app-kit/core'; diff --git a/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.spec.ts b/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.spec.ts index 5c8912340..172b19e8d 100644 --- a/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.spec.ts +++ b/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.spec.ts @@ -26,27 +26,44 @@ describe('createEntityPropertyBindingProvider', () => { }); it('should return a correct data binding provider store', async () => { - const provider = createEntityPropertyBindingProvider({ metadataModule, timeSeriesDataQuery: jest.fn() }); + const provider = createEntityPropertyBindingProvider({ + metadataModule, + timeSeriesDataQuery: jest.fn(), + propertyValueQuery: jest.fn(), + }); const store = provider.createStore(false); expect(store).toBeDefined(); expect((store as EntityPropertyBindingProviderStore)['metadataModule']).toBe(metadataModule); }); - it('should return query when data binding context has all values', async () => { + it('should return time series query when data binding context has all values', async () => { const query = { key: 'value' }; const provider = createEntityPropertyBindingProvider({ metadataModule, timeSeriesDataQuery: jest.fn().mockReturnValue(query), + propertyValueQuery: jest.fn(), }); const result = provider.createQuery(mockDataBindingInput); expect(result).toBe(query); }); + it('should return static query when data binding context has all values', async () => { + const query = { key: 'value' }; + const provider = createEntityPropertyBindingProvider({ + metadataModule, + timeSeriesDataQuery: jest.fn().mockReturnValue({ random: 'abc' }), + propertyValueQuery: jest.fn().mockReturnValue(query), + }); + const result = provider.createQuery({ ...mockDataBindingInput, isStaticData: true }); + expect(result).toBe(query); + }); + it('should not return query when data binding context misses property name', async () => { const query = { key: 'value' }; const provider = createEntityPropertyBindingProvider({ metadataModule, timeSeriesDataQuery: jest.fn().mockReturnValue(query), + propertyValueQuery: jest.fn(), }); const result = provider.createQuery({ dataBindingContext: { ...mockDataBindingInput.dataBindingContext, propertyName: undefined }, diff --git a/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.ts b/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.ts index 5cf6b063e..508ecb823 100644 --- a/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.ts +++ b/packages/source-iottwinmaker/src/data-binding-provider/createEntityPropertyBindingProvider.ts @@ -1,17 +1,19 @@ import { TwinMakerErrorCode } from '../common/error'; import { ITwinMakerEntityDataBindingContext, IValueDataBinding, IValueDataBindingProvider } from './types'; import { EntityPropertyBindingProviderStore } from './EntityPropertyBindingProviderStore'; -import { ErrorDetails, TimeSeriesDataQuery } from '@iot-app-kit/core'; -import { TwinMakerQuery } from '../time-series-data/types'; +import { ErrorDetails, Query, TimeSeriesDataQuery, DataBase, DataRequest } from '@iot-app-kit/core'; +import { TwinMakerHistoryQuery, TwinMakerPropertyValueQuery } from '../common/queryTypes'; import { TwinMakerMetadataModule } from '../metadata-module/TwinMakerMetadataModule'; export const createEntityPropertyBindingProvider = ({ metadataModule, timeSeriesDataQuery, + propertyValueQuery, onError, }: { metadataModule: TwinMakerMetadataModule; - timeSeriesDataQuery: (query: TwinMakerQuery) => TimeSeriesDataQuery; + timeSeriesDataQuery: (query: TwinMakerHistoryQuery) => TimeSeriesDataQuery; + propertyValueQuery: (query: TwinMakerPropertyValueQuery) => Query; onError?: (errorCode: TwinMakerErrorCode, errorDetails?: ErrorDetails) => void; }): IValueDataBindingProvider => { return { @@ -21,7 +23,6 @@ export const createEntityPropertyBindingProvider = ({ metadataModule, onError, }), - // TODO: add non time series data support createQuery: (dataBinding: IValueDataBinding) => { const context = dataBinding.dataBindingContext as ITwinMakerEntityDataBindingContext; if (!context || !context.entityId || !context.componentName || !context.propertyName) { @@ -29,8 +30,11 @@ export const createEntityPropertyBindingProvider = ({ } if (dataBinding.isStaticData) { - // TODO: return property value query - return undefined; + return propertyValueQuery({ + entityId: context.entityId, + componentName: context.componentName, + properties: [{ propertyName: context.propertyName }], + }); } return timeSeriesDataQuery({ diff --git a/packages/source-iottwinmaker/src/data-binding-provider/types.ts b/packages/source-iottwinmaker/src/data-binding-provider/types.ts index f0e5cba92..feba817a2 100644 --- a/packages/source-iottwinmaker/src/data-binding-provider/types.ts +++ b/packages/source-iottwinmaker/src/data-binding-provider/types.ts @@ -1,4 +1,4 @@ -import { TimeSeriesDataRequest, Query, TimeSeriesData } from '@iot-app-kit/core'; +import { DataBase, DataRequest, Query, TimeSeriesData } from '@iot-app-kit/core'; export interface ITwinMakerEntityDataBindingContext { entityId: string; @@ -75,7 +75,7 @@ export interface IValueDataBindingStore { export interface IValueDataBindingProvider { createStore(isDataBindingTemplateProvider: boolean): IValueDataBindingStore; - createQuery(dataBinding: IValueDataBinding): Query | undefined; // TODO: add non time series data support + createQuery(dataBinding: IValueDataBinding): Query | undefined; } /************************************************ diff --git a/packages/source-iottwinmaker/src/index.ts b/packages/source-iottwinmaker/src/index.ts index 4b6a3eec2..9c028ea51 100644 --- a/packages/source-iottwinmaker/src/index.ts +++ b/packages/source-iottwinmaker/src/index.ts @@ -3,7 +3,14 @@ export * from './initialize'; export * from './types'; export { createPropertyIndentifierKey } from './video-data/utils/twinmakerUtils'; export * from './video-data/types'; -export * from './time-series-data/types'; +export type { + PropertyQueryInfo, + TwinMakerBaseQuery, + TwinMakerEntityHistoryQuery, + TwinMakerComponentHistoryQuery, + TwinMakerQuery, +} from './common/queryTypes'; +export type { TwinMakerDataStreamQuery, TwinMakerDataStreamIdComponent } from './time-series-data/types'; export { toDataStreamId, fromDataStreamId } from './time-series-data/utils/dataStreamId'; export { decorateDataBindingTemplate, diff --git a/packages/source-iottwinmaker/src/initialize.spec.ts b/packages/source-iottwinmaker/src/initialize.spec.ts index 871a241ba..99d5a6c65 100644 --- a/packages/source-iottwinmaker/src/initialize.spec.ts +++ b/packages/source-iottwinmaker/src/initialize.spec.ts @@ -1,20 +1,22 @@ import { initialize } from './initialize'; import { TwinMakerTimeSeriesDataProvider } from './time-series-data/provider'; -import type { Credentials } from '@aws-sdk/types'; +import type { AwsCredentialIdentity } from '@aws-sdk/types'; import { createMockSiteWiseSDK } from './__mocks__/iotsitewiseSDK'; import { createMockTwinMakerSDK } from './__mocks__/iottwinmakerSDK'; import { createMockKinesisVideoArchivedMediaSDK } from './__mocks__/kinesisVideoArchivedMediaSDK'; import { createMockKinesisVideoSDK } from './__mocks__/kinesisVideoSDK'; import { createMockSecretsManagerSDK } from './__mocks__/secretsManagerSDK'; +import { TwinMakerPropertyValueDataProvider } from './property-value/provider'; describe('initialize', () => { + const query = { entityId: 'entity-1', componentName: 'comp-1', properties: [{ propertyName: 'prop-1' }] }; it('should return timeSeries query data provider', async () => { - const init = initialize('ws-id', { awsCredentials: {} as Credentials, awsRegion: 'us-east-1' }); - const query = { entityId: 'entity-1', componentName: 'comp-1', properties: [{ propertyName: 'prop-1' }] }; + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); const params = { settings: { fetchFromStartToEnd: true }, viewport: { start: new Date(), end: new Date() } }; - const provider = init.query.timeSeriesData(query).build('random', params) as TwinMakerTimeSeriesDataProvider; + const provider = init.query.timeSeriesData(query).build('random', params); - expect(provider.input).toEqual({ + expect(provider instanceof TwinMakerTimeSeriesDataProvider).toBeTrue(); + expect((provider as TwinMakerTimeSeriesDataProvider).input).toEqual({ queries: [ { workspaceId: 'ws-id', @@ -28,8 +30,18 @@ describe('initialize', () => { expect(provider.updateViewport).toBeFunction(); }); + it('should return propertyValue query data provider', async () => { + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); + const params = { settings: { refreshRate: 5000 } }; + const provider = init.query.propertyValue(query).build('random', params); + + expect(provider instanceof TwinMakerPropertyValueDataProvider).toBeTrue(); + expect(provider.subscribe).toBeFunction(); + expect(provider.unsubscribe).toBeFunction(); + }); + it('should return S3SceneLoader', async () => { - const init = initialize('ws-id', { awsCredentials: {} as Credentials, awsRegion: 'us-east-1' }); + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); const result = init.s3SceneLoader('scene-id'); expect(result).toBeDefined(); @@ -38,7 +50,7 @@ describe('initialize', () => { }); it('should return valueDataBindingProviders', async () => { - const init = initialize('ws-id', { awsCredentials: {} as Credentials, awsRegion: 'us-east-1' }); + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); const result = init.valueDataBindingProviders(); expect(result).toBeDefined(); @@ -47,7 +59,7 @@ describe('initialize', () => { it('should return sceneMetadataModule', async () => { const init = initialize('ws-id', { - awsCredentials: {} as Credentials, + awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1', iotTwinMakerClient: createMockTwinMakerSDK({}), iotSiteWiseClient: createMockSiteWiseSDK({}), @@ -63,7 +75,7 @@ describe('initialize', () => { }); it('should return VideoData', async () => { - const init = initialize('ws-id', { awsCredentials: {} as Credentials, awsRegion: 'us-east-1' }); + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); const result = init.videoData({ kvsStreamName: 'kvs-stream-name' }); expect(result).toBeDefined(); @@ -72,15 +84,20 @@ describe('initialize', () => { }); it('converts a time series data query to string with contents that uniquely represent the query', () => { - const { query } = initialize('ws-id', { awsCredentials: {} as Credentials, awsRegion: 'us-east-1' }); - const timeSeriesDataQuery = query.timeSeriesData({ - entityId: 'entity-1', - componentName: 'comp-1', - properties: [{ propertyName: 'prop-1' }], - }); + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); + const timeSeriesDataQuery = init.query.timeSeriesData(query); expect(timeSeriesDataQuery.toQueryString()).toMatchInlineSnapshot( `"{"source":"iottwinmaker","queryType":"time-series-data","query":{"entityId":"entity-1","componentName":"comp-1","properties":[{"propertyName":"prop-1"}]}}"` ); }); + + it('converts a property value data query to string with contents that uniquely represent the query', () => { + const init = initialize('ws-id', { awsCredentials: {} as AwsCredentialIdentity, awsRegion: 'us-east-1' }); + const propertyValueQuery = init.query.propertyValue(query); + + expect(propertyValueQuery.toQueryString()).toMatchInlineSnapshot( + `"{"source":"iottwinmaker","queryType":"property-value","query":{"entityId":"entity-1","componentName":"comp-1","properties":[{"propertyName":"prop-1"}]}}"` + ); + }); }); diff --git a/packages/source-iottwinmaker/src/initialize.ts b/packages/source-iottwinmaker/src/initialize.ts index c0b86ed88..c291ce3c9 100644 --- a/packages/source-iottwinmaker/src/initialize.ts +++ b/packages/source-iottwinmaker/src/initialize.ts @@ -18,16 +18,18 @@ import { S3SceneLoader } from './scene-module/S3SceneLoader'; import { SceneMetadataModule } from './scene-module/SceneMetadataModule'; import { KGDataModule } from './knowledgeGraph-module/KGDataModule'; import { VideoDataImpl } from './video-data/VideoData'; -import { ErrorDetails, TimeSeriesDataModule } from '@iot-app-kit/core'; +import { DataBase, ErrorDetails, Query, TimeSeriesDataModule, DataRequest } from '@iot-app-kit/core'; import { TwinMakerTimeSeriesDataProvider } from './time-series-data/provider'; import { createDataSource } from './time-series-data/data-source'; import { TwinMakerMetadataModule } from './metadata-module/TwinMakerMetadataModule'; import type { Credentials, CredentialProvider } from '@aws-sdk/types'; import type { VideoDataProps } from './types'; -import type { TwinMakerDataStreamQuery, TwinMakerQuery } from './time-series-data/types'; +import type { TwinMakerDataStreamQuery } from './time-series-data/types'; import type { TimeSeriesDataQuery, TimeSeriesDataRequest } from '@iot-app-kit/core'; import { TwinMakerErrorCode } from './common/error'; import { createEntityPropertyBindingProvider } from './data-binding-provider/createEntityPropertyBindingProvider'; +import { TwinMakerHistoryQuery, TwinMakerPropertyValueQuery } from './common/queryTypes'; +import { TwinMakerPropertyValueDataProvider } from './property-value/provider'; const SOURCE = 'iottwinmaker'; @@ -129,7 +131,7 @@ export const initialize = ( const secretsManagerClient: SecretsManagerClient = authInput.secretsManagerClient ?? secretsManagersdk(inputWithCred.awsCredentials, inputWithCred.awsRegion); - // For caching TwinMaker API calls + // For caching TwinMaker metadata API calls const cachedQueryClient = new QueryClient({ defaultOptions: { queries: { @@ -138,12 +140,23 @@ export const initialize = ( }, }); + // For refreshing TwinMaker property value API calls + const refreshingQueryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000, + }, + }, + }); + const twinMakerMetadataModule = new TwinMakerMetadataModule(workspaceId, twinMakerClient, cachedQueryClient); const twinMakerTimeSeriesModule = new TimeSeriesDataModule( createDataSource(twinMakerMetadataModule, twinMakerClient) ); - const timeSeriesDataQuery: (query: TwinMakerQuery) => TimeSeriesDataQuery = (query: TwinMakerQuery) => ({ + const timeSeriesDataQuery: (query: TwinMakerHistoryQuery) => TimeSeriesDataQuery = ( + query: TwinMakerHistoryQuery + ) => ({ toQueryString: () => JSON.stringify({ source: SOURCE, @@ -162,15 +175,36 @@ export const initialize = ( }), }); + const propertyValueQuery = (query: TwinMakerPropertyValueQuery): Query => ({ + toQueryString: () => + JSON.stringify({ + source: SOURCE, + queryType: 'property-value', + query, + }), + build: (_sessionId: string, params: DataRequest) => + new TwinMakerPropertyValueDataProvider(refreshingQueryClient, twinMakerClient, { + queries: [ + { + workspaceId, + ...query, + }, + ], + request: params, + }), + }); + return { query: { - timeSeriesData: (query: TwinMakerQuery): TimeSeriesDataQuery => timeSeriesDataQuery(query), + timeSeriesData: timeSeriesDataQuery, + propertyValue: propertyValueQuery, }, s3SceneLoader: (sceneId: string) => new S3SceneLoader({ workspaceId, sceneId, twinMakerClient, s3Client }), valueDataBindingProviders: (onError?: (errorCode: TwinMakerErrorCode, errorDetails?: ErrorDetails) => void) => ({ TwinMakerEntityProperty: createEntityPropertyBindingProvider({ metadataModule: twinMakerMetadataModule, timeSeriesDataQuery, + propertyValueQuery, onError, }), }), diff --git a/packages/source-iottwinmaker/src/metadata-module/TwinMakerMetadataModule.ts b/packages/source-iottwinmaker/src/metadata-module/TwinMakerMetadataModule.ts index c07035e2e..b55e10f49 100644 --- a/packages/source-iottwinmaker/src/metadata-module/TwinMakerMetadataModule.ts +++ b/packages/source-iottwinmaker/src/metadata-module/TwinMakerMetadataModule.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { GetEntityCommand, IoTTwinMakerClient, ListEntitiesCommand } from '@aws-sdk/client-iottwinmaker'; -import { isDefined } from '../time-series-data/utils/values'; import type { EntitySummary, GetEntityResponse, ListEntitiesResponse } from '@aws-sdk/client-iottwinmaker'; import type { ErrorDetails } from '@iot-app-kit/core'; import { QueryClient } from '@tanstack/query-core'; +import { isDefined } from '../utils/propertyValueUtils'; export class TwinMakerMetadataModule { private readonly workspaceId: string; diff --git a/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.spec.ts b/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.spec.ts new file mode 100644 index 000000000..834e948b3 --- /dev/null +++ b/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.spec.ts @@ -0,0 +1,152 @@ +import { GetPropertyValueCommandOutput } from '@aws-sdk/client-iottwinmaker'; +import { createMockTwinMakerSDK } from '../../__mocks__/iottwinmakerSDK'; + +import { getPropertyValueByEntity } from './getPropertyValueByEntity'; +import { TwinMakerStaticDataQuery } from '../types'; + +describe('getPropertyValueByEntity', () => { + const mockEntityRef1 = { + entityId: 'entity-1', + componentName: 'comp-1', + propertyName: 'prop-1', + }; + const mockEntityRef2 = { + entityId: 'entity-2', + componentName: 'comp-2', + propertyName: 'prop-2', + }; + const mockEntityRef3 = { + ...mockEntityRef2, + propertyName: 'prop-3', + }; + const mockQuery1: TwinMakerStaticDataQuery = { + workspaceId: 'ws-1', + entityId: mockEntityRef1.entityId, + componentName: mockEntityRef1.componentName, + properties: [ + { + propertyName: mockEntityRef1.propertyName, + }, + ], + }; + const mockQuery2: TwinMakerStaticDataQuery = { + workspaceId: 'ws-1', + entityId: mockEntityRef2.entityId, + componentName: mockEntityRef2.componentName, + properties: [ + { + propertyName: mockEntityRef2.propertyName, + }, + { + propertyName: mockEntityRef3.propertyName, + }, + ], + }; + + const getPropertyValue = jest.fn(); + const tmClient = createMockTwinMakerSDK({ + getPropertyValue, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should send correct request and return correct data with multiple properties', async () => { + const mockAPIResponse: GetPropertyValueCommandOutput = { + $metadata: {}, + propertyValues: { + [mockEntityRef2.propertyName]: { + propertyReference: mockEntityRef2, + propertyValue: { + longValue: 111, + }, + }, + [mockEntityRef3.propertyName]: { + propertyReference: mockEntityRef3, + propertyValue: { + booleanValue: true, + }, + }, + }, + }; + getPropertyValue.mockResolvedValue(mockAPIResponse); + const result = await getPropertyValueByEntity({ + query: mockQuery2, + client: tmClient, + }); + + expect(getPropertyValue).toBeCalledWith({ + workspaceId: mockQuery2.workspaceId, + entityId: mockQuery2.entityId, + componentName: mockQuery2.componentName, + selectedProperties: [mockEntityRef2.propertyName, mockEntityRef3.propertyName], + }); + expect(result).toEqual([ + { + dataType: 'NUMBER', + data: [{ y: 111 }], + meta: mockEntityRef2, + }, + { + dataType: 'BOOLEAN', + data: [{ y: 'true' }], + meta: mockEntityRef3, + }, + ]); + }); + + it('should return correct result with nextToken', async () => { + const mockAPIResponse1: GetPropertyValueCommandOutput = { + nextToken: '11223344', + $metadata: {}, + propertyValues: { + [mockEntityRef1.propertyName]: { + propertyReference: mockEntityRef1, + propertyValue: { + stringValue: 'result-1', + }, + }, + }, + }; + const mockAPIResponse2: GetPropertyValueCommandOutput = { + nextToken: undefined, + $metadata: {}, + propertyValues: { + [mockEntityRef1.propertyName]: { + propertyReference: mockEntityRef1, + propertyValue: { + stringValue: 'result-2', + }, + }, + }, + }; + getPropertyValue.mockResolvedValueOnce(mockAPIResponse1).mockResolvedValueOnce(mockAPIResponse2); + + const result = await getPropertyValueByEntity({ + query: mockQuery1, + client: tmClient, + }); + + expect(result).toEqual([ + expect.objectContaining({ + data: [ + { + y: 'result-1', + }, + ], + dataType: 'STRING', + meta: mockEntityRef1, + }), + expect.objectContaining({ + data: [ + { + y: 'result-2', + }, + ], + meta: mockEntityRef1, + dataType: 'STRING', + }), + ]); + }); +}); diff --git a/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.ts b/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.ts new file mode 100644 index 000000000..ac7c4a906 --- /dev/null +++ b/packages/source-iottwinmaker/src/property-value/client/getPropertyValueByEntity.ts @@ -0,0 +1,65 @@ +import { GetPropertyValueCommand, GetPropertyValueResponse, IoTTwinMakerClient } from '@aws-sdk/client-iottwinmaker'; +import { DataType, DataStreamBase } from '@iot-app-kit/core'; +import { TwinMakerStaticDataQuery } from '../types'; +import { toValue } from '../../utils/propertyValueUtils'; +import { isNumber } from 'lodash'; + +/** + * Call TwinMaker GetPropertyValue API to get property value + * @param query defines the entityId, componentName and propertyNames to call API + * @returns a list of propertyValues for each property separately + */ +export const getPropertyValueByEntity = async ({ + client, + query, +}: { + client: IoTTwinMakerClient; + query: TwinMakerStaticDataQuery; +}): Promise => { + const results: DataStreamBase[] = []; + let nextToken: string | undefined = undefined; + do { + const response: GetPropertyValueResponse = await client.send( + new GetPropertyValueCommand({ + workspaceId: query.workspaceId, + entityId: query.entityId, + componentName: query.componentName, + selectedProperties: query.properties.map((property) => property.propertyName), + nextToken, + }) + ); + + if (response.propertyValues) { + Object.keys(response.propertyValues).forEach((key) => { + const value = response.propertyValues?.[key].propertyValue; + if (!value) { + return; + } + const meta = { + entityId: query.entityId, + componentName: query.componentName, + propertyName: key, + }; + + const data = toValue(value); + if (!data) { + return; + } + + // TODO: handle non-primitive types when needed + let dataType: DataType = 'STRING'; + if (value.booleanValue !== undefined) { + dataType = 'BOOLEAN'; + } else if (isNumber(data)) { + dataType = 'NUMBER'; + } + + results.push({ dataType, data: [{ y: data }], meta }); + }); + } + + nextToken = response.nextToken; + } while (nextToken); + + return results; +}; diff --git a/packages/source-iottwinmaker/src/property-value/provider.spec.ts b/packages/source-iottwinmaker/src/property-value/provider.spec.ts new file mode 100644 index 000000000..46f13cc2a --- /dev/null +++ b/packages/source-iottwinmaker/src/property-value/provider.spec.ts @@ -0,0 +1,68 @@ +import { TwinMakerPropertyValueDataProvider } from './provider'; +import { GetPropertyValueCommandOutput } from '@aws-sdk/client-iottwinmaker'; +import { MINUTE_IN_MS } from '../common/timeConstants'; + +import flushPromises from 'flush-promises'; +import { QueryClient } from '@tanstack/query-core'; +import { createMockTwinMakerSDK } from '../__mocks__/iottwinmakerSDK'; + +const getPropertyValue = jest.fn(); +const tmClient = createMockTwinMakerSDK({ getPropertyValue }); +const queryClient = new QueryClient(); +const mockEntityRef1 = { + entityId: 'entity-1', + componentName: 'comp-1', + propertyName: 'prop-1', +}; + +it('should subscribes, updates, and unsubscribes to property value data', async () => { + const refreshRate = MINUTE_IN_MS; + const mockAPIResponse: GetPropertyValueCommandOutput = { + $metadata: {}, + propertyValues: { + [mockEntityRef1.propertyName]: { + propertyReference: mockEntityRef1, + propertyValue: { + stringValue: 'result-2', + }, + }, + }, + }; + getPropertyValue.mockResolvedValue(mockAPIResponse); + + const provider = new TwinMakerPropertyValueDataProvider(queryClient, tmClient, { + queries: [ + { + workspaceId: 'ws-1', + entityId: mockEntityRef1.entityId, + componentName: mockEntityRef1.componentName, + properties: [{ propertyName: mockEntityRef1.propertyName }], + }, + ], + request: { + settings: { refreshRate }, + }, + }); + + const subscribeCallback = jest.fn(); + + // subscribe + provider.subscribe({ next: subscribeCallback }); + + await flushPromises(); + + expect(subscribeCallback).toBeCalledWith([ + { + dataStreams: [ + { + data: [{ y: 'result-2' }], + dataType: 'STRING', + meta: mockEntityRef1, + }, + ], + }, + ]); + + // unsubscribe + provider.unsubscribe(); +}); diff --git a/packages/source-iottwinmaker/src/property-value/provider.ts b/packages/source-iottwinmaker/src/property-value/provider.ts new file mode 100644 index 000000000..5d2bad297 --- /dev/null +++ b/packages/source-iottwinmaker/src/property-value/provider.ts @@ -0,0 +1,76 @@ +import { DataRequest, Provider, ProviderObserver, DataBase, DataStreamBase } from '@iot-app-kit/core'; +import { QueryClient, QueryObserver } from '@tanstack/react-query'; + +import { IoTTwinMakerClient } from '@aws-sdk/client-iottwinmaker'; +import { getPropertyValueByEntity } from './client/getPropertyValueByEntity'; +import { TwinMakerStaticDataQuery } from './types'; + +/** + * Provider for TwinMaker time series data + */ +export class TwinMakerPropertyValueDataProvider implements Provider { + public tmClient: IoTTwinMakerClient; + public queryClient: QueryClient; + + public observer: QueryObserver; + + private _unsubscribes: (() => void)[] = []; + + constructor( + queryClient: QueryClient, + tmClient: IoTTwinMakerClient, + { + request, + queries, + }: { + request: DataRequest; + queries: TwinMakerStaticDataQuery[]; + } + ) { + this.queryClient = queryClient; + this.tmClient = tmClient; + this.observer = new QueryObserver(queryClient, { + queryKey: queries, + queryFn: async () => { + try { + const promises: Promise[] = []; + queries.forEach((query) => { + promises.push(getPropertyValueByEntity({ client: tmClient, query: query })); + }); + return [{ dataStreams: (await Promise.all(promises)).flat() }]; + } catch (_err) { + // TODO: return error + return []; + } + }, + refetchInterval: request.settings?.refreshRate, + }); + } + + subscribe = (observer: ProviderObserver) => { + this._unsubscribes.push( + this.observer.subscribe((result) => { + if (result.data) { + observer.next(result.data); + } + }) + ); + }; + + unsubscribe = () => { + this._unsubscribes.forEach((unsub) => { + try { + unsub(); + } catch (e) { + const err = e as Error; + + // sometimes things get out of sync, if there's no subscription, then we don't need to do anything. + if (err.message.includes('subscription does not exist.')) { + return; + } + + throw e; + } + }); + }; +} diff --git a/packages/source-iottwinmaker/src/property-value/types.ts b/packages/source-iottwinmaker/src/property-value/types.ts new file mode 100644 index 000000000..a2238d886 --- /dev/null +++ b/packages/source-iottwinmaker/src/property-value/types.ts @@ -0,0 +1,3 @@ +import { TwinMakerPropertyValueQuery } from '../common/queryTypes'; + +export type TwinMakerStaticDataQuery = TwinMakerPropertyValueQuery & { workspaceId: string }; diff --git a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts index 9a2643f1f..c0984b5b8 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts @@ -3,7 +3,7 @@ import { GetPropertyValueHistoryCommand, IoTTwinMakerClient } from '@aws-sdk/cli import { isEmpty, isEqual } from 'lodash'; import { TwinMakerMetadataModule } from '../../metadata-module/TwinMakerMetadataModule'; import { fromDataStreamId, toDataStreamId } from '../utils/dataStreamId'; -import { toDataPoint, isDefined, toDataStream, toDataType } from '../utils/values'; +import { toDataPoint, toDataStream } from '../utils/values'; import type { GetEntityResponse } from '@aws-sdk/client-iottwinmaker'; import type { OnSuccessCallback, @@ -12,6 +12,7 @@ import type { DataStream, DataPoint, } from '@iot-app-kit/core'; +import { isDefined, toDataType } from '../../utils/propertyValueUtils'; export const getPropertyValueHistoryByComponentTypeRequest = async ({ workspaceId, diff --git a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.spec.ts b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.spec.ts index 9a0046242..2d3c5dfea 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.spec.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.spec.ts @@ -43,10 +43,8 @@ describe('getPropertyValueHistoryByEntity', () => { start, end, }; - const getEntity = jest.fn(); const getPropertyValueHistory = jest.fn(); const tmClient = createMockTwinMakerSDK({ - getEntity, getPropertyValueHistory, }); const onSuccess = jest.fn(); diff --git a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts index 427d594da..7a96ce108 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts @@ -1,8 +1,9 @@ import { GetPropertyValueHistoryCommand, IoTTwinMakerClient } from '@aws-sdk/client-iottwinmaker'; import { isEqual } from 'lodash'; import { fromDataStreamId, toDataStreamId } from '../utils/dataStreamId'; -import { toDataPoint, isDefined, toDataStream } from '../utils/values'; +import { toDataPoint, toDataStream } from '../utils/values'; import type { OnSuccessCallback, RequestInformationAndRange, ErrorCallback } from '@iot-app-kit/core'; +import { isDefined } from '../../utils/propertyValueUtils'; export const getPropertyValueHistoryByEntityRequest = ({ workspaceId, diff --git a/packages/source-iottwinmaker/src/time-series-data/completeDataStreams.ts b/packages/source-iottwinmaker/src/time-series-data/completeDataStreams.ts index 1aa3b51bf..f1cd7eab9 100644 --- a/packages/source-iottwinmaker/src/time-series-data/completeDataStreams.ts +++ b/packages/source-iottwinmaker/src/time-series-data/completeDataStreams.ts @@ -1,7 +1,7 @@ import { fromDataStreamId } from './utils/dataStreamId'; -import { toDataType } from './utils/values'; import type { DataStream } from '@iot-app-kit/core'; import type { GetEntityResponse } from '@aws-sdk/client-iottwinmaker'; +import { toDataType } from '../utils/propertyValueUtils'; /** * Get completed data streams by merging together the data streams with the entities. diff --git a/packages/source-iottwinmaker/src/time-series-data/data-source.ts b/packages/source-iottwinmaker/src/time-series-data/data-source.ts index f5645e125..88630c706 100644 --- a/packages/source-iottwinmaker/src/time-series-data/data-source.ts +++ b/packages/source-iottwinmaker/src/time-series-data/data-source.ts @@ -3,10 +3,11 @@ import { toDataStreamId } from './utils/dataStreamId'; import { getPropertyValueHistoryByEntity } from './client/getPropertyValueHistoryByEntity'; import { getPropertyValueHistoryByComponentType } from './client/getPropertyValueHistoryByComponentType'; import { TwinMakerMetadataModule } from '../metadata-module/TwinMakerMetadataModule'; -import { isDefined } from './utils/values'; import { isEmpty } from 'lodash'; import type { DataSource, RequestInformationAndRange } from '@iot-app-kit/core'; -import type { TwinMakerComponentHistoryQuery, TwinMakerDataStreamQuery, TwinMakerEntityHistoryQuery } from './types'; +import type { TwinMakerDataStreamQuery } from './types'; +import { TwinMakerComponentHistoryQuery, TwinMakerEntityHistoryQuery } from '../common/queryTypes'; +import { isDefined } from '../utils/propertyValueUtils'; export const createDataSource = ( metadataModule: TwinMakerMetadataModule, @@ -36,7 +37,7 @@ export const createDataSource = ( getRequestsFromQuery: async ({ query }) => { const entityQuery = query as TwinMakerEntityHistoryQuery; if (entityQuery.entityId && entityQuery.componentName) { - return query.properties.flatMap(({ propertyName, refId }) => ({ + return entityQuery.properties.flatMap(({ propertyName, refId }) => ({ id: toDataStreamId({ workspaceId: query.workspaceId, entityId: entityQuery.entityId, diff --git a/packages/source-iottwinmaker/src/time-series-data/provider.ts b/packages/source-iottwinmaker/src/time-series-data/provider.ts index cf0a8cf38..0f0b3a35d 100644 --- a/packages/source-iottwinmaker/src/time-series-data/provider.ts +++ b/packages/source-iottwinmaker/src/time-series-data/provider.ts @@ -1,8 +1,7 @@ -import { TimeSeriesDataModule } from '@iot-app-kit/core'; +import { ProviderWithViewport, TimeSeriesDataModule } from '@iot-app-kit/core'; import { subscribeToTimeSeriesData } from './subscribeToTimeSeriesData'; import { TwinMakerMetadataModule } from '../metadata-module/TwinMakerMetadataModule'; import type { - Provider, ProviderObserver, TimeSeriesData, DataModuleSubscription, @@ -14,7 +13,7 @@ import type { TwinMakerDataStreamQuery } from './types'; /** * Provider for TwinMaker time series data */ -export class TwinMakerTimeSeriesDataProvider implements Provider { +export class TwinMakerTimeSeriesDataProvider implements ProviderWithViewport { private update: (subscriptionUpdate: SubscriptionUpdate) => void = () => {}; public dataModule: TimeSeriesDataModule; diff --git a/packages/source-iottwinmaker/src/time-series-data/subscribeToTimeSeriesData.ts b/packages/source-iottwinmaker/src/time-series-data/subscribeToTimeSeriesData.ts index 7b628acb7..9f9f9c686 100644 --- a/packages/source-iottwinmaker/src/time-series-data/subscribeToTimeSeriesData.ts +++ b/packages/source-iottwinmaker/src/time-series-data/subscribeToTimeSeriesData.ts @@ -10,8 +10,9 @@ import type { ErrorDetails, Viewport, } from '@iot-app-kit/core'; -import type { TwinMakerDataStreamQuery, TwinMakerEntityHistoryQuery } from './types'; +import type { TwinMakerDataStreamQuery } from './types'; import type { GetEntityResponse } from '@aws-sdk/client-iottwinmaker'; +import { TwinMakerEntityHistoryQuery } from '../common/queryTypes'; export const subscribeToTimeSeriesData = (metadataModule: TwinMakerMetadataModule, dataModule: TimeSeriesDataModule) => diff --git a/packages/source-iottwinmaker/src/time-series-data/types.ts b/packages/source-iottwinmaker/src/time-series-data/types.ts index 6fa4b3557..c398d4a3d 100644 --- a/packages/source-iottwinmaker/src/time-series-data/types.ts +++ b/packages/source-iottwinmaker/src/time-series-data/types.ts @@ -1,26 +1,7 @@ -import type { DataStreamQuery, RefId } from '@iot-app-kit/core'; +import type { DataStreamQuery } from '@iot-app-kit/core'; +import { TwinMakerHistoryQuery } from '../common/queryTypes'; -export type PropertyQueryInfo = { - propertyName: string; - - refId?: RefId; -}; - -export type TwinMakerBaseQuery = { - properties: PropertyQueryInfo[]; -}; - -export interface TwinMakerEntityHistoryQuery extends TwinMakerBaseQuery { - entityId: string; - componentName: string; -} - -export interface TwinMakerComponentHistoryQuery extends TwinMakerBaseQuery { - componentTypeId: string; -} - -export type TwinMakerQuery = TwinMakerEntityHistoryQuery | TwinMakerComponentHistoryQuery; -export type TwinMakerDataStreamQuery = TwinMakerQuery & { workspaceId: string } & DataStreamQuery; +export type TwinMakerDataStreamQuery = TwinMakerHistoryQuery & { workspaceId: string } & DataStreamQuery; export type TwinMakerDataStreamIdComponent = { workspaceId: string; diff --git a/packages/source-iottwinmaker/src/time-series-data/utils/values.spec.ts b/packages/source-iottwinmaker/src/time-series-data/utils/values.spec.ts index 613253116..a814a1fb5 100644 --- a/packages/source-iottwinmaker/src/time-series-data/utils/values.spec.ts +++ b/packages/source-iottwinmaker/src/time-series-data/utils/values.spec.ts @@ -1,55 +1,4 @@ -import { isDefined, toDataPoint, toDataStream, toDataType, toValue } from './values'; - -describe('isDefined', () => { - it('should return true when value is defined', () => { - expect(isDefined('0')).toBeTruthy(); - expect(isDefined({})).toBeTruthy(); - expect(isDefined(0)).toBeTruthy(); - expect(isDefined(NaN)).toBeTruthy(); - expect(isDefined(false)).toBeTruthy(); - }); - - it('should return false when value is nullish', () => { - expect(isDefined(undefined)).toBeFalsy(); - expect(isDefined(null)).toBeFalsy(); - }); -}); - -describe('toValue', () => { - beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => null); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should return undefined when no value is defined', () => { - expect(toValue({ doubleValue: undefined })).toBeUndefined(); - expect(console.warn).toBeCalledTimes(1); - }); - - it('should return number when number value is defined', () => { - expect(toValue({ doubleValue: 123.4 })).toEqual(123.4); - expect(toValue({ integerValue: 1234 })).toEqual(1234); - expect(toValue({ longValue: 1234 })).toEqual(1234); - }); - - it('should return boolean string when boolan value is defined', () => { - expect(toValue({ booleanValue: true })).toEqual('true'); - expect(toValue({ booleanValue: false })).toEqual('false'); - }); - - it('should return string when other values are defined', () => { - expect(toValue({ stringValue: 'test' })).toEqual('test'); - expect(toValue({ listValue: [{ booleanValue: true }] })).toEqual('[{"booleanValue":true}]'); - expect(toValue({ mapValue: { test: { booleanValue: true } } })).toEqual('{"test":{"booleanValue":true}}'); - expect(toValue({ expression: 'a == 40' })).toEqual('a == 40'); - expect(toValue({ relationshipValue: { targetEntityId: 'entity-1', targetComponentName: 'comp-1' } })).toEqual( - '{"targetEntityId":"entity-1","targetComponentName":"comp-1"}' - ); - }); -}); +import { toDataPoint, toDataStream } from './values'; describe('toDataPoint', () => { it('should return undefined when input is not defined', () => { @@ -126,27 +75,3 @@ describe('toDataStream', () => { }); }); }); - -describe('toDataType', () => { - it('should return undefined when type is not defined', () => { - expect(toDataType({ type: undefined })).toBeUndefined(); - }); - - it('should return BOOLEAN', () => { - expect(toDataType({ type: 'BOOLEAN' })).toEqual('BOOLEAN'); - }); - - it('should return NUMBER', () => { - expect(toDataType({ type: 'DOUBLE' })).toEqual('NUMBER'); - expect(toDataType({ type: 'INTEGER' })).toEqual('NUMBER'); - expect(toDataType({ type: 'LONG' })).toEqual('NUMBER'); - }); - - it('should return STRING', () => { - expect(toDataType({ type: 'LIST' })).toEqual('STRING'); - expect(toDataType({ type: 'MAP' })).toEqual('STRING'); - expect(toDataType({ type: 'RELATIONSHIP' })).toEqual('STRING'); - expect(toDataType({ type: 'STRING' })).toEqual('STRING'); - expect(toDataType({ type: 'RANDOM' })).toEqual('STRING'); - }); -}); diff --git a/packages/source-iottwinmaker/src/time-series-data/utils/values.ts b/packages/source-iottwinmaker/src/time-series-data/utils/values.ts index e536c861e..794998f07 100644 --- a/packages/source-iottwinmaker/src/time-series-data/utils/values.ts +++ b/packages/source-iottwinmaker/src/time-series-data/utils/values.ts @@ -1,43 +1,7 @@ -import { Type } from '@aws-sdk/client-iottwinmaker'; -import { isEmpty, isNil, isNumber, isString } from 'lodash'; -import { DATA_TYPE } from '@iot-app-kit/core'; -import type { DataValue, PropertyValue, DataType as TMDataType } from '@aws-sdk/client-iottwinmaker'; -import type { DataStream, DataPoint, Primitive, DataType } from '@iot-app-kit/core'; - -/** - * Check if value is not null and not undefined. - * - * @param value The value to check. - * @returns Returns false if the value is nullish, otherwise true. - */ -export const isDefined = (value: T | null | undefined): value is T => !isNil(value); - -/** - * Extracts the value out of a TwinMaker property's DataValue - * - * NOTE: Currently we treat booleans and objects as strings. - */ -export const toValue = (dataValue: DataValue): Primitive | undefined => { - const values = Object.values(dataValue).filter(isDefined); - - if (isEmpty(values)) { - console.warn('Expected value to have at least one property value, but instead it has none!'); - return undefined; - } - - if (values.length > 1) { - console.warn('More than one value found in DataValue, use a random value only'); - } - - const value = values[0]; - - if (isNumber(value) || isString(value)) { - return value; - } - - // Non-primitive value converts to string by default - return JSON.stringify(value); -}; +import { isNil } from 'lodash'; +import type { PropertyValue } from '@aws-sdk/client-iottwinmaker'; +import type { DataStream, DataPoint } from '@iot-app-kit/core'; +import { toValue } from '../../utils/propertyValueUtils'; /** * Converts a response for data into a data point understood by IoT App Kit. @@ -62,28 +26,6 @@ export const toDataPoint = (propertyValue: PropertyValue | undefined): DataPoint }; }; -/** - * Convert the TwinMaker DataType into AppKit DataType - * - * @param tmDataType the TwinMaker DataType to be converted. - * @returns the converted AppKit DataType - */ -export const toDataType = (tmDataType: TMDataType): DataType | undefined => { - if (!tmDataType.type) return undefined; - - switch (tmDataType.type) { - case Type.BOOLEAN: - return DATA_TYPE.BOOLEAN; - case Type.DOUBLE: - case Type.INTEGER: - case Type.LONG: - return DATA_TYPE.NUMBER; - default: - // Other types are converted to string for now. - return DATA_TYPE.STRING; - } -}; - export const toDataStream = ({ streamId, dataPoints = [], diff --git a/packages/source-iottwinmaker/src/utils/propertyValueUtils.spec.ts b/packages/source-iottwinmaker/src/utils/propertyValueUtils.spec.ts new file mode 100644 index 000000000..ecf3ac05a --- /dev/null +++ b/packages/source-iottwinmaker/src/utils/propertyValueUtils.spec.ts @@ -0,0 +1,76 @@ +import { isDefined, toDataType, toValue } from './propertyValueUtils'; + +describe('isDefined', () => { + it('should return true when value is defined', () => { + expect(isDefined('0')).toBeTruthy(); + expect(isDefined({})).toBeTruthy(); + expect(isDefined(0)).toBeTruthy(); + expect(isDefined(NaN)).toBeTruthy(); + expect(isDefined(false)).toBeTruthy(); + }); + + it('should return false when value is nullish', () => { + expect(isDefined(undefined)).toBeFalsy(); + expect(isDefined(null)).toBeFalsy(); + }); +}); + +describe('toValue', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => null); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should return undefined when no value is defined', () => { + expect(toValue({ doubleValue: undefined })).toBeUndefined(); + expect(console.warn).toBeCalledTimes(1); + }); + + it('should return number when number value is defined', () => { + expect(toValue({ doubleValue: 123.4 })).toEqual(123.4); + expect(toValue({ integerValue: 1234 })).toEqual(1234); + expect(toValue({ longValue: 1234 })).toEqual(1234); + }); + + it('should return boolean string when boolan value is defined', () => { + expect(toValue({ booleanValue: true })).toEqual('true'); + expect(toValue({ booleanValue: false })).toEqual('false'); + }); + + it('should return string when other values are defined', () => { + expect(toValue({ stringValue: 'test' })).toEqual('test'); + expect(toValue({ listValue: [{ booleanValue: true }] })).toEqual('[{"booleanValue":true}]'); + expect(toValue({ mapValue: { test: { booleanValue: true } } })).toEqual('{"test":{"booleanValue":true}}'); + expect(toValue({ expression: 'a == 40' })).toEqual('a == 40'); + expect(toValue({ relationshipValue: { targetEntityId: 'entity-1', targetComponentName: 'comp-1' } })).toEqual( + '{"targetEntityId":"entity-1","targetComponentName":"comp-1"}' + ); + }); +}); + +describe('toDataType', () => { + it('should return undefined when type is not defined', () => { + expect(toDataType({ type: undefined })).toBeUndefined(); + }); + + it('should return BOOLEAN', () => { + expect(toDataType({ type: 'BOOLEAN' })).toEqual('BOOLEAN'); + }); + + it('should return NUMBER', () => { + expect(toDataType({ type: 'DOUBLE' })).toEqual('NUMBER'); + expect(toDataType({ type: 'INTEGER' })).toEqual('NUMBER'); + expect(toDataType({ type: 'LONG' })).toEqual('NUMBER'); + }); + + it('should return STRING', () => { + expect(toDataType({ type: 'LIST' })).toEqual('STRING'); + expect(toDataType({ type: 'MAP' })).toEqual('STRING'); + expect(toDataType({ type: 'RELATIONSHIP' })).toEqual('STRING'); + expect(toDataType({ type: 'STRING' })).toEqual('STRING'); + expect(toDataType({ type: 'RANDOM' })).toEqual('STRING'); + }); +}); diff --git a/packages/source-iottwinmaker/src/utils/propertyValueUtils.ts b/packages/source-iottwinmaker/src/utils/propertyValueUtils.ts new file mode 100644 index 000000000..890dddbbe --- /dev/null +++ b/packages/source-iottwinmaker/src/utils/propertyValueUtils.ts @@ -0,0 +1,62 @@ +import { Type } from '@aws-sdk/client-iottwinmaker'; +import { isEmpty, isNil, isNumber, isString } from 'lodash'; +import { DATA_TYPE } from '@iot-app-kit/core'; +import type { DataValue, DataType as TMDataType } from '@aws-sdk/client-iottwinmaker'; +import type { Primitive, DataType } from '@iot-app-kit/core'; + +/** + * Check if value is not null and not undefined. + * + * @param value The value to check. + * @returns Returns false if the value is nullish, otherwise true. + */ +export const isDefined = (value: T | null | undefined): value is T => !isNil(value); + +/** + * Extracts the value out of a TwinMaker property's DataValue + * + * NOTE: Currently we treat booleans and objects as strings. + */ +export const toValue = (dataValue: DataValue): Primitive | undefined => { + const values = Object.values(dataValue).filter(isDefined); + + if (isEmpty(values)) { + console.warn('Expected value to have at least one property value, but instead it has none!'); + return undefined; + } + + if (values.length > 1) { + console.warn('More than one value found in DataValue, use a random value only'); + } + + const value = values[0]; + + if (isNumber(value) || isString(value)) { + return value; + } + + // Non-primitive value converts to string by default + return JSON.stringify(value); +}; + +/** + * Convert the TwinMaker DataType into AppKit DataType + * + * @param tmDataType the TwinMaker DataType to be converted. + * @returns the converted AppKit DataType + */ +export const toDataType = (tmDataType: TMDataType): DataType | undefined => { + if (!tmDataType.type) return undefined; + + switch (tmDataType.type) { + case Type.BOOLEAN: + return DATA_TYPE.BOOLEAN; + case Type.DOUBLE: + case Type.INTEGER: + case Type.LONG: + return DATA_TYPE.NUMBER; + default: + // Other types are converted to string for now. + return DATA_TYPE.STRING; + } +};