♻️ Use drift instead for floor

This commit is contained in:
2024-09-14 00:30:33 +08:00
parent db808650e3
commit b14e55355f
24 changed files with 886 additions and 692 deletions

View File

@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/providers/database/tables/messages.dart';
import 'package:solian/models/event.dart';
part 'database.g.dart';
@DriftDatabase(tables: [LocalMessageEventTable])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'solar_network_local_db');
}
}
class DatabaseProvider extends GetxController {
final database = AppDatabase();
}

View File

@ -0,0 +1,429 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class $LocalMessageEventTableTable extends LocalMessageEventTable
with TableInfo<$LocalMessageEventTableTable, LocalMessageEventTableData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalMessageEventTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _channelIdMeta =
const VerificationMeta('channelId');
@override
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override
late final GeneratedColumnWithTypeConverter<Event?, String> data =
GeneratedColumn<String>('data', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<Event?>($LocalMessageEventTableTable.$converterdata);
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: Constant(DateTime.now()));
@override
List<GeneratedColumn> get $columns => [id, channelId, data, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_message_event_table';
@override
VerificationContext validateIntegrity(
Insertable<LocalMessageEventTableData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('channel_id')) {
context.handle(_channelIdMeta,
channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
} else if (isInserting) {
context.missing(_channelIdMeta);
}
context.handle(_dataMeta, const VerificationResult.success());
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
LocalMessageEventTableData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return LocalMessageEventTableData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
data: $LocalMessageEventTableTable.$converterdata.fromSql(attachedDatabase
.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$LocalMessageEventTableTable createAlias(String alias) {
return $LocalMessageEventTableTable(attachedDatabase, alias);
}
static TypeConverter<Event?, String> $converterdata =
const MessageEventConverter();
}
class LocalMessageEventTableData extends DataClass
implements Insertable<LocalMessageEventTableData> {
final int id;
final int channelId;
final Event? data;
final DateTime createdAt;
const LocalMessageEventTableData(
{required this.id,
required this.channelId,
this.data,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
if (!nullToAbsent || data != null) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data));
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
LocalMessageEventTableCompanion toCompanion(bool nullToAbsent) {
return LocalMessageEventTableCompanion(
id: Value(id),
channelId: Value(channelId),
data: data == null && nullToAbsent ? const Value.absent() : Value(data),
createdAt: Value(createdAt),
);
}
factory LocalMessageEventTableData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return LocalMessageEventTableData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
data: serializer.fromJson<Event?>(json['data']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'data': serializer.toJson<Event?>(data),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
LocalMessageEventTableData copyWith(
{int? id,
int? channelId,
Value<Event?> data = const Value.absent(),
DateTime? createdAt}) =>
LocalMessageEventTableData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data.present ? data.value : this.data,
createdAt: createdAt ?? this.createdAt,
);
LocalMessageEventTableData copyWithCompanion(
LocalMessageEventTableCompanion data) {
return LocalMessageEventTableData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
data: data.data.present ? data.data.value : this.data,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, data, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is LocalMessageEventTableData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.data == this.data &&
other.createdAt == this.createdAt);
}
class LocalMessageEventTableCompanion
extends UpdateCompanion<LocalMessageEventTableData> {
final Value<int> id;
final Value<int> channelId;
final Value<Event?> data;
final Value<DateTime> createdAt;
const LocalMessageEventTableCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.data = const Value.absent(),
this.createdAt = const Value.absent(),
});
LocalMessageEventTableCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required Event? data,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
data = Value(data);
static Insertable<LocalMessageEventTableData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? data,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (data != null) 'data': data,
if (createdAt != null) 'created_at': createdAt,
});
}
LocalMessageEventTableCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<Event?>? data,
Value<DateTime>? createdAt}) {
return LocalMessageEventTableCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (data.present) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data.value));
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $LocalMessageEventTableTable localMessageEventTable =
$LocalMessageEventTableTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [localMessageEventTable];
}
typedef $$LocalMessageEventTableTableCreateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
required int channelId,
required Event? data,
Value<DateTime> createdAt,
});
typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
Value<int> channelId,
Value<Event?> data,
Value<DateTime> createdAt,
});
class $$LocalMessageEventTableTableFilterComposer
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableFilterComposer(super.$state);
ColumnFilters<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
$state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableOrderingComposer
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableOrderingComposer(super.$state);
ColumnOrderings<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get data => $state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()> {
$$LocalMessageEventTableTableTableManager(
_$AppDatabase db, $LocalMessageEventTableTable table)
: super(TableManagerState(
db: db,
table: table,
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
ComposerState(db, table)),
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
ComposerState(db, table)),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<int> channelId = const Value.absent(),
Value<Event?> data = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required int channelId,
required Event? data,
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion.insert(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LocalMessageEventTableTableProcessedTableManager
= ProcessedTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$LocalMessageEventTableTableTableManager get localMessageEventTable =>
$$LocalMessageEventTableTableTableManager(
_db, _db.localMessageEventTable);
}

View File

@ -0,0 +1,202 @@
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart';
class MessagesFetchingProvider extends GetxController {
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/whats-new?pivot=$pivot&take=$take',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
return (result, response.count);
}
Future<Event?> fetchRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> fetchRemoteEvents(
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await fetchRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
Future<LocalMessageEventTableData> receiveEvent(Event remote) async {
// Insert record
final database = Get.find<DatabaseProvider>().database;
final entry = await database
.into(database.localMessageEventTable)
.insertReturning(LocalMessageEventTableCompanion.insert(
id: Value(remote.id),
channelId: remote.channelId,
data: remote,
createdAt: Value(remote.createdAt),
));
// Handle side-effect like editing and deleting
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.getSingleOrNull();
if (target != null) {
target.data!.body = remote.body;
target.data!.updatedAt = remote.updatedAt;
await (database.update(database.localMessageEventTable)
..where((x) => x.id.equals(target.id)))
.write(
LocalMessageEventTableCompanion(data: Value(target.data)),
);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await (database.delete(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.go();
}
}
return entry;
}
Future<LocalMessageEventTableData?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final database = Get.find<DatabaseProvider>().database;
final localRecord = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(id)))
.getSingleOrNull();
if (localRecord != null) return localRecord;
final remoteRecord = await fetchRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
/// Pull the remote events to local database
Future<(List<Event>, int)?> pullRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final database = Get.find<DatabaseProvider>().database;
final lastOne = await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
final data = await fetchRemoteEvents(
channel,
scope,
remainDepth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await database.batch((batch) {
batch.insertAll(
database.localMessageEventTable,
data.$1.map((x) => LocalMessageEventTableCompanion(
id: Value(x.id),
channelId: Value(x.channelId),
data: Value(x),
createdAt: Value(x.createdAt),
)),
);
});
}
return data;
}
Future<List<LocalMessageEventTableData>> listEvents(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.get();
}
Future<LocalMessageEventTableData?> getLastInChannel(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
}
}

View File

@ -0,0 +1,13 @@
import 'dart:convert';
import 'package:drift/drift.dart';
class JsonConverter extends TypeConverter<Object?, String> {
const JsonConverter();
@override
Object? fromSql(String fromDb) => jsonDecode(fromDb);
@override
String toSql(Object? value) => jsonEncode(value);
}

View File

@ -0,0 +1,22 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:solian/models/event.dart';
class LocalMessageEventTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
TextColumn get data => text().map(const MessageEventConverter())();
DateTimeColumn get createdAt =>
dateTime().withDefault(Constant(DateTime.now()))();
}
class MessageEventConverter extends TypeConverter<Event?, String> {
const MessageEventConverter();
@override
Event? fromSql(String fromDb) => Event.fromJson(jsonDecode(fromDb));
@override
String toSql(Event? value) => jsonEncode(value?.toJson());
}