From 4d067948abfa3e9f8f86bb4a61df74a7d69ed247 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Wed, 20 Nov 2024 15:25:12 +0530 Subject: [PATCH] [client] request bookmarks --- client/lib/app.dart | 3 +- client/lib/libphylum/db/bookmarks.drift | 7 + client/lib/libphylum/db/db.dart | 3 +- client/lib/libphylum/db/db.g.dart | 397 +++++++++++++++++- client/lib/libphylum/phylum_account.dart | 2 + .../repositories/bookmark_repository.dart | 49 +++ .../repositories/user_repository.dart | 2 +- .../libphylum/requests/bookmarks_request.dart | 17 + 8 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 client/lib/libphylum/db/bookmarks.drift create mode 100644 client/lib/libphylum/repositories/bookmark_repository.dart create mode 100644 client/lib/libphylum/requests/bookmarks_request.dart diff --git a/client/lib/app.dart b/client/lib/app.dart index 94d8973b..69991a01 100644 --- a/client/lib/app.dart +++ b/client/lib/app.dart @@ -74,7 +74,8 @@ class _PhylumAppState extends State { super.initState(); _listener = AppLifecycleListener(onStateChange: (state) { if (state == AppLifecycleState.resumed) { - widget.account.userRepository.refreshUserList(); + widget.account.userRepository.refresh(); + widget.account.bookmarkRepository.refresh(); } }); } diff --git a/client/lib/libphylum/db/bookmarks.drift b/client/lib/libphylum/db/bookmarks.drift new file mode 100644 index 00000000..a07ef77a --- /dev/null +++ b/client/lib/libphylum/db/bookmarks.drift @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS bookmarks( + resource_id TEXT NOT NULL PRIMARY KEY REFERENCES resources(id), + name TEXT NOT NULL, + deleted BOOLEAN NOT NULL CHECK (deleted IN (0, 1)) +); + +listBookmarks: SELECT resource_id, name FROM bookmarks WHERE deleted = 0; \ No newline at end of file diff --git a/client/lib/libphylum/db/db.dart b/client/lib/libphylum/db/db.dart index 5652ab42..4d8b3938 100644 --- a/client/lib/libphylum/db/db.dart +++ b/client/lib/libphylum/db/db.dart @@ -7,6 +7,7 @@ import 'unsupported.dart' if (dart.library.ffi) 'native.dart' if (dart.library.h part 'db.g.dart'; @DriftDatabase(include: { + 'bookmarks.drift', 'resources.drift', 'users.drift', }) @@ -18,7 +19,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase({required this.id}) : super(_openConnection(id)); @override - int get schemaVersion => 12; + int get schemaVersion => 13; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/client/lib/libphylum/db/db.g.dart b/client/lib/libphylum/db/db.g.dart index d174e204..b16a5e4b 100644 --- a/client/lib/libphylum/db/db.g.dart +++ b/client/lib/libphylum/db/db.g.dart @@ -852,11 +852,250 @@ class ResourcesCompanion extends UpdateCompanion { } } +class Bookmarks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Bookmarks(this.attachedDatabase, [this._alias]); + static const VerificationMeta _resourceIdMeta = + const VerificationMeta('resourceId'); + late final GeneratedColumn resourceId = GeneratedColumn( + 'resource_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY REFERENCES resources(id)'); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + static const VerificationMeta _deletedMeta = + const VerificationMeta('deleted'); + late final GeneratedColumn deleted = GeneratedColumn( + 'deleted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL CHECK (deleted IN (0, 1))'); + @override + List get $columns => [resourceId, name, deleted]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'bookmarks'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('resource_id')) { + context.handle( + _resourceIdMeta, + resourceId.isAcceptableOrUnknown( + data['resource_id']!, _resourceIdMeta)); + } else if (isInserting) { + context.missing(_resourceIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('deleted')) { + context.handle(_deletedMeta, + deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); + } else if (isInserting) { + context.missing(_deletedMeta); + } + return context; + } + + @override + Set get $primaryKey => {resourceId}; + @override + Bookmark map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Bookmark( + resourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}resource_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + deleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + ); + } + + @override + Bookmarks createAlias(String alias) { + return Bookmarks(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class Bookmark extends DataClass implements Insertable { + final String resourceId; + final String name; + final bool deleted; + const Bookmark( + {required this.resourceId, required this.name, required this.deleted}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['resource_id'] = Variable(resourceId); + map['name'] = Variable(name); + map['deleted'] = Variable(deleted); + return map; + } + + BookmarksCompanion toCompanion(bool nullToAbsent) { + return BookmarksCompanion( + resourceId: Value(resourceId), + name: Value(name), + deleted: Value(deleted), + ); + } + + factory Bookmark.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Bookmark( + resourceId: serializer.fromJson(json['resource_id']), + name: serializer.fromJson(json['name']), + deleted: serializer.fromJson(json['deleted']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'resource_id': serializer.toJson(resourceId), + 'name': serializer.toJson(name), + 'deleted': serializer.toJson(deleted), + }; + } + + Bookmark copyWith({String? resourceId, String? name, bool? deleted}) => + Bookmark( + resourceId: resourceId ?? this.resourceId, + name: name ?? this.name, + deleted: deleted ?? this.deleted, + ); + Bookmark copyWithCompanion(BookmarksCompanion data) { + return Bookmark( + resourceId: + data.resourceId.present ? data.resourceId.value : this.resourceId, + name: data.name.present ? data.name.value : this.name, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + ); + } + + @override + String toString() { + return (StringBuffer('Bookmark(') + ..write('resourceId: $resourceId, ') + ..write('name: $name, ') + ..write('deleted: $deleted') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(resourceId, name, deleted); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Bookmark && + other.resourceId == this.resourceId && + other.name == this.name && + other.deleted == this.deleted); +} + +class BookmarksCompanion extends UpdateCompanion { + final Value resourceId; + final Value name; + final Value deleted; + final Value rowid; + const BookmarksCompanion({ + this.resourceId = const Value.absent(), + this.name = const Value.absent(), + this.deleted = const Value.absent(), + this.rowid = const Value.absent(), + }); + BookmarksCompanion.insert({ + required String resourceId, + required String name, + required bool deleted, + this.rowid = const Value.absent(), + }) : resourceId = Value(resourceId), + name = Value(name), + deleted = Value(deleted); + static Insertable custom({ + Expression? resourceId, + Expression? name, + Expression? deleted, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (resourceId != null) 'resource_id': resourceId, + if (name != null) 'name': name, + if (deleted != null) 'deleted': deleted, + if (rowid != null) 'rowid': rowid, + }); + } + + BookmarksCompanion copyWith( + {Value? resourceId, + Value? name, + Value? deleted, + Value? rowid}) { + return BookmarksCompanion( + resourceId: resourceId ?? this.resourceId, + name: name ?? this.name, + deleted: deleted ?? this.deleted, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (resourceId.present) { + map['resource_id'] = Variable(resourceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (deleted.present) { + map['deleted'] = Variable(deleted.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BookmarksCompanion(') + ..write('resourceId: $resourceId, ') + ..write('name: $name, ') + ..write('deleted: $deleted, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); late final Users users = Users(this); late final Resources resources = Resources(this); + late final Bookmarks bookmarks = Bookmarks(this); Selectable parents(String id) { return customSelect( 'WITH RECURSIVE parents (id, parent, name, permissions) AS (SELECT id, parent, name, permissions FROM resources WHERE id = ?1 UNION ALL SELECT r.id, r.parent, r.name, r.permissions FROM resources AS r JOIN parents AS p ON r.id = p.parent) SELECT * FROM parents', @@ -873,11 +1112,24 @@ abstract class _$AppDatabase extends GeneratedDatabase { )); } + Selectable listBookmarks() { + return customSelect( + 'SELECT resource_id, name FROM bookmarks WHERE deleted = 0', + variables: [], + readsFrom: { + bookmarks, + }).map((QueryRow row) => ListBookmarksResult( + resourceId: row.read('resource_id'), + name: row.read('name'), + )); + } + @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [users, resources]; + List get allSchemaEntities => + [users, resources, bookmarks]; } typedef $UsersCreateCompanionBuilder = UsersCompanion Function({ @@ -1280,6 +1532,138 @@ typedef $ResourcesProcessedTableManager = ProcessedTableManager< (Resource, BaseReferences<_$AppDatabase, Resources, Resource>), Resource, PrefetchHooks Function()>; +typedef $BookmarksCreateCompanionBuilder = BookmarksCompanion Function({ + required String resourceId, + required String name, + required bool deleted, + Value rowid, +}); +typedef $BookmarksUpdateCompanionBuilder = BookmarksCompanion Function({ + Value resourceId, + Value name, + Value deleted, + Value rowid, +}); + +class $BookmarksFilterComposer extends Composer<_$AppDatabase, Bookmarks> { + $BookmarksFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get resourceId => $composableBuilder( + column: $table.resourceId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnFilters(column)); +} + +class $BookmarksOrderingComposer extends Composer<_$AppDatabase, Bookmarks> { + $BookmarksOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get resourceId => $composableBuilder( + column: $table.resourceId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deleted => $composableBuilder( + column: $table.deleted, builder: (column) => ColumnOrderings(column)); +} + +class $BookmarksAnnotationComposer extends Composer<_$AppDatabase, Bookmarks> { + $BookmarksAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get resourceId => $composableBuilder( + column: $table.resourceId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get deleted => + $composableBuilder(column: $table.deleted, builder: (column) => column); +} + +class $BookmarksTableManager extends RootTableManager< + _$AppDatabase, + Bookmarks, + Bookmark, + $BookmarksFilterComposer, + $BookmarksOrderingComposer, + $BookmarksAnnotationComposer, + $BookmarksCreateCompanionBuilder, + $BookmarksUpdateCompanionBuilder, + (Bookmark, BaseReferences<_$AppDatabase, Bookmarks, Bookmark>), + Bookmark, + PrefetchHooks Function()> { + $BookmarksTableManager(_$AppDatabase db, Bookmarks table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $BookmarksFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $BookmarksOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $BookmarksAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value resourceId = const Value.absent(), + Value name = const Value.absent(), + Value deleted = const Value.absent(), + Value rowid = const Value.absent(), + }) => + BookmarksCompanion( + resourceId: resourceId, + name: name, + deleted: deleted, + rowid: rowid, + ), + createCompanionCallback: ({ + required String resourceId, + required String name, + required bool deleted, + Value rowid = const Value.absent(), + }) => + BookmarksCompanion.insert( + resourceId: resourceId, + name: name, + deleted: deleted, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $BookmarksProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + Bookmarks, + Bookmark, + $BookmarksFilterComposer, + $BookmarksOrderingComposer, + $BookmarksAnnotationComposer, + $BookmarksCreateCompanionBuilder, + $BookmarksUpdateCompanionBuilder, + (Bookmark, BaseReferences<_$AppDatabase, Bookmarks, Bookmark>), + Bookmark, + PrefetchHooks Function()>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -1287,6 +1671,8 @@ class $AppDatabaseManager { $UsersTableManager get users => $UsersTableManager(_db, _db.users); $ResourcesTableManager get resources => $ResourcesTableManager(_db, _db.resources); + $BookmarksTableManager get bookmarks => + $BookmarksTableManager(_db, _db.bookmarks); } class ParentsResult { @@ -1301,3 +1687,12 @@ class ParentsResult { this.permissions, }); } + +class ListBookmarksResult { + final String resourceId; + final String name; + ListBookmarksResult({ + required this.resourceId, + required this.name, + }); +} diff --git a/client/lib/libphylum/phylum_account.dart b/client/lib/libphylum/phylum_account.dart index c59627bf..e5855c87 100644 --- a/client/lib/libphylum/phylum_account.dart +++ b/client/lib/libphylum/phylum_account.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; +import 'package:phylum/libphylum/repositories/bookmark_repository.dart'; import 'package:phylum/libphylum/repositories/resource_repository.dart'; import 'package:phylum/libphylum/repositories/user_repository.dart'; import 'package:phylum/util/logging.dart'; @@ -16,6 +17,7 @@ class PhylumAccount extends Account { late final db = AppDatabase(id: id); late final resourceRepository = ResourceRepository(account: this); late final userRepository = UserRepository(account: this); + late final bookmarkRepository = BookmarkRepository(account: this); final _dispatcher = SimulatedBadNetworkDispatcher.good(dropRate: 0); // final _dispatcher = HttpClientDispatcher(); diff --git a/client/lib/libphylum/repositories/bookmark_repository.dart b/client/lib/libphylum/repositories/bookmark_repository.dart new file mode 100644 index 00000000..a7f20b90 --- /dev/null +++ b/client/lib/libphylum/repositories/bookmark_repository.dart @@ -0,0 +1,49 @@ +import 'package:drift/drift.dart'; +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/libphylum/phylum_api_types.dart'; +import 'package:phylum/libphylum/requests/bookmarks_request.dart'; + +const keyLastBookmarkFetch = "lastBookmarksFetch"; + +class BookmarkRepository { + final PhylumAccount account; + List _bookmarks = const []; + List get bookmarks => _bookmarks; + + BookmarkRepository({required this.account}) { + account.db.bookmarks.select().get().then( + (bookmarks) => _bookmarks = List.unmodifiable(bookmarks), + ); + } + + Future refresh() async { + return account.apiClient.sendRequest( + BookmarksRequest(since: account.getPersisted(keyLastBookmarkFetch) as int?), + callback: (request, response) async { + if (response is PhylumApiSuccessResponse) { + await parseBookmarksResponse(response.data); + } + }, + ); + } + + Future parseBookmarksResponse(Map data) async { + final db = account.db; + final bookmarks = (data["bookmarks"] as List).cast().map((u) => parseBookmarkObject(u.cast())); + await db.batch((batch) { + batch.insertAllOnConflictUpdate(db.bookmarks, bookmarks); + }); + account.persist(keyLastBookmarkFetch, data["until"]); + _bookmarks = List.unmodifiable(bookmarks); + } + + Bookmark parseBookmarkObject(Map data) { + return Bookmark( + resourceId: data['resource_id'], + name: data['name'], + deleted: data['deleted'], + ); + } +} diff --git a/client/lib/libphylum/repositories/user_repository.dart b/client/lib/libphylum/repositories/user_repository.dart index 2ae59db7..20938aa9 100644 --- a/client/lib/libphylum/repositories/user_repository.dart +++ b/client/lib/libphylum/repositories/user_repository.dart @@ -14,7 +14,7 @@ class UserRepository { account.db.users.select().get().then((users) => _users = Map.unmodifiable(Map.fromIterable(users, key: (u) => u.username))); } - Future refreshUserList() async { + Future refresh() async { return account.apiClient.sendRequest(UsersListRequest(), callback: (request, response) async { if (response is PhylumApiSuccessResponse) { await processUserListResponse(response.data); diff --git a/client/lib/libphylum/requests/bookmarks_request.dart b/client/lib/libphylum/requests/bookmarks_request.dart new file mode 100644 index 00000000..79aed965 --- /dev/null +++ b/client/lib/libphylum/requests/bookmarks_request.dart @@ -0,0 +1,17 @@ +import 'package:http/http.dart'; +import 'package:offtheline/offtheline.dart'; + +class BookmarksRequest extends ApiRequest { + final int? since; + + BookmarksRequest({this.since}); + + @override + BaseRequest createRequest(ApiClient api) { + final uri = api.createUriBuilder('/api/v1/my/bookmarks/list'); + if (since != null) { + uri.queryParameters['since'] = since.toString(); + } + return Request('get', uri.build()); + } +}