From d54def39cac787cc01d33500ca4d1f88f62fa2f7 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 28 Jun 2025 17:50:08 -0500 Subject: [PATCH] drift logs --- mobile/lib/domain/services/log.service.dart | 6 +- .../infrastructure/entities/log.entity.dart | 20 + .../entities/log.entity.drift.dart | 589 ++++++++++++++++++ .../repositories/db.repository.dart | 2 + .../repositories/db.repository.drift.dart | 17 +- .../repositories/log.repository.dart | 91 ++- .../presentation/pages/dev/dev_logger.dart | 56 +- mobile/lib/utils/bootstrap.dart | 6 +- .../repositories/store_repository_test.dart | 51 +- .../test/infrastructure/repository.mock.dart | 2 +- .../modules/shared/sync_service_test.dart | 6 +- mobile/test/test_utils.dart | 2 - 12 files changed, 772 insertions(+), 76 deletions(-) create mode 100644 mobile/lib/infrastructure/entities/log.entity.drift.dart diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 67591da4d1..369106fcaa 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -14,7 +14,7 @@ import 'package:logging/logging.dart'; /// writes them to a persistent [ILogRepository], and manages log levels /// via [IStoreRepository] class LogService { - final IsarLogRepository _logRepository; + final LogRepository _logRepository; final IStoreRepository _storeRepository; final List _msgBuffer = []; @@ -37,7 +37,7 @@ class LogService { } static Future init({ - required IsarLogRepository logRepository, + required LogRepository logRepository, required IStoreRepository storeRepository, bool shouldBuffer = true, }) async { @@ -50,7 +50,7 @@ class LogService { } static Future create({ - required IsarLogRepository logRepository, + required LogRepository logRepository, required IStoreRepository storeRepository, bool shouldBuffer = true, }) async { diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index 6a38924e24..2827f1dbca 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -1,5 +1,7 @@ import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:isar/isar.dart'; +import 'package:drift/drift.dart'; part 'log.entity.g.dart'; @@ -45,3 +47,21 @@ class LoggerMessage { ); } } + +class LoggerMessageEntity extends Table with DriftDefaultsMixin { + const LoggerMessageEntity(); + + IntColumn get id => integer().autoIncrement()(); + + TextColumn get message => text()(); + + TextColumn get details => text().nullable()(); + + IntColumn get level => intEnum()(); + + DateTimeColumn get createdAt => dateTime()(); + + TextColumn get context1 => text().nullable()(); + + TextColumn get context2 => text().nullable()(); +} diff --git a/mobile/lib/infrastructure/entities/log.entity.drift.dart b/mobile/lib/infrastructure/entities/log.entity.drift.dart new file mode 100644 index 0000000000..a36065fbbc --- /dev/null +++ b/mobile/lib/infrastructure/entities/log.entity.drift.dart @@ -0,0 +1,589 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/log.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart' as i3; + +typedef $$LoggerMessageEntityTableCreateCompanionBuilder + = i1.LoggerMessageEntityCompanion Function({ + required int id, + required String message, + i0.Value details, + required i2.LogLevel level, + required DateTime createdAt, + i0.Value context1, + i0.Value context2, +}); +typedef $$LoggerMessageEntityTableUpdateCompanionBuilder + = i1.LoggerMessageEntityCompanion Function({ + i0.Value id, + i0.Value message, + i0.Value details, + i0.Value level, + i0.Value createdAt, + i0.Value context1, + i0.Value context2, +}); + +class $$LoggerMessageEntityTableFilterComposer + extends i0.Composer { + $$LoggerMessageEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get message => $composableBuilder( + column: $table.message, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get details => $composableBuilder( + column: $table.details, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get level => + $composableBuilder( + column: $table.level, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get context1 => $composableBuilder( + column: $table.context1, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get context2 => $composableBuilder( + column: $table.context2, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LoggerMessageEntityTableOrderingComposer + extends i0.Composer { + $$LoggerMessageEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get message => $composableBuilder( + column: $table.message, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get details => $composableBuilder( + column: $table.details, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get level => $composableBuilder( + column: $table.level, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get context1 => $composableBuilder( + column: $table.context1, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get context2 => $composableBuilder( + column: $table.context2, builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LoggerMessageEntityTableAnnotationComposer + extends i0.Composer { + $$LoggerMessageEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get message => + $composableBuilder(column: $table.message, builder: (column) => column); + + i0.GeneratedColumn get details => + $composableBuilder(column: $table.details, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get level => + $composableBuilder(column: $table.level, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get context1 => + $composableBuilder(column: $table.context1, builder: (column) => column); + + i0.GeneratedColumn get context2 => + $composableBuilder(column: $table.context2, builder: (column) => column); +} + +class $$LoggerMessageEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LoggerMessageEntityTable, + i1.LoggerMessageEntityData, + i1.$$LoggerMessageEntityTableFilterComposer, + i1.$$LoggerMessageEntityTableOrderingComposer, + i1.$$LoggerMessageEntityTableAnnotationComposer, + $$LoggerMessageEntityTableCreateCompanionBuilder, + $$LoggerMessageEntityTableUpdateCompanionBuilder, + ( + i1.LoggerMessageEntityData, + i0.BaseReferences + ), + i1.LoggerMessageEntityData, + i0.PrefetchHooks Function()> { + $$LoggerMessageEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LoggerMessageEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => i1 + .$$LoggerMessageEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$LoggerMessageEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LoggerMessageEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value message = const i0.Value.absent(), + i0.Value details = const i0.Value.absent(), + i0.Value level = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value context1 = const i0.Value.absent(), + i0.Value context2 = const i0.Value.absent(), + }) => + i1.LoggerMessageEntityCompanion( + id: id, + message: message, + details: details, + level: level, + createdAt: createdAt, + context1: context1, + context2: context2, + ), + createCompanionCallback: ({ + required int id, + required String message, + i0.Value details = const i0.Value.absent(), + required i2.LogLevel level, + required DateTime createdAt, + i0.Value context1 = const i0.Value.absent(), + i0.Value context2 = const i0.Value.absent(), + }) => + i1.LoggerMessageEntityCompanion.insert( + id: id, + message: message, + details: details, + level: level, + createdAt: createdAt, + context1: context1, + context2: context2, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LoggerMessageEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LoggerMessageEntityTable, + i1.LoggerMessageEntityData, + i1.$$LoggerMessageEntityTableFilterComposer, + i1.$$LoggerMessageEntityTableOrderingComposer, + i1.$$LoggerMessageEntityTableAnnotationComposer, + $$LoggerMessageEntityTableCreateCompanionBuilder, + $$LoggerMessageEntityTableUpdateCompanionBuilder, + ( + i1.LoggerMessageEntityData, + i0.BaseReferences + ), + i1.LoggerMessageEntityData, + i0.PrefetchHooks Function()>; + +class $LoggerMessageEntityTable extends i3.LoggerMessageEntity + with i0.TableInfo<$LoggerMessageEntityTable, i1.LoggerMessageEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LoggerMessageEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const i0.VerificationMeta _messageMeta = + const i0.VerificationMeta('message'); + @override + late final i0.GeneratedColumn message = i0.GeneratedColumn( + 'message', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _detailsMeta = + const i0.VerificationMeta('details'); + @override + late final i0.GeneratedColumn details = i0.GeneratedColumn( + 'details', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + late final i0.GeneratedColumnWithTypeConverter level = + i0.GeneratedColumn('level', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LoggerMessageEntityTable.$converterlevel); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: true); + static const i0.VerificationMeta _context1Meta = + const i0.VerificationMeta('context1'); + @override + late final i0.GeneratedColumn context1 = i0.GeneratedColumn( + 'context1', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _context2Meta = + const i0.VerificationMeta('context2'); + @override + late final i0.GeneratedColumn context2 = i0.GeneratedColumn( + 'context2', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => + [id, message, details, level, createdAt, context1, context2]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'logger_message_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('message')) { + context.handle(_messageMeta, + message.isAcceptableOrUnknown(data['message']!, _messageMeta)); + } else if (isInserting) { + context.missing(_messageMeta); + } + if (data.containsKey('details')) { + context.handle(_detailsMeta, + details.isAcceptableOrUnknown(data['details']!, _detailsMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('context1')) { + context.handle(_context1Meta, + context1.isAcceptableOrUnknown(data['context1']!, _context1Meta)); + } + if (data.containsKey('context2')) { + context.handle(_context2Meta, + context2.isAcceptableOrUnknown(data['context2']!, _context2Meta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LoggerMessageEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LoggerMessageEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!, + message: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}message'])!, + details: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}details']), + level: i1.$LoggerMessageEntityTable.$converterlevel.fromSql( + attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}level'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + context1: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}context1']), + context2: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}context2']), + ); + } + + @override + $LoggerMessageEntityTable createAlias(String alias) { + return $LoggerMessageEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converterlevel = + const i0.EnumIndexConverter(i2.LogLevel.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LoggerMessageEntityData extends i0.DataClass + implements i0.Insertable { + final int id; + final String message; + final String? details; + final i2.LogLevel level; + final DateTime createdAt; + final String? context1; + final String? context2; + const LoggerMessageEntityData( + {required this.id, + required this.message, + this.details, + required this.level, + required this.createdAt, + this.context1, + this.context2}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['message'] = i0.Variable(message); + if (!nullToAbsent || details != null) { + map['details'] = i0.Variable(details); + } + { + map['level'] = i0.Variable( + i1.$LoggerMessageEntityTable.$converterlevel.toSql(level)); + } + map['created_at'] = i0.Variable(createdAt); + if (!nullToAbsent || context1 != null) { + map['context1'] = i0.Variable(context1); + } + if (!nullToAbsent || context2 != null) { + map['context2'] = i0.Variable(context2); + } + return map; + } + + factory LoggerMessageEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LoggerMessageEntityData( + id: serializer.fromJson(json['id']), + message: serializer.fromJson(json['message']), + details: serializer.fromJson(json['details']), + level: i1.$LoggerMessageEntityTable.$converterlevel + .fromJson(serializer.fromJson(json['level'])), + createdAt: serializer.fromJson(json['createdAt']), + context1: serializer.fromJson(json['context1']), + context2: serializer.fromJson(json['context2']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'message': serializer.toJson(message), + 'details': serializer.toJson(details), + 'level': serializer.toJson( + i1.$LoggerMessageEntityTable.$converterlevel.toJson(level)), + 'createdAt': serializer.toJson(createdAt), + 'context1': serializer.toJson(context1), + 'context2': serializer.toJson(context2), + }; + } + + i1.LoggerMessageEntityData copyWith( + {int? id, + String? message, + i0.Value details = const i0.Value.absent(), + i2.LogLevel? level, + DateTime? createdAt, + i0.Value context1 = const i0.Value.absent(), + i0.Value context2 = const i0.Value.absent()}) => + i1.LoggerMessageEntityData( + id: id ?? this.id, + message: message ?? this.message, + details: details.present ? details.value : this.details, + level: level ?? this.level, + createdAt: createdAt ?? this.createdAt, + context1: context1.present ? context1.value : this.context1, + context2: context2.present ? context2.value : this.context2, + ); + LoggerMessageEntityData copyWithCompanion( + i1.LoggerMessageEntityCompanion data) { + return LoggerMessageEntityData( + id: data.id.present ? data.id.value : this.id, + message: data.message.present ? data.message.value : this.message, + details: data.details.present ? data.details.value : this.details, + level: data.level.present ? data.level.value : this.level, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + context1: data.context1.present ? data.context1.value : this.context1, + context2: data.context2.present ? data.context2.value : this.context2, + ); + } + + @override + String toString() { + return (StringBuffer('LoggerMessageEntityData(') + ..write('id: $id, ') + ..write('message: $message, ') + ..write('details: $details, ') + ..write('level: $level, ') + ..write('createdAt: $createdAt, ') + ..write('context1: $context1, ') + ..write('context2: $context2') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, message, details, level, createdAt, context1, context2); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LoggerMessageEntityData && + other.id == this.id && + other.message == this.message && + other.details == this.details && + other.level == this.level && + other.createdAt == this.createdAt && + other.context1 == this.context1 && + other.context2 == this.context2); +} + +class LoggerMessageEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value message; + final i0.Value details; + final i0.Value level; + final i0.Value createdAt; + final i0.Value context1; + final i0.Value context2; + const LoggerMessageEntityCompanion({ + this.id = const i0.Value.absent(), + this.message = const i0.Value.absent(), + this.details = const i0.Value.absent(), + this.level = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.context1 = const i0.Value.absent(), + this.context2 = const i0.Value.absent(), + }); + LoggerMessageEntityCompanion.insert({ + required int id, + required String message, + this.details = const i0.Value.absent(), + required i2.LogLevel level, + required DateTime createdAt, + this.context1 = const i0.Value.absent(), + this.context2 = const i0.Value.absent(), + }) : id = i0.Value(id), + message = i0.Value(message), + level = i0.Value(level), + createdAt = i0.Value(createdAt); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? message, + i0.Expression? details, + i0.Expression? level, + i0.Expression? createdAt, + i0.Expression? context1, + i0.Expression? context2, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (message != null) 'message': message, + if (details != null) 'details': details, + if (level != null) 'level': level, + if (createdAt != null) 'created_at': createdAt, + if (context1 != null) 'context1': context1, + if (context2 != null) 'context2': context2, + }); + } + + i1.LoggerMessageEntityCompanion copyWith( + {i0.Value? id, + i0.Value? message, + i0.Value? details, + i0.Value? level, + i0.Value? createdAt, + i0.Value? context1, + i0.Value? context2}) { + return i1.LoggerMessageEntityCompanion( + id: id ?? this.id, + message: message ?? this.message, + details: details ?? this.details, + level: level ?? this.level, + createdAt: createdAt ?? this.createdAt, + context1: context1 ?? this.context1, + context2: context2 ?? this.context2, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (message.present) { + map['message'] = i0.Variable(message.value); + } + if (details.present) { + map['details'] = i0.Variable(details.value); + } + if (level.present) { + map['level'] = i0.Variable( + i1.$LoggerMessageEntityTable.$converterlevel.toSql(level.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (context1.present) { + map['context1'] = i0.Variable(context1.value); + } + if (context2.present) { + map['context2'] = i0.Variable(context2.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LoggerMessageEntityCompanion(') + ..write('id: $id, ') + ..write('message: $message, ') + ..write('details: $details, ') + ..write('level: $level, ') + ..write('createdAt: $createdAt, ') + ..write('context1: $context1, ') + ..write('context2: $context2') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 983ce76cf8..a288955c64 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; @@ -48,6 +49,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { RemoteAlbumAssetEntity, RemoteAlbumUserEntity, StoreEntity, + LoggerMessageEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 1ac93d04a6..140b8e7fb5 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -25,9 +25,11 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d as i11; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' as i12; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart' as i13; -import 'package:drift/internal/modular.dart' as i14; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i14; +import 'package:drift/internal/modular.dart' as i15; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -54,8 +56,10 @@ abstract class $Drift extends i0.GeneratedDatabase { late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i11.$RemoteAlbumUserEntityTable(this); late final i12.$StoreEntityTable storeEntity = i12.$StoreEntityTable(this); - i13.MergedAssetDrift get mergedAssetDrift => i14.ReadDatabaseContainer(this) - .accessor(i13.MergedAssetDrift.new); + late final i13.$LoggerMessageEntityTable loggerMessageEntity = + i13.$LoggerMessageEntityTable(this); + i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this) + .accessor(i14.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -75,7 +79,8 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteAlbumEntity, remoteAlbumAssetEntity, remoteAlbumUserEntity, - storeEntity + storeEntity, + loggerMessageEntity ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -214,4 +219,6 @@ class $DriftManager { .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); i12.$$StoreEntityTableTableManager get storeEntity => i12.$$StoreEntityTableTableManager(_db, _db.storeEntity); + i13.$$LoggerMessageEntityTableTableManager get loggerMessageEntity => + i13.$$LoggerMessageEntityTableTableManager(_db, _db.loggerMessageEntity); } diff --git a/mobile/lib/infrastructure/repositories/log.repository.dart b/mobile/lib/infrastructure/repositories/log.repository.dart index 717900910a..5a09249075 100644 --- a/mobile/lib/infrastructure/repositories/log.repository.dart +++ b/mobile/lib/infrastructure/repositories/log.repository.dart @@ -1,46 +1,99 @@ +import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -class IsarLogRepository extends IsarDatabaseRepository { - final Isar _db; - const IsarLogRepository(super.db) : _db = db; +final driftLogRepositoryProvider = Provider( + (ref) => LogRepository(ref.watch(driftProvider)), +); + +class LogRepository { + final Drift _db; + + const LogRepository(this._db); Future deleteAll() async { - await transaction(() async => await _db.loggerMessages.clear()); + await _db.transaction(() async { + await _db.delete(_db.loggerMessageEntity).go(); + }); return true; } Future> getAll() async { - final logs = - await _db.loggerMessages.where().sortByCreatedAtDesc().findAll(); - return logs.map((l) => l.toDto()).toList(); + final query = _db.select(_db.loggerMessageEntity) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]); + final results = await query.get(); + return results + .map( + (row) => LogMessage( + message: row.message, + level: row.level, + createdAt: row.createdAt, + logger: row.context1, + error: row.details, + stack: row.context2, + ), + ) + .toList(); } Future insert(LogMessage log) async { - final logEntity = LoggerMessage.fromDto(log); - await transaction(() async { - await _db.loggerMessages.put(logEntity); + await _db.transaction(() async { + await _db.into(_db.loggerMessageEntity).insert( + LoggerMessageEntityCompanion.insert( + id: 0, // Will be auto-incremented by the database + message: log.message, + details: Value(log.error), + level: log.level, + createdAt: log.createdAt, + context1: Value(log.logger), + context2: Value(log.stack), + ), + ); }); return true; } Future insertAll(Iterable logs) async { - await transaction(() async { - final logEntities = - logs.map((log) => LoggerMessage.fromDto(log)).toList(); - await _db.loggerMessages.putAll(logEntities); + await _db.transaction(() async { + for (final log in logs) { + await _db.into(_db.loggerMessageEntity).insert( + LoggerMessageEntityCompanion.insert( + id: 0, // Will be auto-incremented by the database + message: log.message, + details: Value(log.error), + level: log.level, + createdAt: log.createdAt, + context1: Value(log.logger), + context2: Value(log.stack), + ), + ); + } }); return true; } Future truncate({int limit = 250}) async { - await transaction(() async { - final count = await _db.loggerMessages.count(); + await _db.transaction(() async { + final countQuery = _db.selectOnly(_db.loggerMessageEntity) + ..addColumns([_db.loggerMessageEntity.id.count()]); + final countResult = await countQuery.getSingle(); + final count = countResult.read(_db.loggerMessageEntity.id.count()) ?? 0; + if (count <= limit) return; + final toRemove = count - limit; - await _db.loggerMessages.where().limit(toRemove).deleteAll(); + final oldestIds = await (_db.select(_db.loggerMessageEntity) + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]) + ..limit(toRemove)) + .get(); + + final idsToDelete = oldestIds.map((row) => row.id).toList(); + await (_db.delete(_db.loggerMessageEntity) + ..where((tbl) => tbl.id.isIn(idsToDelete))) + .go(); }); } } diff --git a/mobile/lib/presentation/pages/dev/dev_logger.dart b/mobile/lib/presentation/pages/dev/dev_logger.dart index ab9849f87c..f0121ccb63 100644 --- a/mobile/lib/presentation/pages/dev/dev_logger.dart +++ b/mobile/lib/presentation/pages/dev/dev_logger.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:io'; +import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; -import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; -// ignore: import_rule_isar -import 'package:isar/isar.dart'; const kDevLoggerTag = 'DEV'; @@ -14,28 +13,38 @@ abstract final class DLog { const DLog(); static Stream> watchLog() { - final db = Isar.getInstance(); - if (db == null) { - return const Stream.empty(); - } + final db = Drift(); - return db.loggerMessages - .filter() - .context1EqualTo(kDevLoggerTag) - .sortByCreatedAtDesc() - .watch(fireImmediately: true) - .map((logs) => logs.map((log) => log.toDto()).toList()); + final query = db.select(db.loggerMessageEntity) + ..where((tbl) => tbl.context1.equals(kDevLoggerTag)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]); + + return query.watch().map( + (rows) => rows + .map( + (row) => LogMessage( + message: row.message, + level: row.level, + createdAt: row.createdAt, + logger: row.context1, + error: row.details, + stack: row.context2, + ), + ) + .toList(), + ); } static void clearLog() { - final db = Isar.getInstance(); - if (db == null) { - return; - } + final db = Drift(); - db.writeTxnSync(() { - db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); - }); + unawaited( + db.transaction(() async { + await (db.delete(db.loggerMessageEntity) + ..where((tbl) => tbl.context1.equals(kDevLoggerTag))) + .go(); + }), + ); } static void log(String message, [Object? error, StackTrace? stackTrace]) { @@ -49,10 +58,7 @@ abstract final class DLog { debugPrint('StackTrace: $stackTrace'); } - final isar = Isar.getInstance(); - if (isar == null) { - return; - } + final db = Drift(); final record = LogMessage( message: message, @@ -63,6 +69,6 @@ abstract final class DLog { stack: stackTrace?.toString(), ); - unawaited(IsarLogRepository(isar).insert(record)); + unawaited(LogRepository(db).insert(record)); } } diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 6e34f6b4ab..967a9f2f76 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -58,9 +58,10 @@ abstract final class Bootstrap { Isar db, { bool shouldBufferLogs = true, }) async { + final driftDb = Drift(); await StoreService.init(storeRepository: IsarStoreRepository(db)); await LogService.init( - logRepository: IsarLogRepository(db), + logRepository: LogRepository(driftDb), storeRepository: IsarStoreRepository(db), shouldBuffer: shouldBufferLogs, ); @@ -71,9 +72,8 @@ abstract final class Bootstrap { bool shouldBufferLogs = true, }) async { await StoreService.init(storeRepository: DriftStoreRepository(db)); - final isarDb = await initIsar(); await LogService.init( - logRepository: IsarLogRepository(isarDb), + logRepository: LogRepository(db), storeRepository: DriftStoreRepository(db), shouldBuffer: shouldBufferLogs, ); diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index ce13c1ecdd..269486a92c 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,12 +1,13 @@ +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/drift_store.repository.dart'; import '../../fixtures/user.stub.dart'; -import '../../test_utils.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); @@ -14,16 +15,28 @@ const _kTestVersion = 10; const _kTestColorfulInterface = false; final _kTestUser = UserStub.admin; -Future _addIntStoreValue(Isar db, StoreKey key, int? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: value, strValue: null)); +Future _addIntStoreValue(Drift db, StoreKey key, int? value) async { + await db.into(db.storeEntity).insert( + StoreEntityCompanion.insert( + id: key.id, + intValue: Value(value), + strValue: const Value(null), + ), + ); } -Future _addStrStoreValue(Isar db, StoreKey key, String? value) async { - await db.storeValues.put(StoreValue(key.id, intValue: null, strValue: value)); +Future _addStrStoreValue(Drift db, StoreKey key, String? value) async { + await db.into(db.storeEntity).insert( + StoreEntityCompanion.insert( + id: key.id, + intValue: const Value(null), + strValue: Value(value), + ), + ); } -Future _populateStore(Isar db) async { - await db.writeTxn(() async { +Future _populateStore(Drift db) async { + await db.transaction(() async { await _addIntStoreValue( db, StoreKey.colorfulInterface, @@ -40,12 +53,12 @@ Future _populateStore(Isar db) async { } void main() { - late Isar db; - late IsarStoreRepository sut; + late Drift db; + late DriftStoreRepository sut; setUp(() async { - db = await TestUtils.initIsar(); - sut = IsarStoreRepository(db); + db = Drift(NativeDatabase.memory()); + sut = DriftStoreRepository(db); }); group('Store Repository converters:', () { @@ -105,10 +118,16 @@ void main() { }); test('deleteAll()', () async { - final count = await db.storeValues.count(); + final countQuery = db.selectOnly(db.storeEntity) + ..addColumns([db.storeEntity.id.count()]); + final countResult = await countQuery.getSingle(); + final count = countResult.read(db.storeEntity.id.count()) ?? 0; expect(count, isNot(isZero)); await sut.deleteAll(); - expectLater(await db.storeValues.count(), isZero); + + final newCountResult = await countQuery.getSingle(); + final newCount = newCountResult.read(db.storeEntity.id.count()) ?? 0; + expect(newCount, isZero); }); }); diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 1fde303863..d65a6fa704 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -12,7 +12,7 @@ import 'package:mocktail/mocktail.dart'; class MockStoreRepository extends Mock implements IsarStoreRepository {} -class MockLogRepository extends Mock implements IsarLogRepository {} +class MockLogRepository extends Mock implements LogRepository {} class MockIsarUserRepository extends Mock implements IsarUserRepository {} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index a78f65af67..f84b00ddd0 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -1,3 +1,4 @@ +import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -8,6 +9,7 @@ import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; @@ -80,12 +82,12 @@ void main() { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); final db = await TestUtils.initIsar(); - + final driftDb = Drift(NativeDatabase.memory()); db.writeTxnSync(() => db.clearSync()); await StoreService.init(storeRepository: IsarStoreRepository(db)); await Store.put(StoreKey.currentUser, owner); await LogService.init( - logRepository: IsarLogRepository(db), + logRepository: LogRepository(driftDb), storeRepository: IsarStoreRepository(db), ); }); diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 596d3bcd1c..1cc9bdb65b 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -15,7 +15,6 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; @@ -41,7 +40,6 @@ abstract final class TestUtils { final db = await Isar.open( [ - StoreValueSchema, ExifInfoSchema, AssetSchema, AlbumSchema,