Skip to content

Commit

Permalink
Merge pull request #108 from p2panda/delete-associated-data
Browse files Browse the repository at this point in the history
Delete associated data when removing sighting
  • Loading branch information
sandreae authored May 31, 2024
2 parents 65b0ceb + fee3f52 commit 2a8be9e
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 12 deletions.
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(
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

0 comments on commit 2a8be9e

Please sign in to comment.