Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete associated data when removing sighting #108

Merged
merged 10 commits into from
May 31, 2024
47 changes: 47 additions & 0 deletions packages/app/lib/models/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -59,3 +63,46 @@ abstract class Paginator<T> {
return next;
}
}

Future<List<Map<String, dynamic>>> paginateOverEverything(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FetchAll pagination list widget contains similar logic, we could probably reuse this method over there. Will raise a separate issue for that though.

SchemaId schemaId, String fields,
{String filter = '', int pageSize = DEFAULT_PAGE_SIZE}) async {
String filterStr = filter.isNotEmpty ? "filter: { $filter }," : "";

bool hasNextPage = true;
String? endCursor;
List<Map<String, dynamic>> 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<String, dynamic>);
}
}

return documents;
}
2 changes: 1 addition & 1 deletion packages/app/lib/models/blobs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});

Expand Down
59 changes: 54 additions & 5 deletions packages/app/lib/models/location.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -157,7 +159,6 @@ String locationQuery(DocumentId sightingId) {
const locationTreeSchemaId = SchemaIds.bee_attributes_location_tree;

String parameters = '''
first: 1,
filter: {
sighting: { eq: "$sightingId" },
},
Expand Down Expand Up @@ -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<void> 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<Location> locations =
getAllLocationsFromResult(result.data as Map<String, dynamic>);

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<String, dynamic> result) {
var boxLocations = result[BOX_RESULTS_KEY]['documents'] as List;
var buildingLocations = result[BUILDING_RESULTS_KEY]['documents'] as List;
Expand Down Expand Up @@ -225,6 +249,31 @@ Location? getLocationFromResults(Map<String, dynamic> result) {
return null;
}

List<Location> getAllLocationsFromResult(Map<String, dynamic> result) {
List<Location> list = [];

for (var item in result[BOX_RESULTS_KEY]['documents'] as List) {
list.add(Location.fromJson(LocationType.Box, item as Map<String, dynamic>));
}

for (var item in result[BUILDING_RESULTS_KEY]['documents'] as List) {
list.add(
Location.fromJson(LocationType.Building, item as Map<String, dynamic>));
}

for (var item in result[GROUND_RESULTS_KEY]['documents'] as List) {
list.add(
Location.fromJson(LocationType.Ground, item as Map<String, dynamic>));
}

for (var item in result[TREE_RESULTS_KEY]['documents'] as List) {
list.add(
Location.fromJson(LocationType.Tree, item as Map<String, dynamic>));
}

return list;
}

/*
* Location: Tree
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/app/lib/models/sightings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,6 +120,15 @@ class Sighting {
}

Future<DocumentViewId> 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;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/app/lib/models/used_for.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:app/models/schema_ids.dart';
class UsedFor {
final DocumentId id;
DocumentViewId viewId;

DocumentId sighting;
String usedFor;

Expand Down Expand Up @@ -102,7 +103,8 @@ String allUsesQuery(List<DocumentId>? 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;
Expand Down Expand Up @@ -137,3 +139,13 @@ Future<DocumentViewId> createUsedFor(
Future<DocumentViewId> deleteUsedFor(DocumentViewId viewId) async {
return await delete(SchemaIds.bee_attributes_used_for, viewId);
}

Future<void> 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();
}
}
2 changes: 1 addition & 1 deletion packages/app/lib/ui/screens/sighting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class _SightingScreenState extends State<SightingScreen> {
backgroundColor: MeliColors.electric,
appBarColor: MeliColors.electric,
actionRight: sighting != null
? SightingPopupMenu(viewId: sighting.viewId)
? SightingPopupMenu(sighting: sighting)
: null,
body: SingleChildScrollView(
child: Builder(builder: (BuildContext context) {
Expand Down
7 changes: 3 additions & 4 deletions packages/app/lib/ui/widgets/sighting_popup_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down