From 9020c2f42ae7f2054dd19a6f156b1d3db00b9887 Mon Sep 17 00:00:00 2001 From: Xinyi Xu Date: Tue, 13 Sep 2022 22:57:02 -0700 Subject: [PATCH] Add multi-property query support (#105) Co-authored-by: Xinyi Xu --- ...ropertyValueHistoryByComponentType.spec.ts | 62 +++++- .../getPropertyValueHistoryByComponentType.ts | 25 ++- .../getPropertyValueHistoryByEntity.spec.ts | 179 +++++++++++++----- .../client/getPropertyValueHistoryByEntity.ts | 159 ++++++++++++---- 4 files changed, 330 insertions(+), 95 deletions(-) diff --git a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.spec.ts b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.spec.ts index 4301f2d9f..411049f5c 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.spec.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.spec.ts @@ -21,6 +21,12 @@ describe('getPropertyValueHistoryByComponentType', () => { componentName: 'comp-1', propertyName: 'prop-1', }; + const streamIdComponents3: TwinMakerDataStreamIdComponent = { + workspaceId: 'ws-1', + entityId: 'entity-2', + componentName: 'comp-1', + propertyName: 'prop-2', + }; const mockEntityRef1 = { propertyName: streamIdComponents1.propertyName, externalIdProperty: { @@ -34,6 +40,12 @@ describe('getPropertyValueHistoryByComponentType', () => { }, }; const mockEntityRef3 = { + propertyName: streamIdComponents3.propertyName, + externalIdProperty: { + extid: 'ext-id-2', + }, + }; + const mockEntityRef4 = { propertyName: streamIdComponents1.propertyName, externalIdProperty: { extid: 'ext-id-3', @@ -57,6 +69,13 @@ describe('getPropertyValueHistoryByComponentType', () => { end, meta: { componentTypeId: 'comp-type-1' }, }, + { + id: toDataStreamId(streamIdComponents3), + resolution: '0', + start, + end, + meta: { componentTypeId: 'comp-type-1' }, + }, ]; const tmClient = new IoTTwinMakerClient({}); const twinMakerMetadataModule = new TwinMakerMetadataModule('workspace-id', tmClient); @@ -105,6 +124,11 @@ describe('getPropertyValueHistoryByComponentType', () => { isExternalId: false, }, }, + 'prop-2': { + definition: { + isExternalId: false, + }, + }, }, }, 'comp-2': { @@ -150,7 +174,7 @@ describe('getPropertyValueHistoryByComponentType', () => { ], }, { - entityPropertyReference: mockEntityRef3, + entityPropertyReference: mockEntityRef4, values: [ { value: { @@ -187,6 +211,17 @@ describe('getPropertyValueHistoryByComponentType', () => { }, ], }, + { + entityPropertyReference: mockEntityRef3, + values: [ + { + value: { + integerValue: 44, + }, + time: new Date(2022, 1, 4).toISOString(), + }, + ], + }, ], }; @@ -282,13 +317,14 @@ describe('getPropertyValueHistoryByComponentType', () => { requestInformations: [ { ...mockRequestInfos[0], fetchFromStartToEnd: true }, { ...mockRequestInfos[1], fetchFromStartToEnd: true }, + { ...mockRequestInfos[2], fetchFromStartToEnd: true }, ], client: tmClient, }); await flushPromises(); - expect(onSuccess).toBeCalledTimes(3); + expect(onSuccess).toBeCalledTimes(4); expect(onSuccess).toHaveBeenNthCalledWith( 1, [ @@ -355,6 +391,28 @@ describe('getPropertyValueHistoryByComponentType', () => { start, end ); + expect(onSuccess).toHaveBeenNthCalledWith( + 4, + [ + expect.objectContaining({ + id: mockRequestInfos[2].id, + data: [ + { + x: new Date(2022, 1, 4).getTime(), + y: 44, + }, + ], + meta: { + entityId: streamIdComponents3.entityId, + componentName: streamIdComponents3.componentName, + propertyName: streamIdComponents3.propertyName, + }, + }), + ], + { ...mockRequestInfos[2], fetchFromStartToEnd: true }, + start, + end + ); }); it('should trigger onSuccess with correct dataStream response for fetchMostRecentBeforeEnd case', async () => { 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 42c64e32f..5c337f60b 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByComponentType.ts @@ -9,7 +9,7 @@ import { toDataPoint, isDefined, toDataStream, toDataType } from '../utils/value export const getPropertyValueHistoryByComponentTypeRequest = async ({ workspaceId, componentTypeId, - propertyName, + propertyNames, requestInformations, entities, onSuccess, @@ -20,7 +20,7 @@ export const getPropertyValueHistoryByComponentTypeRequest = async ({ }: { workspaceId: string; componentTypeId: string; - propertyName: string; + propertyNames: string[]; requestInformations: Record; entities: GetEntityResponse[]; onError: ErrorCallback; @@ -51,7 +51,7 @@ export const getPropertyValueHistoryByComponentTypeRequest = async ({ new GetPropertyValueHistoryCommand({ workspaceId, componentTypeId, - selectedProperties: [propertyName], + selectedProperties: propertyNames, orderByTime: fetchMostRecent ? 'DESCENDING' : 'ASCENDING', startTime: start.toISOString(), endTime: end.toISOString(), @@ -70,7 +70,8 @@ export const getPropertyValueHistoryByComponentTypeRequest = async ({ Object.values(components || {}).forEach((comp) => { if ( comp.componentTypeId === componentTypeId && - values.entityPropertyReference?.propertyName === propertyName && + values.entityPropertyReference?.propertyName && + propertyNames.includes(values.entityPropertyReference?.propertyName) && values.entityPropertyReference.externalIdProperty && comp.componentName ) { @@ -97,8 +98,9 @@ export const getPropertyValueHistoryByComponentTypeRequest = async ({ return isMatch; }); const entityId = matchingEntity?.entityId; + const propertyName = values.entityPropertyReference.propertyName; - if (!entityId || isEmpty(matchingComponentName)) return; + if (!entityId || isEmpty(matchingComponentName) || !propertyName) return; const streamId = toDataStreamId({ workspaceId, @@ -147,7 +149,7 @@ export const getPropertyValueHistoryByComponentTypeRequest = async ({ getPropertyValueHistoryByComponentTypeRequest({ workspaceId, componentTypeId, - propertyName, + propertyNames, requestInformations, entities, onError, @@ -188,12 +190,12 @@ export const getPropertyValueHistoryByComponentType = async ({ }): Promise => { // Group same component type API call into one const requestInputs: { - params: { componentTypeId: string; propertyName: string; workspaceId: string }; + params: { componentTypeId: string; workspaceId: string }; + propertyNames: string[]; requestInfos: Record; entities: GetEntityResponse[]; }[] = []; - // TODO: May bundle the requests for the same component type, but different properties into the same call requestInformations.forEach(async (info) => { const { workspaceId, propertyName } = fromDataStreamId(info.id); const componentTypeId = @@ -219,13 +221,16 @@ export const getPropertyValueHistoryByComponentType = async ({ if (inputIndex >= 0) { requestInputs[inputIndex].requestInfos[info.id] = info; + if (!requestInputs[inputIndex].propertyNames.includes(propertyName)) { + requestInputs[inputIndex].propertyNames.push(propertyName); + } } else { requestInputs.push({ params: { componentTypeId: componentTypeId, - propertyName: propertyName, workspaceId: workspaceId, }, + propertyNames: [propertyName], requestInfos: { [info.id]: info }, entities: [], }); @@ -240,7 +245,7 @@ export const getPropertyValueHistoryByComponentType = async ({ return getPropertyValueHistoryByComponentTypeRequest({ workspaceId: input.params.workspaceId, componentTypeId: input.params.componentTypeId, - propertyName: input.params.propertyName, + propertyNames: input.propertyNames, requestInformations: input.requestInfos, entities, onSuccess, 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 e81a8a434..ac87b4b59 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 @@ -11,22 +11,41 @@ import { toDataStreamId } from '../utils/dataStreamId'; import { getPropertyValueHistoryByEntity } from './getPropertyValueHistoryByEntity'; describe('getPropertyValueHistoryByEntity', () => { - const streamIdComponents: TwinMakerDataStreamIdComponent = { + const start = new Date(2022, 1, 1); + const end = new Date(2022, 2, 1); + const streamIdComponents1: TwinMakerDataStreamIdComponent = { workspaceId: 'ws-1', entityId: 'entity-1', componentName: 'comp-1', propertyName: 'prop-1', }; - const mockEntityRef: EntityPropertyReference = { - entityId: streamIdComponents.entityId, - componentName: streamIdComponents.componentName, - propertyName: streamIdComponents.propertyName, + const streamIdComponents2: TwinMakerDataStreamIdComponent = { + workspaceId: 'ws-1', + entityId: 'entity-1', + componentName: 'comp-1', + propertyName: 'prop-2', + }; + const mockEntityRef1: EntityPropertyReference = { + entityId: streamIdComponents1.entityId, + componentName: streamIdComponents1.componentName, + propertyName: streamIdComponents1.propertyName, }; - const mockRequestInfo: RequestInformationAndRange = { - id: toDataStreamId(streamIdComponents), + const mockEntityRef2: EntityPropertyReference = { + entityId: streamIdComponents2.entityId, + componentName: streamIdComponents2.componentName, + propertyName: streamIdComponents2.propertyName, + }; + const mockRequestInfo1: RequestInformationAndRange = { + id: toDataStreamId(streamIdComponents1), + resolution: '0', + start, + end, + }; + const mockRequestInfo2: RequestInformationAndRange = { + id: toDataStreamId(streamIdComponents2), resolution: '0', - start: new Date(2022, 1, 1), - end: new Date(2022, 2, 1), + start, + end, }; const tmClient = new IoTTwinMakerClient({}); let sendSpy = jest.spyOn(tmClient, 'send'); @@ -39,67 +58,97 @@ describe('getPropertyValueHistoryByEntity', () => { }); it('should send correct request when fetchMostRecentBeforeStart is true', async () => { - let command: GetPropertyValueHistoryCommand; + const commands: GetPropertyValueHistoryCommand[] = []; sendSpy.mockImplementation((cmd) => { - command = cmd as GetPropertyValueHistoryCommand; + commands.push(cmd as GetPropertyValueHistoryCommand); return Promise.resolve({}); }); await getPropertyValueHistoryByEntity({ onSuccess, onError, - requestInformations: [{ ...mockRequestInfo, fetchMostRecentBeforeStart: true }], + requestInformations: [ + { ...mockRequestInfo1, fetchMostRecentBeforeStart: true }, + { ...mockRequestInfo2, fetchMostRecentBeforeStart: true }, + ], client: tmClient, }); - expect(command!.input.workspaceId).toEqual(streamIdComponents.workspaceId); - expect(command!.input.entityId).toEqual(streamIdComponents.entityId); - expect(command!.input.componentName).toEqual(streamIdComponents.componentName); - expect(command!.input.selectedProperties?.[0]).toEqual(streamIdComponents.propertyName); - expect(command!.input.maxResults).toEqual(1); - expect(command!.input.orderByTime).toEqual('DESCENDING'); - expect(command!.input.startTime).toEqual(new Date(0, 0, 0).toISOString()); - expect(command!.input.endTime).toEqual(mockRequestInfo.start.toISOString()); + // First request + expect(commands[0].input.workspaceId).toEqual(streamIdComponents1.workspaceId); + expect(commands[0].input.entityId).toEqual(streamIdComponents1.entityId); + expect(commands[0].input.componentName).toEqual(streamIdComponents1.componentName); + expect(commands[0].input.selectedProperties).toEqual([streamIdComponents1.propertyName]); + expect(commands[0].input.maxResults).toEqual(1); + expect(commands[0].input.orderByTime).toEqual('DESCENDING'); + expect(commands[0].input.startTime).toEqual(new Date(0, 0, 0).toISOString()); + expect(commands[0].input.endTime).toEqual(start.toISOString()); + // Second request + expect(commands[1].input.workspaceId).toEqual(streamIdComponents2.workspaceId); + expect(commands[1].input.entityId).toEqual(streamIdComponents2.entityId); + expect(commands[1].input.componentName).toEqual(streamIdComponents2.componentName); + expect(commands[1].input.selectedProperties).toEqual([streamIdComponents2.propertyName]); }); it('should send correct request when fetchMostRecentBeforeEnd is true', async () => { - let command: GetPropertyValueHistoryCommand; + const commands: GetPropertyValueHistoryCommand[] = []; sendSpy.mockImplementation((cmd) => { - command = cmd as GetPropertyValueHistoryCommand; + commands.push(cmd as GetPropertyValueHistoryCommand); return Promise.resolve({}); }); await getPropertyValueHistoryByEntity({ onSuccess, onError, - requestInformations: [{ ...mockRequestInfo, fetchMostRecentBeforeEnd: true }], + requestInformations: [ + { ...mockRequestInfo1, fetchMostRecentBeforeEnd: true }, + { ...mockRequestInfo2, fetchMostRecentBeforeEnd: true }, + ], client: tmClient, }); - expect(command!.input.maxResults).toEqual(1); - expect(command!.input.orderByTime).toEqual('DESCENDING'); - expect(command!.input.startTime).toEqual(new Date(0, 0, 0).toISOString()); - expect(command!.input.endTime).toEqual(mockRequestInfo.end.toISOString()); + // Fist request + expect(commands[0].input.maxResults).toEqual(1); + expect(commands[0].input.orderByTime).toEqual('DESCENDING'); + expect(commands[0].input.startTime).toEqual(new Date(0, 0, 0).toISOString()); + expect(commands[0].input.endTime).toEqual(end.toISOString()); + // Second request + expect(commands[1].input.workspaceId).toEqual(streamIdComponents2.workspaceId); + expect(commands[1].input.entityId).toEqual(streamIdComponents2.entityId); + expect(commands[1].input.componentName).toEqual(streamIdComponents2.componentName); + expect(commands[1].input.selectedProperties).toEqual([streamIdComponents2.propertyName]); }); it('should send correct request when fetchFromStartToEnd is true', async () => { - let command: GetPropertyValueHistoryCommand; + const commands: GetPropertyValueHistoryCommand[] = []; sendSpy.mockImplementation((cmd) => { - command = cmd as GetPropertyValueHistoryCommand; + commands.push(cmd as GetPropertyValueHistoryCommand); return Promise.resolve({}); }); await getPropertyValueHistoryByEntity({ onSuccess, onError, - requestInformations: [{ ...mockRequestInfo, fetchFromStartToEnd: true }], + requestInformations: [ + { ...mockRequestInfo1, fetchFromStartToEnd: true }, + { ...mockRequestInfo2, fetchFromStartToEnd: true }, + ], client: tmClient, }); - expect(command!.input.maxResults).toEqual(undefined); - expect(command!.input.orderByTime).toEqual('ASCENDING'); - expect(command!.input.startTime).toEqual(mockRequestInfo.start.toISOString()); - expect(command!.input.endTime).toEqual(mockRequestInfo.end.toISOString()); + // Should combine into the same request + expect(commands.length).toEqual(1); + expect(commands[0].input.workspaceId).toEqual(streamIdComponents1.workspaceId); + expect(commands[0].input.entityId).toEqual(streamIdComponents1.entityId); + expect(commands[0].input.componentName).toEqual(streamIdComponents1.componentName); + expect(commands[0].input.selectedProperties).toEqual([ + streamIdComponents1.propertyName, + streamIdComponents2.propertyName, + ]); + expect(commands[0].input.maxResults).toEqual(undefined); + expect(commands[0].input.orderByTime).toEqual('ASCENDING'); + expect(commands[0].input.startTime).toEqual(start.toISOString()); + expect(commands[0].input.endTime).toEqual(end.toISOString()); }); it('should trigger onSuccess with correct dataStream response', async () => { @@ -110,7 +159,7 @@ describe('getPropertyValueHistoryByEntity', () => { nextToken: '11223344', propertyValues: [ { - entityPropertyReference: mockEntityRef, + entityPropertyReference: mockEntityRef1, values: [ { value: { @@ -126,7 +175,7 @@ describe('getPropertyValueHistoryByEntity', () => { nextToken: undefined, propertyValues: [ { - entityPropertyReference: mockEntityRef, + entityPropertyReference: mockEntityRef1, values: [ { value: { @@ -136,52 +185,84 @@ describe('getPropertyValueHistoryByEntity', () => { }, ], }, + { + entityPropertyReference: mockEntityRef2, + values: [ + { + value: { + integerValue: 44, + }, + time: new Date(2022, 1, 4).toISOString(), + }, + ], + }, ], }); await getPropertyValueHistoryByEntity({ onSuccess, onError, - requestInformations: [{ ...mockRequestInfo, fetchFromStartToEnd: true }], + requestInformations: [ + { ...mockRequestInfo1, fetchFromStartToEnd: true }, + { ...mockRequestInfo2, fetchFromStartToEnd: true }, + ], client: tmClient, }); - expect(onSuccess).toBeCalledTimes(2); + expect(onSuccess).toBeCalledTimes(3); expect(onSuccess).toHaveBeenNthCalledWith( 1, [ expect.objectContaining({ - id: mockRequestInfo.id, + id: mockRequestInfo1.id, data: [ { x: new Date(2022, 1, 2).getTime(), y: 22, }, ], - meta: mockEntityRef, + meta: mockEntityRef1, }), ], - { ...mockRequestInfo, fetchFromStartToEnd: true }, - mockRequestInfo.start, - mockRequestInfo.end + { ...mockRequestInfo1, fetchFromStartToEnd: true }, + start, + end ); expect(onSuccess).toHaveBeenNthCalledWith( 2, [ expect.objectContaining({ - id: mockRequestInfo.id, + id: mockRequestInfo1.id, data: [ { x: new Date(2022, 1, 3).getTime(), y: 33, }, ], - meta: mockEntityRef, + meta: mockEntityRef1, + }), + ], + { ...mockRequestInfo1, fetchFromStartToEnd: true }, + start, + end + ); + expect(onSuccess).toHaveBeenNthCalledWith( + 3, + [ + expect.objectContaining({ + id: mockRequestInfo2.id, + data: [ + { + x: new Date(2022, 1, 4).getTime(), + y: 44, + }, + ], + meta: mockEntityRef2, }), ], - { ...mockRequestInfo, fetchFromStartToEnd: true }, - mockRequestInfo.start, - mockRequestInfo.end + { ...mockRequestInfo2, fetchFromStartToEnd: true }, + start, + end ); }); @@ -202,7 +283,7 @@ describe('getPropertyValueHistoryByEntity', () => { await getPropertyValueHistoryByEntity({ onSuccess, onError, - requestInformations: [{ ...mockRequestInfo, fetchFromStartToEnd: true }], + requestInformations: [{ ...mockRequestInfo1, fetchFromStartToEnd: true }], client: tmClient, }); expect(onError).toBeCalledTimes(1); 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 1834fbe30..823f82888 100644 --- a/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts +++ b/packages/source-iottwinmaker/src/time-series-data/client/getPropertyValueHistoryByEntity.ts @@ -1,32 +1,42 @@ import { GetPropertyValueHistoryCommand, IoTTwinMakerClient } from '@aws-sdk/client-iottwinmaker'; import { OnSuccessCallback, RequestInformationAndRange, ErrorCallback } from '@iot-app-kit/core'; +import { isEqual } from 'lodash'; import { fromDataStreamId, toDataStreamId } from '../utils/dataStreamId'; import { toDataPoint, isDefined, toDataStream } from '../utils/values'; export const getPropertyValueHistoryByEntityRequest = ({ - requestInformation, + workspaceId, + entityId, + componentName, + propertyNames, + requestInformations, onSuccess, onError, nextToken: prevToken, client, }: { - requestInformation: RequestInformationAndRange; + workspaceId: string; + entityId: string; + componentName: string; + propertyNames: string[]; + requestInformations: Record; onError: ErrorCallback; onSuccess: OnSuccessCallback; client: IoTTwinMakerClient; nextToken?: string; }) => { - let { start, end } = requestInformation; + const requestInformationSample = Object.values(requestInformations)[0]; + let { start, end } = requestInformationSample; // fetch leading point without mutating requestInformation - if (requestInformation.fetchMostRecentBeforeStart) { + if (requestInformationSample.fetchMostRecentBeforeStart) { end = start; start = new Date(0, 0, 0); - } else if (requestInformation.fetchMostRecentBeforeEnd) { + } else if (requestInformationSample.fetchMostRecentBeforeEnd) { start = new Date(0, 0, 0); } - const { workspaceId, entityId, componentName, propertyName } = fromDataStreamId(requestInformation.id); - const fetchMostRecent = requestInformation.fetchMostRecentBeforeStart || requestInformation.fetchMostRecentBeforeEnd; + const fetchMostRecent = + requestInformationSample.fetchMostRecentBeforeStart || requestInformationSample.fetchMostRecentBeforeEnd; return client .send( @@ -34,7 +44,7 @@ export const getPropertyValueHistoryByEntityRequest = ({ workspaceId, entityId, componentName, - selectedProperties: [propertyName], + selectedProperties: propertyNames, maxResults: fetchMostRecent ? 1 : undefined, orderByTime: fetchMostRecent ? 'DESCENDING' : 'ASCENDING', startTime: start.toISOString(), @@ -45,30 +55,47 @@ export const getPropertyValueHistoryByEntityRequest = ({ .then((response) => { if (response) { const { propertyValues, nextToken } = response; - const matchingPropertyValue = propertyValues?.find( - ({ entityPropertyReference }) => + propertyValues?.forEach(({ entityPropertyReference, values }) => { + if ( entityPropertyReference?.entityId === entityId && entityPropertyReference?.componentName === componentName && - entityPropertyReference?.propertyName === propertyName - ); - if (matchingPropertyValue?.values) { - /** Report the page of data to the data-module */ - const dataPoints = matchingPropertyValue.values - .map((propertyValue) => toDataPoint(propertyValue)) - .filter(isDefined); + entityPropertyReference?.propertyName && + propertyNames.includes(entityPropertyReference.propertyName) && + values + ) { + /** Report the page of data to the data-module */ + const dataPoints = values.map((propertyValue) => toDataPoint(propertyValue)).filter(isDefined); - const streamId = toDataStreamId({ workspaceId, entityId, componentName, propertyName }); - onSuccess( - [toDataStream({ streamId, dataPoints, entityId, componentName, propertyName })], - requestInformation, - start, - end - ); - } + const streamId = toDataStreamId({ + workspaceId, + entityId, + componentName, + propertyName: entityPropertyReference.propertyName, + }); + onSuccess( + [ + toDataStream({ + streamId, + dataPoints, + entityId, + componentName, + propertyName: entityPropertyReference.propertyName, + }), + ], + requestInformations[streamId], + start, + end + ); + } + }); if (nextToken && !fetchMostRecent) { getPropertyValueHistoryByEntityRequest({ - requestInformation, + workspaceId, + entityId, + componentName, + propertyNames, + requestInformations, onError, onSuccess, nextToken, @@ -78,11 +105,12 @@ export const getPropertyValueHistoryByEntityRequest = ({ } }) .catch((err) => { - const id = toDataStreamId({ workspaceId, entityId, componentName, propertyName }); - onError({ - id, - resolution: 0, - error: { msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }, + Object.keys(requestInformations).forEach((id) => { + onError({ + id, + resolution: 0, + error: { msg: err.message, type: err.name, status: err.$metadata?.httpStatusCode }, + }); }); }); }; @@ -98,10 +126,73 @@ export const getPropertyValueHistoryByEntity = async ({ client: IoTTwinMakerClient; requestInformations: RequestInformationAndRange[]; }): Promise => { - // TODO: May bundle the requests for the same entity & component, but different properties into the same call - const requests = requestInformations.map((requestInformation) => { + const requestInputs: { + params: { entityId: string; componentName: string; workspaceId: string }; + propertyNames: string[]; + requestInfos: Record; + }[] = []; + + requestInformations.forEach(async (info) => { + const { workspaceId, entityId, componentName, propertyName } = fromDataStreamId(info.id); + // Don't combine different propertyNames into the same API call when fetching most recent is true + // because setting maxResults = 1 doesn't work as wanted in this case to return 1 result per property. + if (info.fetchMostRecentBeforeEnd || info.fetchMostRecentBeforeStart) { + requestInputs.push({ + params: { + workspaceId, + entityId, + componentName, + }, + propertyNames: [propertyName], + requestInfos: { [info.id]: info }, + }); + return; + } + + // Find the group of the requests for the same API call + const inputIndex = requestInputs.findIndex((input) => { + const ids = fromDataStreamId(info.id); + if (ids.workspaceId !== workspaceId || ids.entityId !== entityId || ids.componentName !== componentName) + return false; + + const requestInfo = Object.values(input.requestInfos)[0]; + // Compare all fields except id since it has entity info and is different for different request + const inputInfo = { + ...requestInfo, + id: undefined, + start: requestInfo.start.getTime(), + end: requestInfo.end.getTime(), + }; + const compareInfo = { ...info, id: undefined, start: info.start.getTime(), end: info.end.getTime() }; + + return isEqual(inputInfo, compareInfo); + }); + + if (inputIndex >= 0) { + requestInputs[inputIndex].requestInfos[info.id] = info; + if (!requestInputs[inputIndex].propertyNames.includes(propertyName)) { + requestInputs[inputIndex].propertyNames.push(propertyName); + } + } else { + requestInputs.push({ + params: { + workspaceId, + entityId, + componentName, + }, + propertyNames: [propertyName], + requestInfos: { [info.id]: info }, + }); + } + }); + + const requests = requestInputs.map(async (input) => { return getPropertyValueHistoryByEntityRequest({ - requestInformation, + workspaceId: input.params.workspaceId, + entityId: input.params.entityId, + componentName: input.params.componentName, + propertyNames: input.propertyNames, + requestInformations: input.requestInfos, onSuccess, onError, client,