refactor: Update Note and Category models to use 'id' instead of 'uuid', and adjust related database operations
- Changed 'uuid' to 'id' in Note and Category models for consistency. - Updated database operations in NoteRepository to reflect the new 'id' field. - Modified sync models to accommodate changes in Note and Category structures. - Adjusted the handling of notes and categories during synchronization. - Refactored the note editor and home screen to use the new 'id' field. - Ensured that the 'isDirty' flag is properly set and utilized across models.
This commit is contained in:
+144
-82
@@ -35,9 +35,9 @@ class NoteRepository {
|
||||
}
|
||||
|
||||
Future<Note> createNote(Note note) async {
|
||||
final int id = await _database.insertNoteAtTop(
|
||||
await _database.insertNoteAtTop(
|
||||
NotesCompanion.insert(
|
||||
uuid: note.uuid,
|
||||
id: note.id,
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
createdAt: note.createdAt,
|
||||
@@ -45,56 +45,77 @@ class NoteRepository {
|
||||
sortIndex: 0,
|
||||
serverVersion: const Value(0),
|
||||
isDeleted: const Value(false),
|
||||
categoryId: const Value(null),
|
||||
categoryId: Value(note.categoryId),
|
||||
isDirty: const Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
return note.copyWith(id: id, index: 0);
|
||||
return note.copyWith(position: 0, isDirty: true);
|
||||
}
|
||||
|
||||
Future<Note> updateNote(Note note) async {
|
||||
final int noteId =
|
||||
note.id ??
|
||||
(throw ArgumentError('Note id is required to update a note.'));
|
||||
final DbNote? existingNote = await (_database.select(
|
||||
_database.notes,
|
||||
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
|
||||
|
||||
final DbNote row =
|
||||
existingNote ??
|
||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||
|
||||
await _database.updateNoteRow(
|
||||
DbNote(
|
||||
id: noteId,
|
||||
uuid: note.uuid,
|
||||
id: row.id,
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
createdAt: note.createdAt,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: note.updatedAt,
|
||||
sortIndex: note.index,
|
||||
sortIndex: row.sortIndex,
|
||||
serverVersion: note.serverVersion,
|
||||
isDeleted: false,
|
||||
categoryId: note.categoryId,
|
||||
isDirty: true,
|
||||
),
|
||||
);
|
||||
|
||||
return note.copyWith(isDeleted: false, isPermanentlyDeleted: false);
|
||||
return note.copyWith(
|
||||
isDeleted: false,
|
||||
isPermanentlyDeleted: false,
|
||||
isDirty: true,
|
||||
position: row.sortIndex.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteNote(Note note) async {
|
||||
final int noteId =
|
||||
note.id ??
|
||||
(throw ArgumentError('Note id is required to delete a note.'));
|
||||
final DbNote? existingNote = await (_database.select(
|
||||
_database.notes,
|
||||
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
|
||||
|
||||
if (note.isDeleted || note.isPermanentlyDeleted) {
|
||||
await _database.permanentlyDeleteNote(noteId);
|
||||
final DbNote row =
|
||||
existingNote ??
|
||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||
|
||||
if (row.isDeleted || note.isDeleted || note.isPermanentlyDeleted) {
|
||||
await _database.permanentlyDeleteNote(row.id);
|
||||
} else {
|
||||
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
|
||||
await _database.deleteNoteAndShift(
|
||||
id: row.id,
|
||||
removedIndex: row.sortIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> moveNote(Note note, int newIndex) async {
|
||||
final int noteId =
|
||||
note.id ??
|
||||
(throw ArgumentError('Note id is required to reorder a note.'));
|
||||
final DbNote? existingNote = await (_database.select(
|
||||
_database.notes,
|
||||
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
|
||||
|
||||
final DbNote row =
|
||||
existingNote ??
|
||||
(throw ArgumentError('Note not found for id ${note.id}.'));
|
||||
|
||||
await _database.moveNote(
|
||||
id: noteId,
|
||||
oldIndex: note.index,
|
||||
id: row.id,
|
||||
oldIndex: row.sortIndex,
|
||||
newIndex: newIndex,
|
||||
);
|
||||
}
|
||||
@@ -120,26 +141,17 @@ class NoteRepository {
|
||||
? DateTime.utc(1970, 1, 1)
|
||||
: lastSync;
|
||||
|
||||
// Collect pending local changes.
|
||||
// If we already synced at least once, use updatedAt to avoid re-sending
|
||||
// old notes that were already uploaded.
|
||||
final List<DbNote> unsyncedNotes;
|
||||
final List<DbCategory> unsyncedCategories;
|
||||
|
||||
if (forceFull || lastSync == null) {
|
||||
unsyncedNotes = await _database.getUnsyncedNotes();
|
||||
unsyncedCategories = await _database.getUnsyncedCategories();
|
||||
} else {
|
||||
unsyncedNotes = await _database.getNotesChangedSince(lastSync);
|
||||
unsyncedCategories = await _database.getCategoriesChangedSince(
|
||||
lastSync,
|
||||
);
|
||||
}
|
||||
// Collect pending local changes. Dirty flags are the source of truth for
|
||||
// outbound sync; `lastSyncAt` is only used for asking the server what it
|
||||
// changed since our previous successful sync.
|
||||
final List<DbNote> unsyncedNotes = await _database.getUnsyncedNotes();
|
||||
final List<DbCategory> unsyncedCategories = await _database
|
||||
.getUnsyncedCategories();
|
||||
|
||||
final int totalNotesToEncrypt = unsyncedNotes.length;
|
||||
|
||||
// Build sync request (note: we send encrypted data, but locally we have plaintext)
|
||||
// Encrypt notes that still contain content before sending.
|
||||
// Build sync request: local data is plaintext, encryption happens only
|
||||
// for the outbound payload.
|
||||
if (totalNotesToEncrypt == 0) {
|
||||
onProgress?.call(
|
||||
SyncStatus.encrypting,
|
||||
@@ -164,9 +176,8 @@ class NoteRepository {
|
||||
},
|
||||
);
|
||||
|
||||
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
|
||||
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
|
||||
.toList();
|
||||
final List<SyncCategoryPayload> categoriesPayload =
|
||||
await _encryptCategories(unsyncedCategories, masterKey: _masterKey);
|
||||
|
||||
final SyncRequest syncRequest = SyncRequest(
|
||||
lastSyncAt: lastSyncForRequest,
|
||||
@@ -184,7 +195,27 @@ class NoteRepository {
|
||||
final Map<String, dynamic> syncResult = await _authApi.sync(syncRequest);
|
||||
|
||||
if (syncResult['error'] == true) {
|
||||
return {'error': true, 'message': syncResult['body']};
|
||||
final List<String> details = [];
|
||||
final Object? message = syncResult['message'];
|
||||
final Object? exception = syncResult['exception'];
|
||||
final Object? stackTrace = syncResult['stackTrace'];
|
||||
final Object? body = syncResult['body'];
|
||||
|
||||
if (message != null) details.add(message.toString());
|
||||
if (exception != null && exception.toString() != details.firstOrNull) {
|
||||
details.add('Exception: ${exception.toString()}');
|
||||
}
|
||||
if (body != null) {
|
||||
details.add('Body: ${body.toString()}');
|
||||
}
|
||||
if (stackTrace != null) {
|
||||
details.add('StackTrace: ${stackTrace.toString()}');
|
||||
}
|
||||
|
||||
return {
|
||||
'error': true,
|
||||
'message': details.join('\n\n'),
|
||||
};
|
||||
}
|
||||
|
||||
final SyncResponse response = syncResult['data'] as SyncResponse;
|
||||
@@ -217,8 +248,11 @@ class NoteRepository {
|
||||
'notesCount': response.changes.notes.length,
|
||||
'categoriesCount': response.changes.categories.length,
|
||||
};
|
||||
} catch (e) {
|
||||
return {'error': true, 'message': e.toString()};
|
||||
} catch (e, st) {
|
||||
return {
|
||||
'error': true,
|
||||
'message': '$e\n\nStackTrace: $st',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,13 +263,23 @@ class NoteRepository {
|
||||
// Apply categories from server
|
||||
for (final SyncCategoryResponse catResponse
|
||||
in response.changes.categories) {
|
||||
final String categoryName =
|
||||
catResponse.isDeleted || catResponse.encryptedName.isEmpty
|
||||
? ''
|
||||
: await NoteEncryption.decryptNote(
|
||||
catResponse.encryptedName,
|
||||
_masterKey,
|
||||
);
|
||||
|
||||
|
||||
await _database.upsertCategory(
|
||||
CategoriesCompanion(
|
||||
uuid: Value(catResponse.id),
|
||||
encryptedName: Value(catResponse.encryptedName),
|
||||
id: Value(catResponse.id),
|
||||
encryptedName: Value(categoryName),
|
||||
serverVersion: Value(catResponse.serverVersion),
|
||||
isDeleted: Value(catResponse.isDeleted),
|
||||
updatedAt: Value(catResponse.updatedAt),
|
||||
isDirty: const Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -254,14 +298,14 @@ class NoteRepository {
|
||||
|
||||
for (var index = 0; index < decryptedNotes.length; index += 1) {
|
||||
final Map<String, Object?> decryptedNote = decryptedNotes[index];
|
||||
final String noteId = decryptedNote['id']! as String;
|
||||
final SyncNoteResponse noteResponse = response.changes.notes[index];
|
||||
final String noteId = (decryptedNote['id'] as String?) ?? noteResponse.id;
|
||||
final existingNote = await (_database.select(
|
||||
_database.notes,
|
||||
)..where((n) => n.uuid.equals(noteId))).getSingleOrNull();
|
||||
)..where((n) => n.id.equals(noteId))).getSingleOrNull();
|
||||
|
||||
final String decryptedTitle = decryptedNote['title']! as String;
|
||||
final String decryptedBody = decryptedNote['body']! as String;
|
||||
final SyncNoteResponse noteResponse = response.changes.notes[index];
|
||||
final String decryptedTitle = (decryptedNote['title'] as String?) ?? '';
|
||||
final String decryptedBody = (decryptedNote['body'] as String?) ?? '';
|
||||
final bool isPermanentlyDeleted = noteResponse.isPermanentlyDeleted;
|
||||
|
||||
if (existingNote != null) {
|
||||
@@ -269,15 +313,15 @@ class NoteRepository {
|
||||
await _database.updateNoteRow(
|
||||
DbNote(
|
||||
id: existingNote.id,
|
||||
uuid: noteId,
|
||||
title: isPermanentlyDeleted ? '' : decryptedTitle,
|
||||
body: isPermanentlyDeleted ? '' : decryptedBody,
|
||||
createdAt: existingNote.createdAt,
|
||||
updatedAt: noteResponse.updatedAt,
|
||||
sortIndex: noteResponse.position,
|
||||
sortIndex: noteResponse.position.round(),
|
||||
serverVersion: noteResponse.serverVersion,
|
||||
isDeleted: noteResponse.isDeleted,
|
||||
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
|
||||
isDirty: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -286,15 +330,18 @@ class NoteRepository {
|
||||
.into(_database.notes)
|
||||
.insert(
|
||||
NotesCompanion(
|
||||
uuid: Value(noteResponse.id),
|
||||
id: Value(noteResponse.id),
|
||||
title: Value(isPermanentlyDeleted ? '' : decryptedTitle),
|
||||
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
|
||||
createdAt: Value(noteResponse.updatedAt),
|
||||
updatedAt: Value(noteResponse.updatedAt),
|
||||
sortIndex: Value(noteResponse.position),
|
||||
sortIndex: Value(noteResponse.position.round()),
|
||||
serverVersion: Value(noteResponse.serverVersion),
|
||||
isDeleted: Value(noteResponse.isDeleted),
|
||||
categoryId: Value(isPermanentlyDeleted ? null : noteResponse.categoryId),
|
||||
categoryId: Value(
|
||||
isPermanentlyDeleted ? null : noteResponse.categoryId,
|
||||
),
|
||||
isDirty: const Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -314,26 +361,16 @@ class NoteRepository {
|
||||
Note _fromDbNote(DbNote row) {
|
||||
return Note(
|
||||
id: row.id,
|
||||
uuid: row.uuid,
|
||||
title: row.title,
|
||||
body: row.body,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
index: row.sortIndex,
|
||||
position: row.sortIndex.toDouble(),
|
||||
serverVersion: row.serverVersion,
|
||||
isDeleted: row.isDeleted,
|
||||
isPermanentlyDeleted: _isPermanentlyDeleted(row),
|
||||
categoryId: row.categoryId,
|
||||
);
|
||||
}
|
||||
|
||||
Category _fromDbCategory(DbCategory row) {
|
||||
return Category(
|
||||
uuid: row.uuid,
|
||||
encryptedName: row.encryptedName,
|
||||
serverVersion: row.serverVersion,
|
||||
isDeleted: row.isDeleted,
|
||||
updatedAt: row.updatedAt,
|
||||
isDirty: row.isDirty,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -395,6 +432,35 @@ Future<List<SyncNotePayload>> _encryptNotesInParallel(
|
||||
return orderedPayloads.cast<SyncNotePayload>();
|
||||
}
|
||||
|
||||
Future<List<SyncCategoryPayload>> _encryptCategories(
|
||||
List<DbCategory> categories, {
|
||||
required String masterKey,
|
||||
}) async {
|
||||
final List<SyncCategoryPayload> payloads = [];
|
||||
|
||||
for (final DbCategory row in categories) {
|
||||
final String encryptedName = row.encryptedName.isEmpty
|
||||
? ''
|
||||
: await NoteEncryption.encryptNote(row.encryptedName, masterKey);
|
||||
|
||||
payloads.add(
|
||||
SyncCategoryPayload.fromCategory(
|
||||
Category(
|
||||
id: row.id,
|
||||
name: row.encryptedName,
|
||||
serverVersion: row.serverVersion,
|
||||
isDeleted: row.isDeleted,
|
||||
updatedAt: row.updatedAt,
|
||||
isDirty: row.isDirty,
|
||||
),
|
||||
encryptedName: encryptedName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return payloads;
|
||||
}
|
||||
|
||||
List<Future<List<Map<String, Object?>>>> _decryptNoteBatches(
|
||||
List<SyncNoteResponse> notes, {
|
||||
required String masterKey,
|
||||
@@ -467,7 +533,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
|
||||
|
||||
return <String, Object?>{
|
||||
'index': index,
|
||||
'uuid': row.uuid,
|
||||
'id': row.id,
|
||||
'title': row.title,
|
||||
'body': row.body,
|
||||
'createdAt': row.createdAt.toIso8601String(),
|
||||
@@ -487,7 +553,7 @@ SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
|
||||
encryptedTitle: row['encryptedTitle']! as String,
|
||||
encryptedBody: row['encryptedBody']! as String,
|
||||
serverVersion: row['serverVersion']! as int,
|
||||
position: row['position']! as int,
|
||||
position: (row['position']! as num).toDouble(),
|
||||
isDeleted: row['isDeleted']! as bool,
|
||||
isPermanentlyDeleted: row['isPermanentlyDeleted']! as bool,
|
||||
updatedAt: DateTime.parse(row['updatedAt']! as String),
|
||||
@@ -513,19 +579,13 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
|
||||
encryptedTitle = '';
|
||||
encryptedBody = '';
|
||||
} else {
|
||||
encryptedTitle = await NoteEncryption.encryptNote(
|
||||
title,
|
||||
masterKey,
|
||||
);
|
||||
encryptedBody = await NoteEncryption.encryptNote(
|
||||
body,
|
||||
masterKey,
|
||||
);
|
||||
encryptedTitle = await NoteEncryption.encryptNote(title, masterKey);
|
||||
encryptedBody = await NoteEncryption.encryptNote(body, masterKey);
|
||||
}
|
||||
|
||||
encryptedNotes.add(<String, Object?>{
|
||||
'index': note['index'] as int,
|
||||
'id': note['uuid'] as String,
|
||||
'id': note['id'] as String,
|
||||
'categoryId': note['categoryId'] as String?,
|
||||
'encryptedTitle': encryptedTitle,
|
||||
'encryptedBody': encryptedBody,
|
||||
@@ -570,9 +630,11 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
|
||||
decryptedBody = '';
|
||||
}
|
||||
|
||||
final String noteId = note['id']! as String;
|
||||
|
||||
decryptedNotes.add(<String, Object?>{
|
||||
'index': note['index'] as int,
|
||||
'id': note['id'] as String,
|
||||
'id': noteId,
|
||||
'title': decryptedTitle,
|
||||
'body': decryptedBody,
|
||||
'isPermanentlyDeleted': isPermanentlyDeleted,
|
||||
|
||||
Reference in New Issue
Block a user