Skip to content

Commit da6df07

Browse files
fix: associated Flutter models can sync to cloud (#988)
Version 1.6.3 of Amplify fixed an issue where associated/connected models failed to sync down from AppSync, when being used with Flutter-style ModelSchema. Unfortunately, 1.6.3 stopped short of supporting associated models when syncing *up* to AppSync, from the client. This current change expands the HybridCloudSyncInstrumentationTest, to demonstrate successful sync up and down from AppSync. To support this, request-forming logic is extracted from the ModelSchema, and split accross the AppSyncRequestFactory (for DataStore) and the AppSyncGraphQLRequestFactory (for API category). The DataStore flavor of the factory is expanded to properly build requests when dealing with associations in Flutter-style SerializedModels.
1 parent baa798b commit da6df07

File tree

8 files changed

+441
-244
lines changed

8 files changed

+441
-244
lines changed

aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515

1616
package com.amplifyframework.api.aws;
1717

18+
import androidx.annotation.NonNull;
19+
1820
import com.amplifyframework.AmplifyException;
1921
import com.amplifyframework.api.graphql.GraphQLRequest;
2022
import com.amplifyframework.api.graphql.MutationType;
2123
import com.amplifyframework.api.graphql.PaginatedResult;
2224
import com.amplifyframework.api.graphql.QueryType;
2325
import com.amplifyframework.api.graphql.SubscriptionType;
26+
import com.amplifyframework.core.model.AuthRule;
27+
import com.amplifyframework.core.model.AuthStrategy;
2428
import com.amplifyframework.core.model.Model;
29+
import com.amplifyframework.core.model.ModelAssociation;
30+
import com.amplifyframework.core.model.ModelField;
2531
import com.amplifyframework.core.model.ModelSchema;
2632
import com.amplifyframework.core.model.query.predicate.BeginsWithQueryOperator;
2733
import com.amplifyframework.core.model.query.predicate.BetweenQueryOperator;
@@ -40,13 +46,16 @@
4046
import com.amplifyframework.util.Casing;
4147
import com.amplifyframework.util.TypeMaker;
4248

49+
import java.lang.reflect.Field;
4350
import java.lang.reflect.Type;
4451
import java.util.ArrayList;
4552
import java.util.Arrays;
4653
import java.util.Collections;
54+
import java.util.HashMap;
4755
import java.util.List;
4856
import java.util.Locale;
4957
import java.util.Map;
58+
import java.util.Objects;
5059

5160
/**
5261
* Converts provided model or class type into a request container
@@ -198,7 +207,7 @@ public static <R, T extends Model> GraphQLRequest<R> buildMutation(
198207
if (MutationType.DELETE.equals(type)) {
199208
builder.variable("input", inputType, Collections.singletonMap("id", model.getId()));
200209
} else {
201-
builder.variable("input", inputType, schema.getMapOfFieldNameAndValues(model));
210+
builder.variable("input", inputType, getMapOfFieldNameAndValues(schema, model));
202211
}
203212

204213
if (!QueryPredicates.all().equals(predicate)) {
@@ -339,4 +348,52 @@ private static Object appSyncOpValue(QueryOperator<?> qOp) {
339348
);
340349
}
341350
}
351+
352+
private static Map<String, Object> getMapOfFieldNameAndValues(
353+
@NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException {
354+
if (!instance.getClass().getSimpleName().equals(schema.getName())) {
355+
throw new AmplifyException(
356+
"The object provided is not an instance of " + schema.getName() + ".",
357+
"Please provide an instance of " + schema.getName() + " that matches the schema type."
358+
);
359+
}
360+
final Map<String, Object> result = new HashMap<>();
361+
for (ModelField modelField : schema.getFields().values()) {
362+
String fieldName = modelField.getName();
363+
try {
364+
Field privateField = instance.getClass().getDeclaredField(modelField.getName());
365+
privateField.setAccessible(true);
366+
Object fieldValue = privateField.get(instance);
367+
final ModelAssociation association = schema.getAssociations().get(fieldName);
368+
if (association == null) {
369+
result.put(fieldName, fieldValue);
370+
} else if (association.isOwner()) {
371+
Model target = (Model) Objects.requireNonNull(fieldValue);
372+
result.put(association.getTargetName(), target.getId());
373+
}
374+
// Ignore if field is associated, but is not a "belongsTo" relationship
375+
} catch (Exception exception) {
376+
throw new AmplifyException(
377+
"An invalid field was provided. " + fieldName + " is not present in " + schema.getName(),
378+
exception,
379+
"Check if this model schema is a correct representation of the fields in the provided Object");
380+
}
381+
}
382+
383+
/*
384+
* If the owner field exists on the model, and the value is null, it should be omitted when performing a
385+
* mutation because the AppSync server will automatically populate it using the authentication token provided
386+
* in the request header. The logic below filters out the owner field if null for this scenario.
387+
*/
388+
for (AuthRule authRule : schema.getAuthRules()) {
389+
if (AuthStrategy.OWNER.equals(authRule.getAuthStrategy())) {
390+
String ownerField = authRule.getOwnerFieldOrDefault();
391+
if (result.containsKey(ownerField) && result.get(ownerField) == null) {
392+
result.remove(ownerField);
393+
}
394+
}
395+
}
396+
397+
return result;
398+
}
342399
}

aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactoryTest.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515

1616
package com.amplifyframework.api.aws;
1717

18+
import androidx.annotation.NonNull;
19+
1820
import com.amplifyframework.api.graphql.GraphQLRequest;
1921
import com.amplifyframework.api.graphql.MutationType;
2022
import com.amplifyframework.api.graphql.SubscriptionType;
23+
import com.amplifyframework.core.model.AuthStrategy;
24+
import com.amplifyframework.core.model.Model;
25+
import com.amplifyframework.core.model.annotations.AuthRule;
26+
import com.amplifyframework.core.model.annotations.ModelConfig;
2127
import com.amplifyframework.core.model.query.predicate.QueryPredicates;
2228
import com.amplifyframework.core.model.temporal.Temporal;
2329
import com.amplifyframework.testmodels.meeting.Meeting;
@@ -32,8 +38,12 @@
3238
import org.skyscreamer.jsonassert.JSONAssert;
3339

3440
import java.util.Date;
41+
import java.util.HashMap;
42+
import java.util.Map;
3543
import java.util.concurrent.TimeUnit;
3644

45+
import static org.junit.Assert.assertEquals;
46+
3747
/**
3848
* Tests the {@link AppSyncGraphQLRequestFactory}.
3949
*/
@@ -156,4 +166,74 @@ public void validateDateSerializer() throws JSONException {
156166
JSONAssert.assertEquals(Resources.readAsString("create-meeting1.txt"),
157167
requestToCreateMeeting1.getContent(), true);
158168
}
169+
170+
/**
171+
* Verify that the owner field is removed if the value is null.
172+
*/
173+
@Test
174+
public void ownerFieldIsRemovedIfNull() {
175+
// Expect
176+
Map<String, Object> expected = new HashMap<>();
177+
expected.put("id", "111");
178+
expected.put("description", "Mop the floor");
179+
180+
// Act
181+
Todo todo = new Todo("111", "Mop the floor", null);
182+
@SuppressWarnings("unchecked")
183+
Map<String, Object> actual = (Map<String, Object>)
184+
AppSyncGraphQLRequestFactory.buildMutation(todo, QueryPredicates.all(), MutationType.CREATE)
185+
.getVariables()
186+
.get("input");
187+
188+
// Assert
189+
assertEquals(expected, actual);
190+
}
191+
192+
/**
193+
* Verify that the owner field is NOT removed if the value is set..
194+
*/
195+
@Test
196+
public void ownerFieldIsNotRemovedIfSet() {
197+
// Expect
198+
Map<String, Object> expected = new HashMap<>();
199+
expected.put("id", "111");
200+
expected.put("description", "Mop the floor");
201+
expected.put("owner", "johndoe");
202+
203+
// Act
204+
Todo todo = new Todo("111", "Mop the floor", "johndoe");
205+
@SuppressWarnings("unchecked")
206+
Map<String, Object> actual = (Map<String, Object>)
207+
AppSyncGraphQLRequestFactory.buildMutation(todo, QueryPredicates.all(), MutationType.CREATE)
208+
.getVariables()
209+
.get("input");
210+
211+
// Assert
212+
assertEquals(expected, actual);
213+
}
214+
215+
@ModelConfig(authRules = { @AuthRule(allow = AuthStrategy.OWNER) })
216+
static final class Todo implements Model {
217+
@com.amplifyframework.core.model.annotations.ModelField(targetType = "ID", isRequired = true)
218+
private final String id;
219+
220+
@com.amplifyframework.core.model.annotations.ModelField(isRequired = true)
221+
private final String description;
222+
223+
@com.amplifyframework.core.model.annotations.ModelField
224+
private final String owner;
225+
226+
@SuppressWarnings("ParameterName") // checkstyle wants variable names to be >2 chars, but id is only 2.
227+
Todo(String id, String description, String owner) {
228+
this.id = id;
229+
this.description = description;
230+
this.owner = owner;
231+
}
232+
233+
@NonNull
234+
@Override
235+
public String getId() {
236+
return "111";
237+
}
238+
}
159239
}

0 commit comments

Comments
 (0)