diff --git a/src/app/data-client.spec.ts b/src/app/data-client.spec.ts index 217cb865a..398c9bdc4 100644 --- a/src/app/data-client.spec.ts +++ b/src/app/data-client.spec.ts @@ -28,6 +28,8 @@ import { DeleteBinaryDataByFilterResponse, DeleteBinaryDataByIDsResponse, DeleteTabularDataResponse, + ExportTabularDataRequest, + ExportTabularDataResponse, Filter, GetDatabaseConnectionRequest, GetDatabaseConnectionResponse, @@ -102,6 +104,102 @@ describe('DataClient tests', () => { locationId: 'testLocationId', }); + describe('exportTabularData tests', () => { + const sharedAttributes = { + partId: 'partId1', + resourceName: 'resource1', + resourceSubtype: 'resource1:subtype', + methodName: 'Readings', + organizationId: 'orgId1', + locationId: 'locationId1', + robotName: 'robot1', + robotId: 'robotId1', + partName: 'part1', + tags: [], + }; + const timeCaptured1 = new Date(2024, 1, 1); + const timeCaptured2 = new Date(2024, 1, 2); + const tabDataResponse1 = new ExportTabularDataResponse({ + ...sharedAttributes, + methodParameters: Struct.fromJson({ key: 'param1' }), + timeCaptured: Timestamp.fromDate(timeCaptured1), + payload: Struct.fromJson({ key: 'value1' }), + }); + const tabDataResponse2 = new ExportTabularDataResponse({ + ...sharedAttributes, + methodParameters: Struct.fromJson({ key: 'param2' }), + timeCaptured: Timestamp.fromDate(timeCaptured2), + payload: Struct.fromJson({ key: 'value2' }), + }); + + let capReq: ExportTabularDataRequest; + beforeEach(() => { + mockTransport = createRouterTransport(({ service }) => { + service(DataService, { + exportTabularData: (req) => ({ + [Symbol.asyncIterator]: async function* generateResponses() { + await Promise.resolve(); + capReq = req; + yield tabDataResponse1; + yield tabDataResponse2; + }, + }), + }); + }); + }); + + it('gets tabular data', async () => { + const data = await subject().exportTabularData( + 'partId1', + 'resource1', + 'resource1:subtype', + 'Readings' + ); + + expect(data.length).toEqual(2); + + const expectedResponse1 = { + ...sharedAttributes, + methodParameters: { key: 'param1' }, + timeCaptured: timeCaptured1, + payload: { key: 'value1' }, + }; + const expectedResponse2 = { + ...sharedAttributes, + methodParameters: { key: 'param2' }, + timeCaptured: timeCaptured2, + payload: { key: 'value2' }, + }; + + expect(data[0]).toMatchObject(expectedResponse1); + expect(data[1]).toMatchObject(expectedResponse2); + }); + + it('gets tabular data for an interval', async () => { + await subject().exportTabularData( + 'partId1', + 'resource1', + 'resource1:subtype', + 'Readings', + timeCaptured1, + timeCaptured2 + ); + + const expectedRequest = new ExportTabularDataRequest({ + partId: 'partId1', + resourceName: 'resource1', + resourceSubtype: 'resource1:subtype', + methodName: 'Readings', + interval: { + start: Timestamp.fromDate(timeCaptured1), + end: Timestamp.fromDate(timeCaptured2), + }, + }); + + expect(capReq).toStrictEqual(expectedRequest); + }); + }); + describe('tabularDataBySQL tests', () => { type returnType = JsonValue | Date; const data: Record[] = [ diff --git a/src/app/data-client.ts b/src/app/data-client.ts index c86a0e24f..75b3de978 100644 --- a/src/app/data-client.ts +++ b/src/app/data-client.ts @@ -38,6 +38,22 @@ interface TabularData { timeReceived?: Date; } +interface TabularDataPoint { + partId: string; + resourceName: string; + resourceSubtype: string; + methodName: string; + timeCaptured: Date | undefined; + organizationId: string; + locationId: string; + robotName: string; + robotId: string; + partName: string; + methodParameters: JsonValue | undefined; + tags: string[]; + payload: JsonValue | undefined; +} + export type Dataset = Partial & { created?: Date; }; @@ -53,6 +69,70 @@ export class DataClient { this.dataSyncClient = createPromiseClient(DataSyncService, transport); } + /** + * Obtain unified tabular data and metadata from the specified data source. + * + * @param partId The ID of the part that owns the data + * @param resourceName The name of the requested resource that captured the + * data + * @param resourceSubtype The subtype of the requested resource that captured + * the data + * @param methodName The data capture method name + * @param startTime Optional start time (`Date` object) for requesting a + * specific range of data + * @param endTime Optional end time (`Date` object) for requesting a specific + * range of data + * @returns An array of unified tabular data and metadata. + */ + async exportTabularData( + partId: string, + resourceName: string, + resourceSubtype: string, + methodName: string, + startTime?: Date, + endTime?: Date + ) { + const interval = new CaptureInterval(); + if (startTime) { + interval.start = Timestamp.fromDate(startTime); + } + if (endTime) { + interval.end = Timestamp.fromDate(endTime); + } + + const req = { + partId, + resourceName, + resourceSubtype, + methodName, + interval, + }; + + const responses = this.dataClient.exportTabularData(req); + + const dataArray: TabularDataPoint[] = []; + + for await (const response of responses) { + dataArray.push({ + partId: response.partId, + resourceName: response.resourceName, + resourceSubtype: response.resourceSubtype, + methodName: response.methodName, + timeCaptured: response.timeCaptured?.toDate(), + organizationId: response.organizationId, + locationId: response.locationId, + robotName: response.robotName, + robotId: response.robotId, + partName: response.partName, + methodParameters: response.methodParameters?.toJson(), + tags: response.tags, + payload: response.payload?.toJson(), + }); + } + + return dataArray; + } + /** * Obtain unified tabular data and metadata, queried with SQL. *