adding packages
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import '../sync_repository.dart';
|
||||
import 'schema.dart'; // Will export schema.g.dart artifacts via internal generation users
|
||||
|
||||
/// A Drift (SQLite) implementation of SyncRepository.
|
||||
/// Requires a [SyncDatabase] interface which exposes the underlying tables.
|
||||
class DriftSyncRepository<T> implements SyncRepository<T> {
|
||||
final SyncDatabase dbWrapper;
|
||||
final String collectionName;
|
||||
final Map<String, dynamic> Function(T) toJson;
|
||||
final T Function(Map<String, dynamic>) fromJson;
|
||||
|
||||
DriftSyncRepository({
|
||||
required this.dbWrapper,
|
||||
required this.collectionName,
|
||||
required this.toJson,
|
||||
required this.fromJson,
|
||||
});
|
||||
|
||||
GeneratedDatabase get db => dbWrapper.db;
|
||||
|
||||
@override
|
||||
Future<List<SyncRecord<T>>> getAll() async {
|
||||
// Explicitly type the select to ensure 't' is 'SyncEntries'
|
||||
final query = db.select<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
|
||||
..where((t) => t.collection.equals(collectionName));
|
||||
|
||||
final rows = await query.get();
|
||||
|
||||
return rows.map((row) {
|
||||
return SyncRecord<T>(
|
||||
id: row.id,
|
||||
data: fromJson(jsonDecode(row.data)),
|
||||
baseData:
|
||||
row.baseData != null ? fromJson(jsonDecode(row.baseData!)) : null,
|
||||
serverUpdatedAt: row.serverUpdatedAt,
|
||||
isDirty: row.isDirty,
|
||||
isDeleted: row.isDeleted,
|
||||
deletedAt: row.deletedAt,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncRecord<T>?> get(String id) async {
|
||||
final query = db.select<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
|
||||
..where((t) => t.collection.equals(collectionName) & t.id.equals(id));
|
||||
|
||||
final row = await query.getSingleOrNull();
|
||||
if (row == null) return null;
|
||||
|
||||
return SyncRecord<T>(
|
||||
id: row.id,
|
||||
data: fromJson(jsonDecode(row.data)),
|
||||
baseData:
|
||||
row.baseData != null ? fromJson(jsonDecode(row.baseData!)) : null,
|
||||
serverUpdatedAt: row.serverUpdatedAt,
|
||||
isDirty: row.isDirty,
|
||||
isDeleted: row.isDeleted,
|
||||
deletedAt: row.deletedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(SyncRecord<T> record) async {
|
||||
// Note: SyncEntriesCompanion is generated in schema.g.dart
|
||||
await db.into(dbWrapper.syncEntries).insertOnConflictUpdate(
|
||||
SyncEntriesCompanion(
|
||||
collection: Value(collectionName),
|
||||
id: Value(record.id),
|
||||
data: Value(jsonEncode(toJson(record.data))),
|
||||
baseData: Value(
|
||||
record.baseData != null
|
||||
? jsonEncode(toJson(record.baseData as T))
|
||||
: null,
|
||||
),
|
||||
serverUpdatedAt: Value(record.serverUpdatedAt),
|
||||
isDirty: Value(record.isDirty),
|
||||
isDeleted: Value(record.isDeleted),
|
||||
deletedAt: Value(record.deletedAt),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
await (db.delete<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
|
||||
..where((t) => t.collection.equals(collectionName) & t.id.equals(id)))
|
||||
.go();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime> getLastSyncTime() async {
|
||||
final query = db.select<SyncMeta, SyncMetaData>(dbWrapper.syncMeta)
|
||||
..where((t) => t.collection.equals(collectionName));
|
||||
|
||||
final row = await query.getSingleOrNull();
|
||||
return row?.lastSync ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setLastSyncTime(DateTime time) async {
|
||||
await db.into(dbWrapper.syncMeta).insertOnConflictUpdate(
|
||||
SyncMetaCompanion(
|
||||
collection: Value(collectionName),
|
||||
lastSync: Value(time),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'schema.g.dart';
|
||||
|
||||
/// Stores the actual data records + sync metadata
|
||||
/// We use a composite primary key of (collection, id) to store multiple collections in one table
|
||||
class SyncEntries extends Table {
|
||||
TextColumn get collection => text()();
|
||||
TextColumn get id => text()();
|
||||
|
||||
/// serialized JSON of the current local data
|
||||
TextColumn get data => text()();
|
||||
|
||||
/// serialized JSON of the base data (snapshot)
|
||||
TextColumn get baseData => text().nullable()();
|
||||
|
||||
DateTimeColumn get serverUpdatedAt => dateTime().nullable()();
|
||||
BoolColumn get isDirty => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {collection, id};
|
||||
}
|
||||
|
||||
/// Stores global sync state per collection (e.g. last sync time)
|
||||
class SyncMeta extends Table {
|
||||
TextColumn get collection => text()();
|
||||
DateTimeColumn get lastSync => dateTime().withDefault(
|
||||
Constant(DateTime.fromMillisecondsSinceEpoch(0)),
|
||||
)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {collection};
|
||||
}
|
||||
|
||||
/// Dummy database class to trigger generation of Companions and Data classes
|
||||
/// which are used by DriftSyncRepository.
|
||||
@DriftDatabase(tables: [SyncEntries, SyncMeta])
|
||||
class PackageDatabase extends _$PackageDatabase implements SyncDatabase {
|
||||
PackageDatabase(super.e);
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
GeneratedDatabase get db => this;
|
||||
}
|
||||
|
||||
/// Interface that your AppDatabase must implement to be used with DriftSyncRepository.
|
||||
/// This allows the repository to be agnostic of the specific generated database class.
|
||||
abstract class SyncDatabase {
|
||||
GeneratedDatabase get db;
|
||||
TableInfo<SyncEntries, SyncEntry> get syncEntries;
|
||||
TableInfo<SyncMeta, SyncMetaData> get syncMeta;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
import 'sync_repository.dart';
|
||||
|
||||
/// Default In-Memory Implementation (Good for testing/prototyping)
|
||||
class InMemoryRepository<T> implements SyncRepository<T> {
|
||||
final Map<String, SyncRecord<T>> _db = {};
|
||||
DateTime _lastSync = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@override
|
||||
Future<List<SyncRecord<T>>> getAll() async => _db.values.toList();
|
||||
@override
|
||||
Future<SyncRecord<T>?> get(String id) async => _db[id];
|
||||
@override
|
||||
Future<void> save(SyncRecord<T> record) async => _db[record.id] = record;
|
||||
@override
|
||||
Future<void> delete(String id) async => _db.remove(id);
|
||||
@override
|
||||
Future<DateTime> getLastSyncTime() async => _lastSync;
|
||||
@override
|
||||
Future<void> setLastSyncTime(DateTime time) async => _lastSync = time;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
|
||||
/// Wraps your data with sync metadata (Dirty flags, tombstones, versions)
|
||||
class SyncRecord<T> {
|
||||
final String id;
|
||||
final T data;
|
||||
|
||||
/// Server's timestamp. Null if created locally and never synced.
|
||||
final DateTime? serverUpdatedAt;
|
||||
|
||||
/// Snapshot of data before local edits (Critical for 3-way merge).
|
||||
final T? baseData;
|
||||
|
||||
/// True if local data differs from server data.
|
||||
final bool isDirty;
|
||||
|
||||
/// True if marked for deletion (Soft Delete).
|
||||
final bool isDeleted;
|
||||
|
||||
/// When the soft delete happened (For Retention Policy).
|
||||
final DateTime? deletedAt;
|
||||
|
||||
SyncRecord({
|
||||
required this.id,
|
||||
required this.data,
|
||||
this.serverUpdatedAt,
|
||||
this.baseData,
|
||||
this.isDirty = false,
|
||||
this.isDeleted = false,
|
||||
this.deletedAt,
|
||||
});
|
||||
|
||||
SyncRecord<T> copyWith({
|
||||
T? data,
|
||||
DateTime? serverUpdatedAt,
|
||||
T? baseData,
|
||||
bool? isDirty,
|
||||
bool? isDeleted,
|
||||
DateTime? deletedAt,
|
||||
}) {
|
||||
return SyncRecord<T>(
|
||||
id: id,
|
||||
data: data ?? this.data,
|
||||
serverUpdatedAt: serverUpdatedAt ?? this.serverUpdatedAt,
|
||||
baseData: baseData ?? this.baseData,
|
||||
isDirty: isDirty ?? this.isDirty,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract Storage Interface
|
||||
abstract class SyncRepository<T> {
|
||||
Future<List<SyncRecord<T>>> getAll();
|
||||
Future<SyncRecord<T>?> get(String id);
|
||||
Future<void> save(SyncRecord<T> record);
|
||||
Future<void> delete(String id); // Hard Delete
|
||||
Future<DateTime> getLastSyncTime();
|
||||
Future<void> setLastSyncTime(DateTime time);
|
||||
}
|
||||
Reference in New Issue
Block a user