From 6f44b07ad411b304f3c2ebbdd65b746271c4e958 Mon Sep 17 00:00:00 2001 From: Nialixus Date: Thu, 19 Mar 2026 17:12:22 +0700 Subject: [PATCH 1/2] [3.0.0] Pre AI generation --- lib/local_shared.dart | 1 + lib/src/shared_collection.dart | 36 +++-- lib/src/shared_document.dart | 20 +-- lib/src/shared_extension.dart | 9 +- lib/src/shared_many_document.dart | 19 +-- test/src/shared_collection_test.dart | 19 +-- test/src/shared_document_test.dart | 195 +++++++++++++++++++++++++++ 7 files changed, 256 insertions(+), 43 deletions(-) create mode 100644 test/src/shared_document_test.dart diff --git a/lib/local_shared.dart b/lib/local_shared.dart index 5f83c8e..6719001 100644 --- a/lib/local_shared.dart +++ b/lib/local_shared.dart @@ -3,6 +3,7 @@ library local_shared; import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/src/shared_collection.dart b/lib/src/shared_collection.dart index 70d5732..3c53f7c 100644 --- a/lib/src/shared_collection.dart +++ b/lib/src/shared_collection.dart @@ -43,6 +43,23 @@ class SharedCollection { /// The stream controller used to listen for changes in the collection. late final StreamController _controller; + Future> ids() async { + final result = []; + try { + JSON? collection = await Shared._read(id); + if (collection != null) { + for (var item in collection.entries) { + result.add(item.key); + } + } + + return result; + } catch (e) { + debugPrint("Failed to get ids, reason: $e"); + return result; + } + } + /// Creates a new collection. /// /// Optionally, it can replace an existing collection if [replace] is set to true. @@ -72,9 +89,10 @@ class SharedCollection { bool result = await Shared._create(id, collection); // [4] Notify the stream about the change in the collection πŸ“£. - _controller.add({'id': id, 'documents': [ - for (var item in collection.entries) item.value - ]}); + _controller.add({ + 'id': id, + 'documents': [for (var item in collection.entries) item.value] + }); // [5] Returning the result of creating / replacing this collection πŸš€. return SharedMany( @@ -82,9 +100,7 @@ class SharedCollection { message: result ? 'The collection with ID `$id` has been successfully ${replace ? 'recreated' : 'created'}.' : 'Failed to ${replace ? 'recreate' : 'create'} the collection with ID `$id`. Please try again.', - data: [ - for (var item in collection.entries) item.value - ], + data: [for (var item in collection.entries) item.value], ); } catch (e) { // [6] Returning bad news 🧨. @@ -126,7 +142,7 @@ class SharedCollection { /// Migrates the current collection to a new collection with the specified [id]. /// - /// Optionally, it can replace an existing target collection if [replace] is set to true. + /// Optionally, it can replace an existing target collection if [merge] is set to true. /// Optionally, it can force migration even if the current collection does not exist, by setting [force] to true. /// Returns a [SharedResponse] of [SharedMany] indicating the success or [SharedNone] for failure of the migration. /// @@ -171,8 +187,7 @@ class SharedCollection { 'where the same key will prioritize the current collection'; } - JSON merged = (collection??{}).merge(target??{}); - + JSON merged = (collection ?? {}).merge(target ?? {}); // [6] Creating new collection πŸŽ‰. bool result = await Shared._create(id, merged); @@ -181,8 +196,7 @@ class SharedCollection { _controller.add({ 'id': id, 'documents': [ - for (var item in merged.entries) - {'id': item.key, 'data': item.value} + for (var item in merged.entries) {'id': item.key, 'data': item.value} ] }); diff --git a/lib/src/shared_document.dart b/lib/src/shared_document.dart index e3b62b5..dfce138 100644 --- a/lib/src/shared_document.dart +++ b/lib/src/shared_document.dart @@ -44,7 +44,7 @@ class SharedDocument { /// Creates a new document within the associated collection. /// - /// Optionally, it can replace an existing document if [replace] is set to true. + /// Optionally, it can merge an existing document if [merge] is set to true. /// Optionally, it can force creating new collection if the current collection does not exist, by setting [force] to true. /// Returns a [SharedResponse] of [SharedOne] for indicating the success or [SharedNone] for failure of the operation. /// @@ -54,7 +54,7 @@ class SharedDocument { /// ``` Future create( JSON document, { - bool replace = false, + bool merge = false, bool force = true, }) async { try { @@ -70,17 +70,19 @@ class SharedDocument { 'collection and continued by creating a document within it.'; } else { // [3] Check if document exists or not πŸ•Š. - if (collection?[id] != null && !replace) { + if (collection?[id] != null && !merge) { throw 'The document already exists. ' - 'WARNING: To proceed and replace the document with ID `$id`, ' - 'set the `replace` parameter to true. ' - 'This action will irreversibly replace the old document.'; + 'WARNING: To proceed and merge the document with ID `$id`, ' + 'set the `merge` parameter to true. ' + 'This action will irreversibly merge the old document.'; } + JSON merged = (collection?[id] as JSON? ?? {}).merge(document); + // [4] Creating the document πŸŽ‰. bool result = await Shared._create( this.collection.id, - ((collection ?? {})..addEntries([MapEntry(id, document)])), + ((collection ?? {})..addEntries([MapEntry(id, merged)])), ); // [5] Notify the stream about the change in the collection πŸ“£. @@ -97,8 +99,8 @@ class SharedDocument { return SharedOne( success: result, message: result - ? 'The document with ID `$id` has been successfully ${replace ? 'replaced' : 'created'}.' - : 'Failed to ${replace ? 'replace' : 'create'} the document with ID `$id`. Please try again.', + ? 'The document with ID `$id` has been successfully ${merge ? 'merged' : 'created'}.' + : 'Failed to ${merge ? 'merge' : 'create'} the document with ID `$id`. Please try again.', data: (await Shared._read(this.collection.id))?[id], ); } diff --git a/lib/src/shared_extension.dart b/lib/src/shared_extension.dart index 8307eb9..eba5e14 100644 --- a/lib/src/shared_extension.dart +++ b/lib/src/shared_extension.dart @@ -29,7 +29,7 @@ extension _ListExtension on List { } extension JSONExtension on JSON { - /// Merges the current [JSON] object with another [JSON] object. + /// Merges the current [JSON] object with another [JSON] object. /// /// The [other] parameter is the [JSON] object to merge into the current object. /// The merge operation combines the key-value pairs from both objects. @@ -50,10 +50,10 @@ extension JSONExtension on JSON { result[key] = sourceValue.merge(targetValue); } else { // 4. OVERWRITE: Source wins if it's not a map or the target isn't a map - result[key] = sourceValue; + result[key] = targetValue; } } - + return result; } } @@ -79,9 +79,6 @@ extension _JSONExtension on JSON { } } - - - /// Encodes the [JSON] object into a JSON-formatted string. /// /// The method also performs a validation check on the types of entries in the [JSON] object. diff --git a/lib/src/shared_many_document.dart b/lib/src/shared_many_document.dart index e94d037..dee9e9f 100644 --- a/lib/src/shared_many_document.dart +++ b/lib/src/shared_many_document.dart @@ -53,7 +53,7 @@ class SharedManyDocument { /// ``` Future create( JSON Function(int index) document, { - bool replace = false, + bool merge = false, bool force = true, }) async { try { @@ -73,11 +73,11 @@ class SharedManyDocument { // [4] Check if document exists or not πŸ•Š. for (String id in ids) { - if (collection[id] != null && !replace) { + if (collection[id] != null && !merge) { throw 'The document already exists. ' - 'WARNING: To proceed and replace the document with ID `$id`, ' - 'set the `replace` parameter to true. ' - 'This action will irreversibly replace the old document.'; + 'WARNING: To proceed and merge the document with ID `$id`, ' + 'set the `merge` parameter to true. ' + 'This action will irreversibly merge the old document.'; } } @@ -86,7 +86,10 @@ class SharedManyDocument { this.collection.id, ({ ...collection, - for (int i = 0; i < ids.length; i++) ids.elementAt(i): document(i), + for (int i = 0; i < ids.length; i++)...(){ + final id = ids.elementAt(i); + return {id:(collection?[id] as JSON? ?? {}).merge( document(i))}; + }(), }), ); @@ -104,8 +107,8 @@ class SharedManyDocument { return SharedMany( success: result, message: result - ? '${ids.length} document from specified IDs `${ids.join('`, `')}` has been successfully ${replace ? 'replaced' : 'created'}.' - : 'Failed to ${replace ? 'replace' : 'create'} ${ids.length} document from specified IDs `${ids.join('`, `')}`. Please try again.', + ? '${ids.length} document from specified IDs `${ids.join('`, `')}` has been successfully ${merge ? 'merged' : 'created'}.' + : 'Failed to ${merge ? 'replace' : 'create'} ${ids.length} document from specified IDs `${ids.join('`, `')}`. Please try again.', data: [ for (var id in ids) (await Shared._read(this.collection.id))?[id], ].where((e) => e != null).map((e) => e as JSON).toList(), diff --git a/test/src/shared_collection_test.dart b/test/src/shared_collection_test.dart index eef498c..d431931 100644 --- a/test/src/shared_collection_test.dart +++ b/test/src/shared_collection_test.dart @@ -23,7 +23,7 @@ void main() { FlutterSecureStorage.setMockInitialValues({}); SharedPreferences.setMockInitialValues({}); await LocalShared('test_db').initialize(); - + // // 3. Setup the collection instance collection = Shared.collection(collectionId); collection2 = Shared.collection(collectionId2); @@ -58,7 +58,6 @@ void main() { expect(response.message, contains('The collection already exists')); }); - test('Create Existing Collection Forcibly', () async { // Arrange: Mock up an existing collection await collection.create(); @@ -114,7 +113,7 @@ void main() { expect(oldResponse.message, contains('does not exist')); }); - test('Update Collection to Existing Collection', () async { + test('Update Collection to Existing Collection', () async { // Arrange: Mock up collection await collection.create(); await collection2.create(); @@ -173,15 +172,16 @@ void main() { expect(response, isA()); }); - - test('Update Non Existing Collection to Existing Collection Forcibly', () async { + test('Update Non Existing Collection to Existing Collection Forcibly', + () async { // Arrange: Mock up collection await collection.delete(); await collection2.create(); await document2.create(data); // Act: Update the collection - final update = await collection.update(collectionId2, merge: true, force: true); + final update = + await collection.update(collectionId2, merge: true, force: true); final response = await collection2.read(); // Assert: Should return these values @@ -192,7 +192,7 @@ void main() { expect(response, isA()); }); - test('Update Collection to the Same Collection', () async { + test('Update Collection to the Same Collection', () async { // Arrange: Mock up collection await collection.create(); await document.create(data); @@ -203,7 +203,8 @@ void main() { // Assert: Should return these values expect(update.success, isFalse); - expect(update.message, contains('collection ID cannot be the same as the current one')); + expect(update.message, + contains('collection ID cannot be the same as the current one')); expect(update.data, isNull); expect(update, isA()); expect(response.success, isTrue); @@ -243,4 +244,4 @@ void main() { expect(response, isA()); }); }); -} \ No newline at end of file +} diff --git a/test/src/shared_document_test.dart b/test/src/shared_document_test.dart new file mode 100644 index 0000000..d47a58a --- /dev/null +++ b/test/src/shared_document_test.dart @@ -0,0 +1,195 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_shared/local_shared.dart'; + +void main() { + // 1. Mandatory for MethodChannels + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Shared Document Test', () { + late SharedCollection collection; + late SharedDocument document; + const String collectionId = 'collection_1'; + const String documentId = 'document_1'; + const JSON data = {'key': 'value1', '1': '1'}; + const JSON data2 = {'key': 'value2', '2': '2'}; + + setUp(() async { + // 2. Initialize LocalShared + FlutterSecureStorage.setMockInitialValues({}); + SharedPreferences.setMockInitialValues({}); + await LocalShared('test_db').initialize(); + + // 3. Setup the collection and document instance + collection = Shared.collection(collectionId); + document = collection.document(documentId); + }); + + test('Create Document', () async { + // Arrange: Force clear the workspace + await collection.delete(); + + // Act: Create a new document (force=true by default in create) + final response = await document.create(data); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response.data, data); + expect(response, isA()); + expect(response.message, contains('has been successfully created')); + }); + + test('Create Document in Non-Existing Collection (No Force)', () async { + // Arrange: Ensure collection doesn't exist + await collection.delete(); + + // Act: Try to create document without forcing collection creation + final response = await document.create(data, force: false); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('Create Existing Document', () async { + // Arrange: Mock up an existing document + await document.create(data); + + // Act: Create a new document with the same ID + final response = await document.create(data); + + // Assert: Should return these values + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('The document already exists')); + }); + + test('Create Existing Document Forcibly (Merge)', () async { + // Arrange: Mock up an existing document + await document.create(data); + + // Act: Create a new document with the same ID and merge: true + final response = await document.create(data2, merge: true); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response.data, data.merge(data2)); + expect(response, isA()); + expect(response.message, contains('has been successfully merged')); + }); + + test('Read Document', () async { + // Arrange: Mock up document + await document.create(data); + + // Act: Read the document + final response = await document.read(); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response.data, data); + expect(response, isA()); + expect(response.message, contains('has been successfully retrieved')); + }); + + test('Read Non-Existing Document', () async { + // Arrange: Clear the collection + await collection.create(replace: true); + + // Act: Read a document that doesn't exist + final response = await document.read(); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('Read Document in Non-Existing Collection', () async { + // Arrange: Delete the collection + await collection.delete(); + + // Act: Read a document when collection is gone + final response = await document.read(); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('Update Document', () async { + // Arrange: Mock up document + await document.create(data); + + // Act: Update the document (merging data2) + final response = await document.update(data2); + + // Assert: Should return merged values + expect(response.success, isTrue); + expect(response.data, data.merge(data2)); + expect(response, isA()); + expect(response.message, contains('has been successfully updated')); + }); + + test('Update Non-Existing Document (No Force)', () async { + // Arrange: Clear the collection + await collection.create(replace: true); + + // Act: Update a document that doesn't exist without force + final response = await document.update(data2, force: false); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + + test('Update Non-Existing Document Forcibly', () async { + // Arrange: Clear the collection + await collection.create(replace: true); + + // Act: Update a document that doesn't exist with force: true + final response = await document.update(data2, force: true); + + // Assert: Should succeed and create the document + expect(response.success, isTrue); + expect(response.data, data2); + expect(response, isA()); + expect(response.message, contains('has been successfully updated')); + }); + + test('Delete Document', () async { + // Arrange: Mock up document + await document.create(data); + + // Act: Delete the document + final response = await document.delete(); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response.message, contains('has been successfully deleted')); + expect(response.data, isNull); + expect(response, isA()); + + // Verify it's actually gone + final readResponse = await document.read(); + expect(readResponse.success, isFalse); + }); + + test('Delete Non-Existing Document', () async { + // Arrange: Clear the collection + await collection.create(replace: true); + + // Act: Delete a document that doesn't exist + final response = await document.delete(); + + // Assert: Should fail + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('does not exist')); + }); + }); +} From 0327bc07100d42a92ee6347917cf5e882d75d9af Mon Sep 17 00:00:00 2001 From: Nialixus Date: Thu, 19 Mar 2026 19:52:19 +0700 Subject: [PATCH 2/2] [3.0.0] AI - Update Test --- example/lib/src/collection_crud.dart | 2 +- lib/src/shared_collection.dart | 64 ++++++++++-- lib/src/shared_document.dart | 79 +++++++++++++++ lib/src/shared_extension.dart | 7 +- lib/src/shared_many_document.dart | 96 ++++++++++++++++++ test/src/shared_collection_test.dart | 113 +++++++++++++++++---- test/src/shared_document_test.dart | 67 +++++++++++++ test/src/shared_many_document_test.dart | 127 ++++++++++++++++++++++++ 8 files changed, 526 insertions(+), 29 deletions(-) create mode 100644 test/src/shared_many_document_test.dart diff --git a/example/lib/src/collection_crud.dart b/example/lib/src/collection_crud.dart index 7e01c39..d8a74df 100644 --- a/example/lib/src/collection_crud.dart +++ b/example/lib/src/collection_crud.dart @@ -55,7 +55,7 @@ class _A extends State { String id = 'MY_COLLECTION_${Random().nextInt(1000).toString().padLeft(3, '0')}'; final response = - await Shared.col(collection.text).update(id); + await Shared.col(collection.text).migrate(id); this.response.text = '$response'; json.text = '${response.data}'; collection.text = id; diff --git a/lib/src/shared_collection.dart b/lib/src/shared_collection.dart index 3c53f7c..78e5e22 100644 --- a/lib/src/shared_collection.dart +++ b/lib/src/shared_collection.dart @@ -140,17 +140,68 @@ class SharedCollection { } } - /// Migrates the current collection to a new collection with the specified [id]. + /// Updates the contents of the collection. /// - /// Optionally, it can replace an existing target collection if [merge] is set to true. - /// Optionally, it can force migration even if the current collection does not exist, by setting [force] to true. - /// Returns a [SharedResponse] of [SharedMany] indicating the success or [SharedNone] for failure of the migration. + /// Optionally, it can force update if the current collection does not exist, by setting [force] to true. + /// Returns a [SharedResponse] of [SharedMany] indicating success or [SharedNone] for failure. /// /// ```dart - /// final response = await Shared.col(id).update(newId); + /// final response = await Shared.col('myCollection').update({'id': {'key': 'value'}}); /// print(response); // SharedMany(success: true, message: '...', data: []) /// ``` - Future update( + Future update(JSON document, {bool force = false}) async { + try { + // [1] Get collection πŸ“‚. + JSON? collection = await Shared._read(id); + + // [2] Check if collection exists or not πŸ‘». + if (collection == null && !force) { + throw 'Unable to update the collection. ' + 'The specified collection with ID `$id` does not exist. ' + 'To forcibly continue, set the `force` parameter to true. ' + 'This action will create a new collection.'; + } + + // [3] Updating the collection πŸŽ‰. + final bool result = await Shared._create(id, (collection ?? {}).merge(document)); + + // [4] Notify the stream about the change in the collection πŸ“£. + _controller.add({ + 'id': id, + 'documents': [ + for (var entry in ((await Shared._read(id)) ?? {}).entries) + {'id': entry.key, 'data': entry.value}, + ] + }); + + final JSON updated = (await Shared._read(id)) ?? {}; + + // [5] Returning the result of updating this collection πŸš€. + return SharedMany( + success: result, + message: result + ? 'The collection with ID `$id` has been successfully updated.' + : 'Failed to update the collection with ID `$id`. Please try again.', + data: [for (var item in updated.entries) item.value as JSON], + ); + } catch (e) { + // [6] Returning bad news 🧨. + return SharedNone(message: '$e'); + } + } + + /// Merges (migrates) the current collection to a new collection with the specified [id]. + /// + /// If [merge] is true and the target collection exists, both collection data sets are merged. + /// If [merge] is false, the target must not exist unless [force] is set to true. + /// Setting [force] true will allow migration from an empty source if the source collection is missing. + /// Returns a [SharedResponse] of [SharedMany] indicating success or [SharedNone] for failure. + /// + /// ```dart + /// final response = await Shared.col(id).merge(newId); + /// print(response); // SharedMany(success: true, message: '...', data: []) + /// ``` + Future migrate( String id, { bool merge = false, bool force = false, @@ -227,6 +278,7 @@ class SharedCollection { } } + /// Deletes the collection. /// /// Returns a [SharedResponse] of [SharedNone] indicating the success or failure of the deletion. diff --git a/lib/src/shared_document.dart b/lib/src/shared_document.dart index dfce138..5299008 100644 --- a/lib/src/shared_document.dart +++ b/lib/src/shared_document.dart @@ -209,6 +209,85 @@ class SharedDocument { } } + /// Migrates the current document to a new document ID inside the same collection. + /// + /// If [merge] is true and the target document already exists, data is merged with the source document. + /// If [merge] is false, the target document must not exist unless [force] is true. + /// If [force] is true, missing source or collection is treated as empty to allow an incremental migration. + /// + /// ```dart + /// final response = await Shared.col('myCollection').doc('doc1').migrate('doc2'); + /// print(response); // SharedOne(success: true, message: '...', data: JSON) + /// ``` + Future migrate(String id, + {bool merge = false, bool force = false,}) async { + try { + // [1] Get collection πŸ“‚. + JSON? collection = await Shared._read(this.collection.id); + + // [2] Check collection existence πŸ”. + if (collection == null && !force) { + throw 'Unable to migrate the document. ' + 'The specified collection with ID `${this.collection.id}` does not exist.'; + } + + // [3] Source and destination cannot be identical. + if (this.id == id && !force) { + throw 'Unable to migrate the document. ' + 'Source and destination document IDs cannot be the same.'; + } + + // [4] Source document existence. + if (collection?[this.id] == null && !force) { + throw 'Unable to migrate the document. ' + 'The source document with ID `${this.id}` does not exist.'; + } + + // [5] Target existence check. + if (collection?[id] != null && !merge) { + throw 'Unable to migrate the document. ' + 'The target document with ID `$id` already exists. ' + 'To merge with existing target, set `merge` to true.'; + } + + final JSON sourceDoc = (collection?[this.id] as JSON?) ?? {}; + final JSON targetDoc = (collection?[id] as JSON?) ?? {}; + final JSON migratedDoc = collection?[id] != null && merge + ? sourceDoc.merge(targetDoc) + : sourceDoc; + + // [6] Write migrated collection. + final JSON updatedCollection = JSON.from(collection ?? {}) + ..remove(this.id) + ..[id] = migratedDoc; + + bool result = await Shared._create(this.collection.id, updatedCollection); + + // [7] Notify stream πŸ“£. + this.collection._controller.add({ + 'id': this.collection.id, + 'documents': [ + for (var item in ((await Shared._read(this.collection.id)) ?? {}).entries) + {'id': item.key, 'data': item.value}, + ], + }); + + // [8] Return. + if (result) { + return SharedOne( + success: true, + message: + 'The document with ID `${this.id}` has been successfully migrated to ID `$id`.', + data: migratedDoc, + ); + } + + throw 'Failed to migrate the document from ID `${this.id}` to ID `$id`.'; + } catch (e) { + return SharedNone(message: '$e'); + } + } + /// Deletes the document within the associated collection. /// /// Returns a [SharedResponse] of [SharedNone] indicating the success or failure of the deletion. diff --git a/lib/src/shared_extension.dart b/lib/src/shared_extension.dart index eba5e14..0e3739b 100644 --- a/lib/src/shared_extension.dart +++ b/lib/src/shared_extension.dart @@ -39,18 +39,17 @@ extension JSONExtension on JSON { // 1. Create a copy of the other (the target/base) final result = JSON.from(other); - // 2. Iterate through the keys of this (the source/priority) + // 2. Iterate through the keys of this (the source) for (var key in keys) { final sourceValue = this[key]; final targetValue = result[key]; if (sourceValue is JSON && targetValue is JSON) { // 3. RECURSION: If both are Maps, merge them - // We cast to JSON so the extension can find the 'merge' method again result[key] = sourceValue.merge(targetValue); } else { - // 4. OVERWRITE: Source wins if it's not a map or the target isn't a map - result[key] = targetValue; + // 4. OVERWRITE: target wins if set, otherwise source + result[key] = targetValue ?? sourceValue; } } diff --git a/lib/src/shared_many_document.dart b/lib/src/shared_many_document.dart index dee9e9f..868a7d8 100644 --- a/lib/src/shared_many_document.dart +++ b/lib/src/shared_many_document.dart @@ -317,6 +317,102 @@ class SharedManyDocument { } } + + Future migrate( + String id, { + bool merge = false, + bool force = false, + }) async { + try { + // [1] Get source collection πŸ“‚. + JSON source = (await Shared._read(collection.id)) ?? {}; + + // [2] Source collection existence. + if (source.isEmpty && !force) { + throw 'Unable to migrate documents. ' + 'The collection with ID `${collection.id}` does not exist.'; + } + + // [3] Source and destination document cannot be identical unless forced. + if (ids.contains(id) && !force) { + throw 'Unable to migrate documents. ' + 'Source and destination document IDs cannot be the same.'; + } + + // [4] Target document + final JSON? existingTarget = source[id] as JSON?; + if (existingTarget != null && !merge && !force) { + throw 'Unable to migrate documents. ' + 'The target document with ID `$id` already exists. ' + 'To merge with existing target, set `merge` to true.'; + } + + // [5] Collect content from source documents. + JSON migratedData = {}; + bool hasData = false; + for (var docId in ids) { + if (docId == id) continue; + final doc = source[docId]; + + if (doc == null) { + if (!force) { + throw 'Unable to migrate documents. ' + 'Source document with ID `$docId` does not exist.'; + } + continue; + } + + if (doc is! JSON) { + throw 'Unable to migrate documents. ' + 'Source document with ID `$docId` has invalid type.'; + } + + migratedData = hasData ? migratedData.merge(doc) : JSON.from(doc); + hasData = true; + source.remove(docId); + } + + // [6] Construct final target document. + JSON finalTarget; + if (existingTarget != null && merge) { + finalTarget = existingTarget.merge(migratedData); + } else { + finalTarget = migratedData; + } + + // If no data is migrated and target is missing, report (unless force). + if (!hasData && existingTarget == null && !force) { + throw 'Unable to migrate documents. ' + 'No source documents were available for migration.'; + } + + source[id] = finalTarget; + + // [7] Persist state πŸŽ‰. + final bool result = await Shared._create(collection.id, source); + + // [8] Notify stream πŸ“£. + collection._controller.add({ + 'id': collection.id, + 'documents': [ + for (var item in ((await Shared._read(collection.id)) ?? {}).entries) + {'id': item.key, 'data': item.value}, + ], + }); + + // [9] Return result πŸš€. + return SharedOne( + success: result, + message: result + ? 'Selected documents from IDs `${ids.join('`, `')}` have been successfully migrated to document `$id`.' + : 'Failed to migrate selected documents to document `$id`. Please try again.', + data: (await Shared._read(collection.id))?[id] as JSON?, + ); + } catch (e) { + return SharedNone(message: '$e'); + } + } + @override String toString() { return '$runtimeType(ids: $ids, collection: $collection)'; diff --git a/test/src/shared_collection_test.dart b/test/src/shared_collection_test.dart index d431931..551d213 100644 --- a/test/src/shared_collection_test.dart +++ b/test/src/shared_collection_test.dart @@ -94,8 +94,46 @@ void main() { await collection.create(); await document.create(data); - // Act: Update the collection - final update = await collection.update(collectionId2); + // Act: Update the collection with partial merge + final response = await collection.update({ + 'document_1': { + 'key': 'updated', + 'newField': 'yes', + } + }); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response.data, isA()); + expect(response.data, contains(equals({'key': 'updated', 'newField': 'yes', '1': '1'}))); + expect(response, isA()); + expect(response.message, contains('has been successfully updated')); + }); + + test('Update Missing Collection Forcibly', () async { + // Arrange: Ensure collection does not exist + await collection.delete(); + + // Act + final response = await collection.update({ + 'someDoc': {'value': 1} + }, force: true); + + // Assert + expect(response.success, isTrue); + expect(response.data, isA()); + expect(response.data, contains(equals({'value': 1}))); + expect(response, isA()); + expect(response.message, contains('has been successfully updated')); + }); + + test('Migrate Collection', () async { + // Arrange: Mock up collection + await collection.create(); + await document.create(data); + + // Act: Migrate the collection + final update = await collection.migrate(collectionId2); final response = await collection2.read(); // Assert: Should return these values @@ -113,14 +151,14 @@ void main() { expect(oldResponse.message, contains('does not exist')); }); - test('Update Collection to Existing Collection', () async { + test('Migrate Collection to Existing Collection', () async { // Arrange: Mock up collection await collection.create(); await collection2.create(); await document.create(data); - // Act: Update the collection - final update = await collection.update(collectionId2); + // Act: Migrate the collection + final update = await collection.migrate(collectionId2); final response = await collection2.read(); // Assert: Should return these values @@ -133,15 +171,15 @@ void main() { expect(response, isA()); }); - test('Update Collection to Existing Collection Forcibly', () async { + test('Migrate Collection to Existing Collection Forcibly', () async { // Arrange: Mock up collection await collection.create(); await collection2.create(); await document.create(data); await document2.create(data2); - // Act: Update the collection - final update = await collection.update(collectionId2, merge: true); + // Act: Migrate the collection + final update = await collection.migrate(collectionId2, merge: true); final response = await collection2.read(); // Assert: Should return these values @@ -152,14 +190,14 @@ void main() { expect(response, isA()); }); - test('Update Non Existing Collection to Existing Collection', () async { + test('Migrate Non Existing Collection to Existing Collection', () async { // Arrange: Mock up collection await collection.delete(); await collection2.create(); await document2.create(data); - // Act: Update the collection - final update = await collection.update(collectionId2); + // Act: Migrate the collection + final update = await collection.migrate(collectionId2); final response = await collection2.read(); // Assert: Should return these values @@ -172,16 +210,16 @@ void main() { expect(response, isA()); }); - test('Update Non Existing Collection to Existing Collection Forcibly', + test('Migrate Non Existing Collection to Existing Collection Forcibly', () async { // Arrange: Mock up collection await collection.delete(); await collection2.create(); await document2.create(data); - // Act: Update the collection + // Act: Migrate the collection final update = - await collection.update(collectionId2, merge: true, force: true); + await collection.migrate(collectionId2, merge: true, force: true); final response = await collection2.read(); // Assert: Should return these values @@ -192,13 +230,13 @@ void main() { expect(response, isA()); }); - test('Update Collection to the Same Collection', () async { + test('Migrate Collection to the Same Collection', () async { // Arrange: Mock up collection await collection.create(); await document.create(data); // Act: Update the collection - final update = await collection.update(collectionId); + final update = await collection.migrate(collectionId); final response = await collection.read(); // Assert: Should return these values @@ -212,13 +250,13 @@ void main() { expect(response, isA()); }); - test('Update Collection to the Same Collection Forcibly', () async { + test('Migrate Collection to the Same Collection Forcibly', () async { // Arrange: Mock up collection await collection.create(); await document.create(data); // Act: Update the collection - final update = await collection.update(collectionId, force: true); + final update = await collection.migrate(collectionId, force: true); final response = await collection.read(); // Assert: Should return these values @@ -229,6 +267,45 @@ void main() { expect(response, isA()); }); + test('Migrate Documents from Collection Using SharedManyDocument', () async { + // Arrange: Setup source collection and documents + await collection.create(); + await document.create(data); + await collection.document('document_2').create({'x': 'y'}); + + // Act: Migrate specific documents from source into a single target document + final response = await collection.docs([documentId, 'document_2']).migrate('merged_document'); + final targetRead = await collection.doc('merged_document').read(); + final sourceRead = await collection.read(); + + // Assert: Should return these values + expect(response.success, isTrue); + expect(response, isA()); + expect(targetRead.success, isTrue); + expect(targetRead.one, containsPair('key', 'value1')); + expect(targetRead.one, containsPair('x', 'y')); + expect(sourceRead.success, isTrue); + expect(sourceRead.data, isA>()); + expect(sourceRead.data, contains(equals({'x': 'y', 'key': 'value1', '1': '1'}))); // merged doc should be new target not in source list + }); + + test('Migrate Documents to Target With Merge', () async { + // Arrange: Setup source collection and existing target document + await collection.create(); + await document.create(data); + await collection.doc('merged_document').create({'1': '2'}); + + // Act: Migrate with merge true into the existing target document + final response = await collection.docs([documentId]).migrate('merged_document', merge: true); + final targetRead = await collection.doc('merged_document').read(); + + // Assert: Should merge existing target document + expect(response.success, isTrue); + expect(targetRead.success, isTrue); + expect(targetRead.one, containsPair('key', 'value1')); + expect(targetRead.one, containsPair('1', '1')); // existing value preserved by target precedence + }); + test('Delete Collection', () async { // Arrange: Mock up collection await collection.create(); diff --git a/test/src/shared_document_test.dart b/test/src/shared_document_test.dart index d47a58a..3368f76 100644 --- a/test/src/shared_document_test.dart +++ b/test/src/shared_document_test.dart @@ -12,6 +12,7 @@ void main() { late SharedDocument document; const String collectionId = 'collection_1'; const String documentId = 'document_1'; + const String documentId2 = 'document_2'; const JSON data = {'key': 'value1', '1': '1'}; const JSON data2 = {'key': 'value2', '2': '2'}; @@ -161,6 +162,72 @@ void main() { expect(response.message, contains('has been successfully updated')); }); + test('Migrate Document', () async { + // Arrange: add source document + await document.create(data); + + // Act + final response = await document.migrate(documentId2); + + // Assert + expect(response.success, isTrue); + expect(response, isA()); + expect(response.data, data); + expect(response.message, contains('has been successfully migrated')); + + final readSource = await document.read(); + expect(readSource.success, isFalse); + + final readDestination = await collection.doc(documentId2).read(); + expect(readDestination.success, isTrue); + expect(readDestination.data, data); + }); + + test('Migrate Document with Existing Target (No Merge)', () async { + // Arrange: add source and target documents + await document.create(data); + await collection.doc(documentId2).create(data2); + + // Act + final response = await document.migrate(documentId2, merge: false); + + // Assert + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('already exists')); + }); + + test('Migrate Document with Existing Target and Merge', () async { + // Arrange: add source and target documents + await document.create(data); + await collection.doc(documentId2).create(data2); + + // Act + final response = await document.migrate(documentId2, merge: true); + + // Assert + expect(response.success, isTrue); + expect(response, isA()); + expect(response.data, data.merge(data2)); + + final readSource = await document.read(); + expect(readSource.success, isFalse); + + final readDestination = await collection.doc(documentId2).read(); + expect(readDestination.success, isTrue); + expect(readDestination.data, data.merge(data2)); + }); + + test('Migrate Document to Same ID fails', () async { + await document.create(data); + + final response = await document.migrate(documentId); + + expect(response.success, isFalse); + expect(response, isA()); + expect(response.message, contains('cannot be the same')); + }); + test('Delete Document', () async { // Arrange: Mock up document await document.create(data); diff --git a/test/src/shared_many_document_test.dart b/test/src/shared_many_document_test.dart new file mode 100644 index 0000000..ff191ef --- /dev/null +++ b/test/src/shared_many_document_test.dart @@ -0,0 +1,127 @@ +ο»Ώimport 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_shared/local_shared.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Shared Many Document Test', () { + late SharedCollection collection; + const String collectionId = 'collection_many'; + const JSON docA = {'a': 1}; + const JSON docB = {'b': 2}; + + setUp(() async { + FlutterSecureStorage.setMockInitialValues({}); + SharedPreferences.setMockInitialValues({}); + await LocalShared('test_db').initialize(); + collection = Shared.collection(collectionId); + await collection.delete(); + }); + + test('Create multiple documents', () async { + final response = await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + + expect(response, isA()); + expect(response.success, isTrue); + expect(response.data, hasLength(2)); + expect(response.data, containsAll([docA, docB])); + }); + + test('Create existing docs without merge should fail', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + + final response = await collection.docs(['docA']).create((_) => {'a': 9}, merge: false); + expect(response, isA()); + expect(response.success, isFalse); + }); + + test('Create existing docs with merge should succeed', () async { + await collection.docs(['docA']).create((_) => docA); + final response = await collection.docs(['docA']).create((_) => {'c': 3}, merge: true); + + expect(response, isA()); + expect(response.success, isTrue); + final stored = (await collection.docs(['docA']).read()).many; + expect(stored, isNotNull); + expect(stored!.first, containsPair('c', 3)); + expect(stored.first, containsPair('a', 1)); + }); + + test('Read missing doc with skip false throws', () async { + final response = await collection.docs(['docA']).read(skip: false); + expect(response, isA()); + expect(response.success, isFalse); + }); + + test('Update existing docs and force missing', () async { + await collection.docs(['docA']).create((_) => docA); + final response = await collection.docs(['docA', 'docB']).update((index) { + return index == 0 ? {'a': 10} : {'b': 20}; + }, force: true); + + expect(response, isA()); + expect(response.success, isTrue); + expect((await collection.docs(['docA', 'docB']).read()).many, hasLength(2)); + }); + + test('Delete existing docs', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + + final response = await collection.docs(['docA']).delete(); + expect(response, isA()); + expect(response.success, isTrue); + final rest = (await collection.read()).data as List?; + expect(rest, hasLength(1)); + }); + + test('Migrate many documents into one target document', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + + final response = await collection.docs(['docA', 'docB']).migrate('target'); + expect(response, isA()); + expect(response.success, isTrue); + + final target = (await collection.doc('target').read()).one; + expect(target, isNotNull); + expect(target, containsPair('a', 1)); + expect(target, containsPair('b', 2)); + + final remaining = await collection.read(); + expect((remaining.data as List), hasLength(1)); + expect((remaining.data as List).first, equals(target)); + }); + + test('Migrate into existing target without merge fails', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + await collection.doc('target').create({'existing': true}); + + final response = await collection.docs(['docA']).migrate('target', merge: false); + expect(response, isA()); + expect(response.success, isFalse); + }); + + test('Migrate into existing target with merge succeeds', () async { + await collection.docs(['docA', 'docB']).create((index) { + return index == 0 ? docA : docB; + }); + await collection.doc('target').create({'existing': true}); + + final response = await collection.docs(['docA']).migrate('target', merge: true); + expect(response, isA()); + expect(response.success, isTrue); + expect((await collection.doc('target').read()).one, containsPair('existing', true)); + }); + }); +}