Skip to content

Commit b896a75

Browse files
nicksanfordnjooma
andauthored
CONSULT-1797-implement-FileUpload-on-the-typescript-SDK (#642)
Co-authored-by: Naveed Jooma <[email protected]>
1 parent f8ff33f commit b896a75

File tree

2 files changed

+287
-33
lines changed

2 files changed

+287
-33
lines changed

src/app/data-client.spec.ts

Lines changed: 156 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { BSON } from 'bsonfy';
21
import { Struct, Timestamp, type JsonValue } from '@bufbuild/protobuf';
32
import { createRouterTransport, type Transport } from '@connectrpc/connect';
3+
import { BSON } from 'bsonfy';
44
import { beforeEach, describe, expect, it, vi } from 'vitest';
55
import { DataService } from '../gen/app/data/v1/data_connect';
66
import {
@@ -33,6 +33,8 @@ import {
3333
Filter,
3434
GetDatabaseConnectionRequest,
3535
GetDatabaseConnectionResponse,
36+
GetLatestTabularDataRequest,
37+
GetLatestTabularDataResponse,
3638
RemoveBinaryDataFromDatasetByIDsRequest,
3739
RemoveBinaryDataFromDatasetByIDsResponse,
3840
RemoveBoundingBoxFromImageByIDRequest,
@@ -50,9 +52,23 @@ import {
5052
TagsByFilterRequest,
5153
TagsByFilterResponse,
5254
TagsFilter,
53-
GetLatestTabularDataRequest,
54-
GetLatestTabularDataResponse,
5555
} from '../gen/app/data/v1/data_pb';
56+
import { DataPipelinesService } from '../gen/app/datapipelines/v1/data_pipelines_connect';
57+
import {
58+
CreateDataPipelineRequest,
59+
CreateDataPipelineResponse,
60+
DataPipeline,
61+
DataPipelineRun,
62+
DataPipelineRunStatus,
63+
DeleteDataPipelineRequest,
64+
DeleteDataPipelineResponse,
65+
GetDataPipelineRequest,
66+
GetDataPipelineResponse,
67+
ListDataPipelineRunsRequest,
68+
ListDataPipelineRunsResponse,
69+
ListDataPipelinesRequest,
70+
ListDataPipelinesResponse,
71+
} from '../gen/app/datapipelines/v1/data_pipelines_pb';
5672
import { DatasetService } from '../gen/app/dataset/v1/dataset_connect';
5773
import {
5874
CreateDatasetRequest,
@@ -72,34 +88,25 @@ import {
7288
DataCaptureUploadRequest,
7389
DataCaptureUploadResponse,
7490
DataType,
91+
FileData,
92+
FileUploadRequest,
93+
FileUploadResponse,
7594
SensorData,
7695
SensorMetadata,
7796
UploadMetadata,
7897
} from '../gen/app/datasync/v1/data_sync_pb';
79-
import { DataClient, type FilterOptions } from './data-client';
8098
import {
81-
DataPipeline,
82-
ListDataPipelinesRequest,
83-
ListDataPipelinesResponse,
84-
GetDataPipelineRequest,
85-
GetDataPipelineResponse,
86-
CreateDataPipelineRequest,
87-
CreateDataPipelineResponse,
88-
DeleteDataPipelineRequest,
89-
DeleteDataPipelineResponse,
90-
DataPipelineRun,
91-
DataPipelineRunStatus,
92-
ListDataPipelineRunsRequest,
93-
ListDataPipelineRunsResponse,
94-
} from '../gen/app/datapipelines/v1/data_pipelines_pb';
95-
import { DataPipelinesService } from '../gen/app/datapipelines/v1/data_pipelines_connect';
99+
DataClient,
100+
type FileUploadOptions,
101+
type FilterOptions,
102+
} from './data-client';
96103
vi.mock('../gen/app/data/v1/data_pb_service');
97104

98105
let mockTransport: Transport;
99106
const subject = () => new DataClient(mockTransport);
100107

101108
describe('DataClient tests', () => {
102-
const filter = subject().createFilter({
109+
const filter = DataClient.createFilter({
103110
componentName: 'testComponentName',
104111
componentType: 'testComponentType',
105112
});
@@ -1035,13 +1042,13 @@ describe('DataClient tests', () => {
10351042

10361043
describe('createFilter tests', () => {
10371044
it('create empty filter', () => {
1038-
const testFilter = subject().createFilter({});
1045+
const testFilter = DataClient.createFilter({});
10391046
expect(testFilter).toEqual(new Filter());
10401047
});
10411048

10421049
it('create filter', () => {
10431050
const opts = { componentName: 'camera' };
1044-
const testFilter = subject().createFilter(opts);
1051+
const testFilter = DataClient.createFilter(opts);
10451052

10461053
const expectedFilter = new Filter({
10471054
componentName: 'camera',
@@ -1089,7 +1096,7 @@ describe('DataClient tests', () => {
10891096
endTime,
10901097
tags: tagsList,
10911098
};
1092-
const testFilter = subject().createFilter(opts);
1099+
const testFilter = DataClient.createFilter(opts);
10931100
expect(testFilter.componentType).toEqual('testComponentType');
10941101

10951102
const expectedFilter = new Filter({
@@ -1716,3 +1723,129 @@ describe('DataPipelineClient tests', () => {
17161723
});
17171724
});
17181725
});
1726+
1727+
describe('fileUpload tests', () => {
1728+
const partId = 'testPartId';
1729+
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
1730+
const options: FileUploadOptions = {
1731+
componentType: 'componentType',
1732+
componentName: 'componentName',
1733+
methodName: 'methodName',
1734+
fileName: 'fileName',
1735+
fileExtension: '.png',
1736+
tags: ['testTag1', 'testTag2'],
1737+
datasetIds: ['dataset1', 'dataset2'],
1738+
};
1739+
1740+
const expectedFileId = 'testFileId';
1741+
const expectedBinaryDataId = 'testBinaryDataId';
1742+
1743+
let capturedRequests: FileUploadRequest[];
1744+
1745+
beforeEach(() => {
1746+
capturedRequests = [];
1747+
mockTransport = createRouterTransport(({ service }) => {
1748+
service(DataSyncService, {
1749+
fileUpload: async (requests: AsyncIterable<FileUploadRequest>) => {
1750+
for await (const request of requests) {
1751+
capturedRequests.push(request);
1752+
}
1753+
return new FileUploadResponse({
1754+
fileId: expectedFileId,
1755+
binaryDataId: expectedBinaryDataId,
1756+
});
1757+
},
1758+
});
1759+
});
1760+
});
1761+
1762+
it('uploads file with metadata and file contents', async () => {
1763+
const result = await subject().fileUpload(binaryData, partId, options);
1764+
1765+
expect(result).toBe(expectedBinaryDataId);
1766+
expect(capturedRequests).toHaveLength(2);
1767+
1768+
// Check metadata request
1769+
const metadataRequest = capturedRequests[0]!;
1770+
expect(metadataRequest.uploadPacket.case).toBe('metadata');
1771+
const metadata = metadataRequest.uploadPacket.value as UploadMetadata;
1772+
expect(metadata.partId).toBe(partId);
1773+
expect(metadata.type).toBe(DataType.FILE);
1774+
expect(metadata.componentType).toBe(options.componentType);
1775+
expect(metadata.componentName).toBe(options.componentName);
1776+
expect(metadata.methodName).toBe(options.methodName);
1777+
expect(metadata.fileName).toBe(options.fileName);
1778+
expect(metadata.fileExtension).toBe(options.fileExtension);
1779+
expect(metadata.tags).toStrictEqual(options.tags);
1780+
expect(metadata.datasetIds).toStrictEqual(options.datasetIds);
1781+
1782+
// Check file contents request
1783+
const fileContentsRequest = capturedRequests[1]!;
1784+
expect(fileContentsRequest.uploadPacket.case).toBe('fileContents');
1785+
const fileContents = fileContentsRequest.uploadPacket.value as FileData;
1786+
expect(fileContents.data).toEqual(binaryData);
1787+
});
1788+
1789+
it('uploads file without optional parameters', async () => {
1790+
const result = await subject().fileUpload(binaryData, partId);
1791+
1792+
expect(result).toBe(expectedBinaryDataId);
1793+
expect(capturedRequests).toHaveLength(2);
1794+
1795+
// Check metadata request
1796+
const metadataRequest = capturedRequests[0]!;
1797+
expect(metadataRequest.uploadPacket.case).toBe('metadata');
1798+
const metadata = metadataRequest.uploadPacket.value as UploadMetadata;
1799+
expect(metadata.partId).toBe(partId);
1800+
expect(metadata.type).toBe(DataType.FILE);
1801+
expect(metadata.componentType).toBe('');
1802+
expect(metadata.componentName).toBe('');
1803+
expect(metadata.methodName).toBe('');
1804+
expect(metadata.fileName).toBe('');
1805+
expect(metadata.fileExtension).toBe('');
1806+
expect(metadata.tags).toStrictEqual([]);
1807+
expect(metadata.datasetIds).toStrictEqual([]);
1808+
1809+
// Check file contents request
1810+
const fileContentsRequest = capturedRequests[1]!;
1811+
expect(fileContentsRequest.uploadPacket.case).toBe('fileContents');
1812+
const fileContents = fileContentsRequest.uploadPacket.value as FileData;
1813+
expect(fileContents.data).toEqual(binaryData);
1814+
});
1815+
1816+
it('chunks file data', async () => {
1817+
const numChunks = 3;
1818+
const data = Uint8Array.from(
1819+
{ length: DataClient.UPLOAD_CHUNK_SIZE * numChunks },
1820+
() => Math.floor(Math.random() * 256)
1821+
);
1822+
1823+
const result = await subject().fileUpload(data, partId);
1824+
expect(result).toBe(expectedBinaryDataId);
1825+
expect(capturedRequests).toHaveLength(1 + numChunks);
1826+
1827+
const metadataRequest = capturedRequests[0]!;
1828+
expect(metadataRequest.uploadPacket.case).toBe('metadata');
1829+
1830+
const contentRequests = capturedRequests.slice(1);
1831+
expect(contentRequests).toHaveLength(numChunks);
1832+
1833+
const receivedLength = contentRequests.reduce(
1834+
(acc, val) => acc + (val.uploadPacket.value as FileData).data.length,
1835+
0
1836+
);
1837+
expect(receivedLength).toEqual(numChunks * DataClient.UPLOAD_CHUNK_SIZE);
1838+
1839+
const receivedData = new Uint8Array(receivedLength);
1840+
let offset = 0;
1841+
for (const req of contentRequests) {
1842+
expect(req.uploadPacket.case).toBe('fileContents');
1843+
const fileData = req.uploadPacket.value as FileData;
1844+
expect(fileData.data).toHaveLength(DataClient.UPLOAD_CHUNK_SIZE);
1845+
receivedData.set(fileData.data, offset);
1846+
offset += fileData.data.length;
1847+
}
1848+
1849+
expect(receivedData).toStrictEqual(data);
1850+
});
1851+
});

0 commit comments

Comments
 (0)