Skip to content

Commit 7b24830

Browse files
yulingtianxiaphillwiggins
authored andcommitted
Save recursively (#190)
* Fix Commit: 6ed26e1 * Update parse_encoder.dart Fix parse encoder bug. * Fix exception when sendSessionId is null. * 1. Support save objects recursively. 2. Fix encoding for Add and Remove operations. 3. Fix some exceptions. 4. Support batch request and handle response.
1 parent 91da0a8 commit 7b24830

12 files changed

+299
-75
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ var response = await dietPlan.remove("listKeywords", ["a"]);
154154
or using with save function
155155

156156
```dart
157-
dietPlan.setAdd('listKeywords', ['a','a','d']);
158-
dietPlan.setAddUnique('listKeywords', ['a','a','d']);
159-
dietPlan.setRemove('listKeywords', ['a']);
157+
dietPlan.setAddAll('listKeywords', ['a','a','d']);
158+
dietPlan.setAddAllUnique('listKeywords', ['a','a','d']);
159+
dietPlan.setRemoveAll('listKeywords', ['a']);
160160
var response = dietPlan.save()
161161
```
162162

example/lib/main.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class _MyAppState extends State<MyApp> {
105105

106106
Future<void> initInstallation() async {
107107
final ParseInstallation installation =
108-
await ParseInstallation.currentInstallation();
108+
await ParseInstallation.currentInstallation();
109109
final ParseResponse response = await installation.create();
110110
print(response);
111111
}
@@ -244,13 +244,13 @@ class _MyAppState extends State<MyApp> {
244244
/// Update current user from server - Best done to verify user is still a valid user
245245
response = await ParseUser.getCurrentUserFromServer(
246246
token: user?.get<String>(keyHeaderSessionToken));
247-
if (response.success) {
247+
if (response?.success ?? false) {
248248
user = response.result;
249249
}
250250

251251
/// log user out
252-
response = await user.logout();
253-
if (response.success) {
252+
response = await user?.logout();
253+
if (response?.success ?? false) {
254254
user = response.result;
255255
}
256256

lib/parse_server_sdk.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library flutter_parse_sdk;
33
import 'dart:async';
44
import 'dart:convert';
55
import 'dart:io';
6+
import 'dart:math';
67
import 'dart:typed_data';
78
import 'package:sembast/sembast.dart';
89
import 'package:sembast/sembast_io.dart';

lib/src/data/core_store_impl.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class CoreStoreImp implements CoreStore {
1616

1717
if (Platform.isIOS || Platform.isAndroid) {
1818
dbDirectory = (await getApplicationDocumentsDirectory()).path;
19-
final String dbPath = path.join('$dbDirectory+/parse', 'parse.db');
19+
final String dbPath = path.join('$dbDirectory/parse', 'parse.db');
2020
final Database db = await factory.openDatabase(dbPath, codec: codec);
2121
_instance = CoreStoreImp._internal(db);
2222
}

lib/src/enums/parse_enum_api_rq.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ enum ParseApiRQ {
3131
decrement,
3232
getConfigs,
3333
addConfig,
34-
liveQuery
34+
liveQuery,
35+
batch
3536
}

lib/src/objects/parse_installation.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class ParseInstallation extends ParseObject {
206206
///Subscribes the device to a channel of push notifications.
207207
void subscribeToChannel(String value) {
208208
final List<dynamic> channel = <String>[value];
209-
setAddUnique('channels', channel);
209+
setAddAllUnique('channels', channel);
210210
save();
211211
}
212212

lib/src/objects/parse_object.dart

Lines changed: 185 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,38 +68,196 @@ class ParseObject extends ParseBase implements ParseCloneable {
6868
final String body = json.encode(toJson(forApiRQ: true));
6969
final Response result = await _client.post(url, body: body);
7070

71-
//Set the objectId on the object after it is created.
72-
//This allows you to perform operations on the object after creation
73-
if (result.statusCode == 201) {
74-
final Map<String, dynamic> map = json.decode(result.body);
75-
objectId = map['objectId'].toString();
76-
}
77-
7871
return handleResponse<ParseObject>(
7972
this, result, ParseApiRQ.create, _debug, className);
8073
} on Exception catch (e) {
8174
return handleException(e, ParseApiRQ.create, _debug, className);
8275
}
8376
}
8477

78+
Future<ParseResponse> update() async {
79+
try {
80+
final Uri url = getSanitisedUri(_client, '$_path/$objectId');
81+
final String body = json.encode(toJson(forApiRQ: true));
82+
final Response result = await _client.put(url, body: body);
83+
return handleResponse<ParseObject>(
84+
this, result, ParseApiRQ.save, _debug, className);
85+
} on Exception catch (e) {
86+
return handleException(e, ParseApiRQ.save, _debug, className);
87+
}
88+
}
89+
8590
/// Saves the current object online
8691
Future<ParseResponse> save() async {
87-
if (getObjectData()[keyVarObjectId] == null) {
88-
return create();
92+
final ParseResponse response = await _saveChildren(this);
93+
if (response.success) {
94+
if (objectId == null) {
95+
return create();
96+
} else {
97+
return update();
98+
}
8999
} else {
90-
try {
91-
final Uri url = getSanitisedUri(_client, '$_path/$objectId');
92-
final String body = json.encode(toJson(forApiRQ: true));
93-
final Response result = await _client.put(url, body: body);
94-
return handleResponse<ParseObject>(
95-
this, result, ParseApiRQ.save, _debug, className);
96-
} on Exception catch (e) {
97-
return handleException(e, ParseApiRQ.save, _debug, className);
100+
return response;
101+
}
102+
}
103+
104+
Future<ParseResponse> _saveChildren(dynamic object) async {
105+
final Set<ParseObject> uniqueObjects = Set<ParseObject>();
106+
final Set<ParseFile> uniqueFiles = Set<ParseFile>();
107+
if (!_collectionDirtyChildren(object, uniqueObjects, uniqueFiles,
108+
Set<ParseObject>(), Set<ParseObject>())) {
109+
final ParseResponse response = ParseResponse();
110+
return response;
111+
}
112+
if (object is ParseObject) {
113+
uniqueObjects.remove(object);
114+
}
115+
for (ParseFile file in uniqueFiles) {
116+
final ParseResponse response = await file.save();
117+
if (!response.success) {
118+
return response;
119+
}
120+
}
121+
List<ParseObject> remaining = uniqueObjects.toList();
122+
final List<ParseObject> finished = List<ParseObject>();
123+
final ParseResponse totalResponse = ParseResponse()
124+
..success = true
125+
..results = List<dynamic>()
126+
..statusCode = 200;
127+
while (remaining.isNotEmpty) {
128+
/* Partition the objects into two sets: those that can be save immediately,
129+
and those that rely on other objects to be created first. */
130+
final List<ParseObject> current = List<ParseObject>();
131+
final List<ParseObject> nextBatch = List<ParseObject>();
132+
for (ParseObject object in remaining) {
133+
if (object._canbeSerialized(finished)) {
134+
current.add(object);
135+
} else {
136+
nextBatch.add(object);
137+
}
138+
}
139+
remaining = nextBatch;
140+
// TODO(yulingtianxia): lazy User
141+
/* Batch requests have currently a limit of 50 packaged requests per single request
142+
This splitting will split the overall array into segments of upto 50 requests
143+
and execute them concurrently with a wrapper task for all of them. */
144+
final List<List<ParseObject>> chunks = [];
145+
for (int i = 0; i < current.length; i += 50) {
146+
chunks.add(current.sublist(i, min(current.length, i + 50)));
147+
}
148+
149+
for (List<ParseObject> chunk in chunks) {
150+
final List<dynamic> requests = chunk.map<dynamic>((ParseObject obj) {
151+
return obj.getRequestJson(obj.objectId == null ? 'POST' : 'PUT');
152+
}).toList();
153+
final ParseResponse response = await batchRequest(requests, chunk);
154+
totalResponse.success &= response.success;
155+
if (response.success) {
156+
totalResponse.results.addAll(response.results);
157+
totalResponse.count += response.count;
158+
} else {
159+
// TODO(yulingtianxia): If there was an error, we want to roll forward the save changes before rethrowing.
160+
totalResponse.statusCode = response.statusCode;
161+
totalResponse.error = response.error;
162+
}
163+
}
164+
finished.addAll(current);
165+
}
166+
return totalResponse;
167+
}
168+
169+
dynamic getRequestJson(String method) {
170+
final Uri tempUri = Uri.parse(_client.data.serverUrl);
171+
final String parsePath = tempUri.path;
172+
final dynamic request = <String, dynamic>{
173+
'method': method,
174+
'path': '$parsePath$_path' + (objectId != null ? '/$objectId' : ''),
175+
'body': toJson(forApiRQ: true)
176+
};
177+
return request;
178+
}
179+
180+
bool _canbeSerialized(List<dynamic> aftersaving, {dynamic value}) {
181+
if (value != null) {
182+
if (value is ParseObject) {
183+
if (value.objectId == null && !aftersaving.contains(value)) {
184+
return false;
185+
}
186+
} else if (value is Map) {
187+
for (dynamic child in value.values) {
188+
if (!_canbeSerialized(aftersaving, value: child)) {
189+
return false;
190+
}
191+
}
192+
} else if (value is List) {
193+
for (dynamic child in value) {
194+
if (!_canbeSerialized(aftersaving, value: child)) {
195+
return false;
196+
}
197+
}
98198
}
199+
} else if (!_canbeSerialized(aftersaving, value: getObjectData())) {
200+
return false;
99201
}
202+
// TODO(yulingtianxia): handle ACL
203+
return true;
100204
}
101205

102-
/// Get the instance of ParseRelation class associated with the given key.
206+
bool _collectionDirtyChildren(dynamic object, Set<ParseObject> uniqueObjects,
207+
Set<ParseFile> uniqueFiles, Set<ParseObject> seen, Set<ParseObject> seenNew) {
208+
if (object is List) {
209+
for (dynamic child in object) {
210+
if (!_collectionDirtyChildren(
211+
child, uniqueObjects, uniqueFiles, seen, seenNew)) {
212+
return false;
213+
}
214+
}
215+
} else if (object is Map) {
216+
for (dynamic child in object.values) {
217+
if (!_collectionDirtyChildren(
218+
child, uniqueObjects, uniqueFiles, seen, seenNew)) {
219+
return false;
220+
}
221+
}
222+
} else if (object is ParseACL) {
223+
// TODO(yulingtianxia): handle ACL
224+
} else if (object is ParseFile) {
225+
if (object.url == null) {
226+
uniqueFiles.add(object);
227+
}
228+
} else if (object is ParseObject) {
229+
/* Check for cycles of new objects. Any such cycle means it will be
230+
impossible to save this collection of objects, so throw an exception. */
231+
if (object.objectId != null) {
232+
seenNew = Set<ParseObject>();
233+
} else {
234+
if (seenNew.contains(object)) {
235+
// TODO(yulingtianxia): throw an error?
236+
return false;
237+
}
238+
seenNew.add(object);
239+
}
240+
241+
/* Check for cycles of any object. If this occurs, then there's no
242+
problem, but we shouldn't recurse any deeper, because it would be
243+
an infinite recursion. */
244+
if (seen.contains(object)) {
245+
return true;
246+
}
247+
seen.add(object);
248+
249+
if (!_collectionDirtyChildren(
250+
object.getObjectData(), uniqueObjects, uniqueFiles, seen, seenNew)) {
251+
return false;
252+
}
253+
254+
// TODO(yulingtianxia): Check Dirty
255+
uniqueObjects.add(object);
256+
}
257+
return true;
258+
}
259+
260+
/// Get the instance of ParseRelation class associated with the given key.
103261
ParseRelation getRelation(String key) {
104262
return ParseRelation(parent: this, key: key);
105263
}
@@ -115,8 +273,8 @@ class ParseObject extends ParseBase implements ParseCloneable {
115273
}
116274

117275
/// Removes an element from an Array
118-
void setRemove(String key, dynamic values) {
119-
_arrayOperation('Remove', key, values);
276+
void setRemove(String key, dynamic value) {
277+
_arrayOperation('Remove', key, <dynamic>[value]);
120278
}
121279

122280
/// Remove multiple elements from an array of an object
@@ -159,8 +317,11 @@ class ParseObject extends ParseBase implements ParseCloneable {
159317
}
160318
}
161319

320+
void setAddUnique(String key, dynamic value) {
321+
_arrayOperation('AddUnique', key, <dynamic>[value]);
322+
}
162323
/// Add a multiple elements to an array of an object
163-
void setAddUnique(String key, List<dynamic> values) {
324+
void setAddAllUnique(String key, List<dynamic> values) {
164325
_arrayOperation('AddUnique', key, values);
165326
}
166327

@@ -175,8 +336,8 @@ class ParseObject extends ParseBase implements ParseCloneable {
175336
}
176337

177338
/// Add a single element to an array of an object
178-
void setAdd(String key, dynamic values) {
179-
_arrayOperation('Add', key, values);
339+
void setAdd(String key, dynamic value) {
340+
_arrayOperation('Add', key, <dynamic>[value]);
180341
}
181342

182343
void addRelation(String key, List<dynamic> values) {
@@ -208,6 +369,7 @@ class ParseObject extends ParseBase implements ParseCloneable {
208369

209370
/// Used in array Operations in save() method
210371
void _arrayOperation(String arrayAction, String key, List<dynamic> values) {
372+
// TODO(yulingtianxia): Array operations should be incremental. Merge add and remove operation.
211373
set<Map<String, dynamic>>(
212374
key, <String, dynamic>{'__op': arrayAction, 'objects': values});
213375
}

lib/src/objects/parse_relation.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,15 @@ class ParseRelation<T extends ParseObject> {
1919
if (object != null) {
2020
_targetClass = object.getClassName();
2121
_objects.add(object);
22-
_parent.addRelation(_key, _objects.map((T value) {
23-
return value.toPointer();
24-
}).toList());
22+
_parent.addRelation(_key, _objects.toList());
2523
}
2624
}
2725

2826
void remove(T object) {
2927
if (object != null) {
3028
_targetClass = object.getClassName();
3129
_objects.remove(object);
32-
_parent.removeRelation(_key, _objects.map((T value) {
33-
return value.toPointer();
34-
}).toList());
30+
_parent.removeRelation(_key, _objects.toList());
3531
}
3632
}
3733

0 commit comments

Comments
 (0)