Files
notas/lib/data/note_repository.dart
T

281 lines
8.4 KiB
Dart

import 'package:drift/drift.dart';
import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart';
import 'package:notas/data/sync_models.dart';
import 'package:notas/models/note.dart';
import 'package:notas/models/category.dart';
import 'package:notas/data/note_encryption.dart';
class NoteRepository {
NoteRepository({
required AppDatabase database,
required AuthApi authApi,
required String masterKey,
}) : _database = database,
_authApi = authApi,
_masterKey = masterKey;
final AppDatabase _database;
final AuthApi _authApi;
final String _masterKey;
Future<List<Note>> loadNotes() async {
return _loadNotesFromDatabase();
}
Future<Note> createNote(Note note) async {
final int id = await _database.insertNoteAtTop(
NotesCompanion.insert(
uuid: note.uuid,
title: note.title,
body: note.body,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
sortIndex: 0,
serverVersion: const Value(0),
isDeleted: const Value(false),
categoryId: const Value(null),
),
);
return note.copyWith(id: id, index: 0);
}
Future<Note> updateNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to update a note.'));
await _database.updateNoteRow(
DbNote(
id: noteId,
uuid: note.uuid,
title: note.title,
body: note.body,
createdAt: note.createdAt,
updatedAt: note.updatedAt,
sortIndex: note.index,
serverVersion: note.serverVersion,
isDeleted: note.isDeleted,
categoryId: note.categoryId,
),
);
return note;
}
Future<void> deleteNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to delete a note.'));
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
}
Future<void> moveNote(Note note, int newIndex) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to reorder a note.'));
await _database.moveNote(
id: noteId,
oldIndex: note.index,
newIndex: newIndex,
);
}
// ========== Sync logic ==========
/// Sincroniza notas con el servidor.
/// Requiere que el usuario esté autenticado (token válido).
Future<Map<String, dynamic>> performSync({bool forceFull = false}) async {
try {
// Get last sync timestamp
final DateTime? lastSync = await _authApi.getLastSyncAt();
final DateTime? lastSyncForRequest = forceFull
? 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,
);
}
// Build sync request (note: we send encrypted data, but locally we have plaintext)
// Encrypt all notes before sending
final List<SyncNotePayload> encryptedNotesPayload = [];
for (final dbNote in unsyncedNotes) {
final note = _fromDbNote(dbNote);
final encryptedTitle = await NoteEncryption.encryptNote(
note.title,
_masterKey,
);
final encryptedBody = await NoteEncryption.encryptNote(
note.body,
_masterKey,
);
encryptedNotesPayload.add(
SyncNotePayload.fromNote(
note,
encryptedTitle: encryptedTitle,
encryptedBody: encryptedBody,
),
);
}
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
.toList();
final SyncRequest syncRequest = SyncRequest(
lastSyncAt: lastSyncForRequest,
changes: SyncChanges(
categories: categoriesPayload,
notes: encryptedNotesPayload,
),
);
// Call sync API
final Map<String, dynamic> syncResult = await _authApi.sync(syncRequest);
if (syncResult['error'] == true) {
return {'error': true, 'message': syncResult['body']};
}
final SyncResponse response = syncResult['data'] as SyncResponse;
// Apply server changes to local database
await _applySyncResponse(response);
// Update lastSyncAt
await _authApi.setLastSyncAt(response.serverTimestamp);
return {
'error': false,
'synced': response.synced,
'notesCount': response.changes.notes.length,
'categoriesCount': response.changes.categories.length,
};
} catch (e) {
return {'error': true, 'message': e.toString()};
}
}
Future<void> _applySyncResponse(SyncResponse response) async {
// Apply categories from server
for (final SyncCategoryResponse catResponse
in response.changes.categories) {
await _database.upsertCategory(
CategoriesCompanion(
uuid: Value(catResponse.id),
encryptedName: Value(catResponse.encryptedName),
serverVersion: Value(catResponse.serverVersion),
isDeleted: Value(catResponse.isDeleted),
updatedAt: Value(catResponse.updatedAt),
),
);
}
// Apply notes from server
for (final SyncNoteResponse noteResponse in response.changes.notes) {
final existingNote = await (_database.select(
_database.notes,
)..where((n) => n.uuid.equals(noteResponse.id))).getSingleOrNull();
// Decrypt note content
String decryptedTitle = 'Encrypted';
String decryptedBody = 'Encrypted';
try {
decryptedTitle = await NoteEncryption.decryptNote(
noteResponse.encryptedTitle,
_masterKey,
);
decryptedBody = await NoteEncryption.decryptNote(
noteResponse.encryptedBody,
_masterKey,
);
} catch (e) {
// If decryption fails, keep default encrypted placeholders
print('Failed to decrypt note ${noteResponse.id}: $e');
}
if (existingNote != null) {
// Update existing note
await _database.updateNoteRow(
DbNote(
id: existingNote.id,
uuid: noteResponse.id,
title: decryptedTitle,
body: decryptedBody,
createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position,
serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted,
categoryId: noteResponse.categoryId,
),
);
} else {
// Insert new note
await _database
.into(_database.notes)
.insert(
NotesCompanion(
uuid: Value(noteResponse.id),
title: Value(decryptedTitle),
body: Value(decryptedBody),
createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position),
serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted),
categoryId: Value(noteResponse.categoryId),
),
);
}
}
}
Future<List<Note>> _loadNotesFromDatabase() async {
final List<DbNote> rows = await _database.getAllNotes();
return rows.map(_fromDbNote).toList();
}
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,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
categoryId: row.categoryId,
);
}
Category _fromDbCategory(DbCategory row) {
return Category(
uuid: row.uuid,
encryptedName: row.encryptedName,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
updatedAt: row.updatedAt,
);
}
}