diff --git a/packages/app/lib/models/base.dart b/packages/app/lib/models/base.dart index 5457ba80..0314051e 100644 --- a/packages/app/lib/models/base.dart +++ b/packages/app/lib/models/base.dart @@ -3,6 +3,10 @@ import 'dart:ui'; import 'package:gql/ast.dart'; +import 'package:graphql/client.dart'; + +import 'package:app/io/graphql/graphql.dart'; +import 'package:app/io/p2panda/schemas.dart'; const DEFAULT_PAGE_SIZE = 10; @@ -59,3 +63,46 @@ abstract class Paginator { return next; } } + +Future>> paginateOverEverything( + SchemaId schemaId, String fields, + {String filter = '', int pageSize = DEFAULT_PAGE_SIZE}) async { + String filterStr = filter.isNotEmpty ? "filter: { $filter }," : ""; + + bool hasNextPage = true; + String? endCursor; + List> documents = []; + + while (hasNextPage) { + final afterStr = endCursor != null ? "after: \"$endCursor\"," : ""; + final document = ''' + query PaginateOverEverything { + $DEFAULT_RESULTS_KEY: all_$schemaId( + first: $pageSize, + $afterStr + $filterStr + ) { + $paginationFields + documents { + $fields + } + } + } + '''; + + final response = await client.query(QueryOptions(document: gql(document))); + if (response.hasException) { + throw "Error during pagination: ${response.exception}"; + } + + final result = response.data![DEFAULT_RESULTS_KEY]; + endCursor = result['endCursor'] as String?; + hasNextPage = result['hasNextPage'] as bool; + + for (var document in result['documents'] as List) { + documents.add(document as Map); + } + } + + return documents; +} diff --git a/packages/app/lib/models/blobs.dart b/packages/app/lib/models/blobs.dart index 4b86c7cd..5afc594d 100644 --- a/packages/app/lib/models/blobs.dart +++ b/packages/app/lib/models/blobs.dart @@ -13,7 +13,7 @@ import 'package:app/models/base.dart'; const MAX_BLOB_PIECE_LENGTH = 256 * 1000; // 256kb as per specification class Blob { - final String id; + final DocumentId id; Blob({required this.id}); diff --git a/packages/app/lib/models/location.dart b/packages/app/lib/models/location.dart index 42b88907..ccb07dbf 100644 --- a/packages/app/lib/models/location.dart +++ b/packages/app/lib/models/location.dart @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:graphql/client.dart'; import 'package:p2panda/p2panda.dart'; +import 'package:app/io/graphql/graphql.dart'; import 'package:app/io/p2panda/publish.dart'; import 'package:app/models/base.dart'; import 'package:app/models/schema_ids.dart'; @@ -157,7 +159,6 @@ String locationQuery(DocumentId sightingId) { const locationTreeSchemaId = SchemaIds.bee_attributes_location_tree; String parameters = ''' - first: 1, filter: { sighting: { eq: "$sightingId" }, }, @@ -192,10 +193,33 @@ String locationQuery(DocumentId sightingId) { '''; } -// Expects multiple results from a multi-query GraphQL request over all -// location types. This method will automatically select one of them based -// on deterministic rules as the UI can only display one location at a time -// for sightings. +/// Deletes all hive locations which are associated with a sighting. +/// +/// Even though we're only displaying _one_ hive location per sighting it might +/// be possible that others exist. To make sure we're cleaning up after ourselves +/// this method deletes _all known_ hive locations to that sighting. +Future deleteAllLocations(DocumentId sightingId) async { + final result = await client + .query(QueryOptions(document: gql(locationQuery(sightingId)))); + + if (result.hasException) { + throw "Deleting all hive locations related to sighting failed: ${result.exception}"; + } + + List locations = + getAllLocationsFromResult(result.data as Map); + + for (var location in locations) { + await location.delete(); + } +} + +/// Returns one hive location for a sighting if it exists. +/// +/// Expects multiple results from a multi-query GraphQL request over all +/// location types. This method will automatically select one of them based +/// on deterministic rules as the UI can only display one location at a time +/// for sightings. Location? getLocationFromResults(Map result) { var boxLocations = result[BOX_RESULTS_KEY]['documents'] as List; var buildingLocations = result[BUILDING_RESULTS_KEY]['documents'] as List; @@ -225,6 +249,31 @@ Location? getLocationFromResults(Map result) { return null; } +List getAllLocationsFromResult(Map result) { + List list = []; + + for (var item in result[BOX_RESULTS_KEY]['documents'] as List) { + list.add(Location.fromJson(LocationType.Box, item as Map)); + } + + for (var item in result[BUILDING_RESULTS_KEY]['documents'] as List) { + list.add( + Location.fromJson(LocationType.Building, item as Map)); + } + + for (var item in result[GROUND_RESULTS_KEY]['documents'] as List) { + list.add( + Location.fromJson(LocationType.Ground, item as Map)); + } + + for (var item in result[TREE_RESULTS_KEY]['documents'] as List) { + list.add( + Location.fromJson(LocationType.Tree, item as Map)); + } + + return list; +} + /* * Location: Tree */ diff --git a/packages/app/lib/models/sightings.dart b/packages/app/lib/models/sightings.dart index 8193c7d9..b81a21a9 100644 --- a/packages/app/lib/models/sightings.dart +++ b/packages/app/lib/models/sightings.dart @@ -8,8 +8,10 @@ import 'package:app/io/p2panda/publish.dart'; import 'package:app/models/base.dart'; import 'package:app/models/blobs.dart'; import 'package:app/models/local_names.dart'; +import 'package:app/models/location.dart'; import 'package:app/models/schema_ids.dart'; import 'package:app/models/species.dart'; +import 'package:app/models/used_for.dart'; class Sighting { final DocumentId id; @@ -118,6 +120,15 @@ class Sighting { } Future delete() async { + // Remove associated "Hive Location" documents + await deleteAllLocations(id); + + // Note: Blobs get automatically garbage-collected on node + + // Remove associated "Used For" documents + await deleteAllUsedFor(id); + + // Finally delete the sighting itself viewId = await deleteSighting(viewId); return viewId; } diff --git a/packages/app/lib/models/used_for.dart b/packages/app/lib/models/used_for.dart index 57365884..64f3ad14 100644 --- a/packages/app/lib/models/used_for.dart +++ b/packages/app/lib/models/used_for.dart @@ -11,6 +11,7 @@ import 'package:app/models/schema_ids.dart'; class UsedFor { final DocumentId id; DocumentViewId viewId; + DocumentId sighting; String usedFor; @@ -102,7 +103,8 @@ String allUsesQuery(List? sightings, String? cursor) { final after = (cursor != null) ? '''after: "$cursor",''' : ''; String filter = ''; if (sightings != null) { - String sightingsString = sightings.map((sighting) => '''"$sighting"''').join(", "); + String sightingsString = + sightings.map((sighting) => '''"$sighting"''').join(", "); filter = '''filter: { sighting: { in: [$sightingsString] } },'''; } const schemaId = SchemaIds.bee_attributes_used_for; @@ -137,3 +139,13 @@ Future createUsedFor( Future deleteUsedFor(DocumentViewId viewId) async { return await delete(SchemaIds.bee_attributes_used_for, viewId); } + +Future deleteAllUsedFor(DocumentId sightingId) async { + final jsonDocuments = await paginateOverEverything( + SchemaIds.bee_attributes_used_for, usedForFields, + filter: 'sighting: { eq: "$sightingId" }'); + + for (var json in jsonDocuments) { + await UsedFor.fromJson(json).delete(); + } +} diff --git a/packages/app/lib/ui/screens/sighting.dart b/packages/app/lib/ui/screens/sighting.dart index 048a211f..272602e8 100644 --- a/packages/app/lib/ui/screens/sighting.dart +++ b/packages/app/lib/ui/screens/sighting.dart @@ -49,7 +49,7 @@ class _SightingScreenState extends State { backgroundColor: MeliColors.electric, appBarColor: MeliColors.electric, actionRight: sighting != null - ? SightingPopupMenu(viewId: sighting.viewId) + ? SightingPopupMenu(sighting: sighting) : null, body: SingleChildScrollView( child: Builder(builder: (BuildContext context) { diff --git a/packages/app/lib/ui/widgets/sighting_popup_menu.dart b/packages/app/lib/ui/widgets/sighting_popup_menu.dart index 6a6bf6ac..10cff50f 100644 --- a/packages/app/lib/ui/widgets/sighting_popup_menu.dart +++ b/packages/app/lib/ui/widgets/sighting_popup_menu.dart @@ -10,9 +10,9 @@ import 'package:app/ui/widgets/confirm_dialog.dart'; import 'package:app/ui/widgets/refresh_provider.dart'; class SightingPopupMenu extends StatelessWidget { - final DocumentViewId viewId; + final Sighting sighting; - const SightingPopupMenu({super.key, required this.viewId}); + const SightingPopupMenu({super.key, required this.sighting}); void _onDelete(BuildContext context) { final messenger = ScaffoldMessenger.of(context); @@ -27,8 +27,7 @@ class SightingPopupMenu extends StatelessWidget { labelAbort: t.sightingDeleteAlertCancel, labelConfirm: t.sightingDeleteAlertConfirm, onConfirm: () async { - // @TODO: also delete all uses documents and hive location documents. - await deleteSighting(viewId); + await sighting.delete(); // Set flag for other widgets to tell them that they might need to // re-render their data. This will make sure that our updates are