diff --git a/lib/src/app/data.dart b/lib/src/app/data.dart index bd48329636..fd5293c9b1 100644 --- a/lib/src/app/data.dart +++ b/lib/src/app/data.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:viam_sdk/protos/app/data.dart'; import 'package:viam_sdk/src/gen/google/protobuf/any.pb.dart'; import 'package:viam_sdk/src/utils.dart'; @@ -13,6 +14,8 @@ import '../gen/app/datasync/v1/data_sync.pbgrpc.dart' hide CaptureInterval; import '../gen/google/protobuf/timestamp.pb.dart'; import '../media/image.dart'; +typedef DatabaseConnection = GetDatabaseConnectionResponse; + /// gRPC client for the [DataClient]. Used for retrieving stored data from app.viam.com. /// /// All calls must be authenticated. @@ -133,6 +136,148 @@ class DataClient { return response.data.map((e) => e.toMap()).toList(); } + /// Delete tabular data older than a provided number of days from an organization. + /// + /// Returns the number of pieces of data that were deleted. + Future deleteTabularData(String organizationId, int olderThanDays) async { + final request = DeleteTabularDataRequest() + ..organizationId = organizationId + ..deleteOlderThanDays = olderThanDays; + final response = await _dataClient.deleteTabularData(request); + return response.deletedCount.toInt(); + } + + /// Delete binary data based on an optionally provided filter. + /// If a [filter] is not provided, all data will be deleted. + /// + /// Returns the number of pieces of data that were deleted. + Future deleteBinaryDataByFilter(Filter? filter, {bool includeInternalData = false}) async { + final request = DeleteBinaryDataByFilterRequest() + ..includeInternalData = includeInternalData + ..filter = filter ?? Filter(); + final response = await _dataClient.deleteBinaryDataByFilter(request); + return response.deletedCount.toInt(); + } + + /// Delete binary data based on data ID. + /// + /// Returns the number of pieces of data that were deleted. + Future deleteBinaryDataByIds(List binaryIds) async { + final request = DeleteBinaryDataByIDsRequest()..binaryIds.addAll(binaryIds); + final response = await _dataClient.deleteBinaryDataByIDs(request); + return response.deletedCount.toInt(); + } + + /// Adds tags to binary data based on IDs. + Future addTagsToBinaryDataByIds(List tags, List binaryIds) async { + final request = AddTagsToBinaryDataByIDsRequest() + ..tags.addAll(tags) + ..binaryIds.addAll(binaryIds); + await _dataClient.addTagsToBinaryDataByIDs(request); + } + + /// Adds tags to binary data based on a filter. + /// If no [filter] is provided, all binary data will be tagged. + Future addTagsToBinaryDataByFilter(List tags, Filter? filter) async { + final request = AddTagsToBinaryDataByFilterRequest() + ..tags.addAll(tags) + ..filter = filter ?? Filter(); + await _dataClient.addTagsToBinaryDataByFilter(request); + } + + /// Remove tags from binary data based on filter. + /// If a [filter] is not provided, the tags will be removed from all data. + /// + /// Returns the number of tags deleted. + Future removeTagsFromBinaryDataByFilter(List tags, Filter? filter) async { + final request = RemoveTagsFromBinaryDataByFilterRequest() + ..tags.addAll(tags) + ..filter = filter ?? Filter(); + final response = await _dataClient.removeTagsFromBinaryDataByFilter(request); + return response.deletedCount.toInt(); + } + + /// Remove tags from binary data based on IDs. + /// + /// Returns the number of tags deleted. + Future removeTagsFromBinaryDataByIds(List tags, List binaryIds) async { + final request = RemoveTagsFromBinaryDataByIDsRequest() + ..tags.addAll(tags) + ..binaryIds.addAll(binaryIds); + final response = await _dataClient.removeTagsFromBinaryDataByIDs(request); + return response.deletedCount.toInt(); + } + + /// Add a bounding box to an image by ID, with x and y coordinates normalized from 0 to 1. + /// + /// Returns the bounding box ID. + Future addBoundingBoxToImageById( + String label, BinaryID binaryId, double xMinNormalized, double yMinNormalized, double xMaxNormalized, double yMaxNormalized) async { + final request = AddBoundingBoxToImageByIDRequest() + ..label = label + ..binaryId = binaryId + ..xMinNormalized = xMinNormalized + ..yMinNormalized = yMinNormalized + ..xMaxNormalized = xMaxNormalized + ..yMaxNormalized = yMaxNormalized; + final response = await _dataClient.addBoundingBoxToImageByID(request); + return response.bboxId; + } + + /// Removes a bounding box from an image based on bbox ID and image ID. + Future removeBoundingBoxFromImageById(String bboxId, BinaryID binaryId) async { + final request = RemoveBoundingBoxFromImageByIDRequest() + ..bboxId = bboxId + ..binaryId = binaryId; + await _dataClient.removeBoundingBoxFromImageByID(request); + } + + /// Returns a list of tags based on a filter. + /// If no [filter] is provided, all tags will be returned. + Future> tagsByFilter(Filter? filter) async { + final request = TagsByFilterRequest()..filter = filter ?? Filter(); + final response = await _dataClient.tagsByFilter(request); + return response.tags; + } + + /// Returns a list of bounding box labels based on a filter. + /// If no [filter] is provided, all labels will be returned. + Future> boundingBoxLabelsByFilter(Filter? filter) async { + final request = BoundingBoxLabelsByFilterRequest()..filter = filter ?? Filter(); + final response = await _dataClient.boundingBoxLabelsByFilter(request); + return response.labels; + } + + /// Returns a database connection to access a MongoDB Atlas Data Federation instance. + Future getDatabaseConnection(String organizationId) async { + final request = GetDatabaseConnectionRequest()..organizationId = organizationId; + return await _dataClient.getDatabaseConnection(request); + } + + /// Configures a database user for Viam's MongoDB Atlas Data Federation instance. + Future configureDatabaseUser(String organizationId, String password) async { + final request = ConfigureDatabaseUserRequest() + ..password = password + ..organizationId = organizationId; + await _dataClient.configureDatabaseUser(request); + } + + /// Adds binary data to a dataset based on IDs. + Future addBinaryDataToDatasetByIds(List binaryIds, String datasetId) async { + final request = AddBinaryDataToDatasetByIDsRequest() + ..binaryIds.addAll(binaryIds) + ..datasetId = datasetId; + await _dataClient.addBinaryDataToDatasetByIDs(request); + } + + /// Removes binary data from a dataset based on IDs. + Future removeBinaryDataFromDatasetByIds(List binaryIds, String datasetId) async { + final request = RemoveBinaryDataFromDatasetByIDsRequest() + ..binaryIds.addAll(binaryIds) + ..datasetId = datasetId; + await _dataClient.removeBinaryDataFromDatasetByIDs(request); + } + /// Upload an image to Viam's Data Manager /// /// If no name is provided, the current timestamp will be used as the filename. diff --git a/test/unit_test/app/data_client_test.dart b/test/unit_test/app/data_client_test.dart index 3ff502b5e5..18d329853f 100644 --- a/test/unit_test/app/data_client_test.dart +++ b/test/unit_test/app/data_client_test.dart @@ -8,6 +8,9 @@ import 'package:mockito/mockito.dart'; import 'package:viam_sdk/protos/app/data.dart'; import 'package:viam_sdk/protos/app/data_sync.dart' hide CaptureInterval; import 'package:viam_sdk/src/app/data.dart'; +import 'package:viam_sdk/src/gen/app/data/v1/data.pb.dart'; +import 'package:viam_sdk/src/gen/app/data/v1/data.pbgrpc.dart'; +import 'package:viam_sdk/src/gen/app/data/v1/data.pbjson.dart'; import 'package:viam_sdk/src/media/image.dart'; import 'package:viam_sdk/src/utils.dart'; @@ -150,6 +153,122 @@ void main() { final response = await dataClient.tabularDataByMql('some_org_id', [Uint8List.fromList('some_query'.codeUnits)]); expect(response, equals(data)); }); + + test('deleteTabularData', () async { + when(serviceClient.deleteTabularData(any)) + .thenAnswer((_) => MockResponseFuture.value(DeleteTabularDataResponse()..deletedCount = Int64(12))); + + final response = await dataClient.deleteTabularData('some_org_id', 5); + expect(response, equals(12)); + }); + + test('deleteBinaryDataByFilter', () async { + when(serviceClient.deleteBinaryDataByFilter(any)) + .thenAnswer((_) => MockResponseFuture.value(DeleteBinaryDataByFilterResponse()..deletedCount = Int64(12))); + + final response = await dataClient.deleteBinaryDataByFilter(Filter(), includeInternalData: true); + expect(response, equals(12)); + }); + + test('deleteBinaryDataByIds', () async { + when(serviceClient.deleteBinaryDataByIDs(any)) + .thenAnswer((_) => MockResponseFuture.value(DeleteBinaryDataByIDsResponse()..deletedCount = Int64(12))); + + final response = await dataClient.deleteBinaryDataByIds([BinaryID(fileId: 'file', organizationId: 'orgId', locationId: 'locId')]); + expect(response, equals(12)); + }); + + test('addTagsToBinaryDataByIds', () async { + when(serviceClient.addTagsToBinaryDataByIDs(any)).thenAnswer((_) => MockResponseFuture.value(AddTagsToBinaryDataByIDsResponse())); + await dataClient.addTagsToBinaryDataByIds(['tags'], [BinaryID(fileId: 'file', organizationId: 'orgId', locationId: 'locId')]); + verify(serviceClient.addTagsToBinaryDataByIDs(any)).called(1); + }); + + test('addTagsToBinaryDataByFilter', () async { + when(serviceClient.addTagsToBinaryDataByFilter(any)) + .thenAnswer((_) => MockResponseFuture.value(AddTagsToBinaryDataByFilterResponse())); + await dataClient.addTagsToBinaryDataByFilter(['tags'], Filter()); + verify(serviceClient.addTagsToBinaryDataByFilter(any)).called(1); + }); + + test('removeTagsFromBinaryDataByFilter', () async { + when(serviceClient.removeTagsFromBinaryDataByFilter(any)) + .thenAnswer((_) => MockResponseFuture.value(RemoveTagsFromBinaryDataByFilterResponse(deletedCount: Int64(15)))); + + final response = await dataClient.removeTagsFromBinaryDataByFilter(['tags'], Filter()); + expect(response, equals(15)); + }); + + test('removeTagsFromBinaryDataByIds', () async { + when(serviceClient.removeTagsFromBinaryDataByIDs(any)) + .thenAnswer((_) => MockResponseFuture.value(RemoveTagsFromBinaryDataByIDsResponse(deletedCount: Int64(18)))); + + final response = await dataClient + .removeTagsFromBinaryDataByIds(['tags'], [BinaryID(organizationId: 'orgId', locationId: 'locId', fileId: 'fileId')]); + expect(response, equals(18)); + }); + + test('addBoundingBoxToImageById', () async { + when(serviceClient.addBoundingBoxToImageByID(any)) + .thenAnswer((_) => MockResponseFuture.value(AddBoundingBoxToImageByIDResponse(bboxId: 'bboxId'))); + + final response = await dataClient.addBoundingBoxToImageById( + 'label', BinaryID(organizationId: 'orgId', locationId: 'locId', fileId: 'fileId'), 0.1, 0.2, 0.3, 0.4); + expect(response, equals('bboxId')); + }); + + test('removeBoundingBoxFromImageById', () async { + when(serviceClient.removeBoundingBoxFromImageByID(any)) + .thenAnswer((_) => MockResponseFuture.value(RemoveBoundingBoxFromImageByIDResponse())); + await dataClient.removeBoundingBoxFromImageById('bboxId', BinaryID(organizationId: 'orgId', locationId: 'locId', fileId: 'fileId')); + verify(serviceClient.removeBoundingBoxFromImageByID(any)).called(1); + }); + + test('tagsByFilter', () async { + when(serviceClient.tagsByFilter(any)).thenAnswer((_) => MockResponseFuture.value(TagsByFilterResponse(tags: ['tags']))); + + final response = await dataClient.tagsByFilter(Filter()); + expect(response, equals(['tags'])); + }); + + test('boundingBoxLabelsByFilter', () async { + when(serviceClient.boundingBoxLabelsByFilter(any)) + .thenAnswer((_) => MockResponseFuture.value(BoundingBoxLabelsByFilterResponse(labels: ['label']))); + + final response = await dataClient.boundingBoxLabelsByFilter(Filter()); + expect(response, equals(['label'])); + }); + + test('getDatabaseConnection', () async { + when(serviceClient.getDatabaseConnection(any)).thenAnswer((_) => + MockResponseFuture.value(GetDatabaseConnectionResponse(hostname: 'hostname', mongodbUri: 'mongo', hasDatabaseUser: true))); + + final response = await dataClient.getDatabaseConnection('orgId'); + expect(response.hostname, equals('hostname')); + expect(response.mongodbUri, equals('mongo')); + expect(response.hasDatabaseUser, equals(true)); + }); + + test('configureDatabaseUser', () async { + when(serviceClient.configureDatabaseUser(any)).thenAnswer((_) => MockResponseFuture.value(ConfigureDatabaseUserResponse())); + await dataClient.configureDatabaseUser('orgId', 'password'); + verify(serviceClient.configureDatabaseUser(any)).called(1); + }); + + test('addBinaryDataToDatasetByIds', () async { + when(serviceClient.addBinaryDataToDatasetByIDs(any)) + .thenAnswer((_) => MockResponseFuture.value(AddBinaryDataToDatasetByIDsResponse())); + await dataClient.addBinaryDataToDatasetByIds([BinaryID(fileId: 'fileId', organizationId: 'orgId', locationId: 'locId')], 'dataset'); + verify(serviceClient.addBinaryDataToDatasetByIDs(any)).called(1); + }); + + test('removeBinaryDataFromDatasetByIds', () async { + when(serviceClient.removeBinaryDataFromDatasetByIDs(any)) + .thenAnswer((_) => MockResponseFuture.value(RemoveBinaryDataFromDatasetByIDsResponse())); + await dataClient + .removeBinaryDataFromDatasetByIds([BinaryID(fileId: 'fileId', organizationId: 'orgId', locationId: 'locId')], 'dataset'); + verify(serviceClient.removeBinaryDataFromDatasetByIDs(any)).called(1); + }); }); group('DataSync Tests', () {