Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/lib/src/collection_crud.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class _A extends State<CollectionCRUD> {
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;
Expand Down
1 change: 1 addition & 0 deletions lib/local_shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
98 changes: 82 additions & 16 deletions lib/src/shared_collection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ class SharedCollection {
/// The stream controller used to listen for changes in the collection.
late final StreamController<JSON> _controller;

Future<List<String>> ids() async {
final result = <String>[];
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.
Expand Down Expand Up @@ -72,19 +89,18 @@ 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(
success: result,
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 🧨.
Expand Down Expand Up @@ -124,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 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('myCollection').update({'id': {'key': 'value'}});
/// print(response); // SharedMany(success: true, message: '...', data: <JSON>[])
/// ```
Future<SharedResponse> 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].
///
/// Optionally, it can replace an existing target collection if [replace] 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.
/// 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).update(newId);
/// final response = await Shared.col(id).merge(newId);
/// print(response); // SharedMany(success: true, message: '...', data: <JSON>[])
/// ```
Future<SharedResponse> update(
Future<SharedResponse> migrate(
String id, {
bool merge = false,
bool force = false,
Expand Down Expand Up @@ -171,8 +238,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);
Expand All @@ -181,8 +247,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}
]
});

Expand Down Expand Up @@ -213,6 +278,7 @@ class SharedCollection {
}
}


/// Deletes the collection.
///
/// Returns a [SharedResponse] of [SharedNone] indicating the success or failure of the deletion.
Expand Down
99 changes: 90 additions & 9 deletions lib/src/shared_document.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -54,7 +54,7 @@ class SharedDocument {
/// ```
Future<SharedResponse> create(
JSON document, {
bool replace = false,
bool merge = false,
bool force = true,
}) async {
try {
Expand All @@ -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 📣.
Expand All @@ -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],
);
}
Expand Down Expand Up @@ -207,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<SharedResponse> 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.
Expand Down
14 changes: 5 additions & 9 deletions lib/src/shared_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -39,21 +39,20 @@ 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] = sourceValue;
// 4. OVERWRITE: target wins if set, otherwise source
result[key] = targetValue ?? sourceValue;
}
}

return result;
}
}
Expand All @@ -79,9 +78,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.
Expand Down
Loading
Loading