Skip to content

Commit 50b63dc

Browse files
fix(datastore,api): Update and delete mutations now work when custom primary key is defined (#1292)
1 parent 867ca95 commit 50b63dc

File tree

28 files changed

+1236
-138
lines changed

28 files changed

+1236
-138
lines changed

aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/ModelConverter.java

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,33 @@ public static <T extends Model> Map<String, Object> toMap(T instance, ModelSchem
4545
final Map<String, Object> result = new HashMap<>();
4646
for (ModelField modelField : schema.getFields().values()) {
4747
String fieldName = modelField.getName();
48-
try {
49-
50-
final ModelAssociation association = schema.getAssociations().get(fieldName);
51-
if (association == null) {
52-
if (instance instanceof SerializedModel
53-
&& !((SerializedModel) instance).getSerializedData().containsKey(modelField.getName())) {
54-
// Skip fields that are not set, so that they are not set to null in the request.
55-
continue;
56-
}
57-
result.put(fieldName, extractFieldValue(modelField, instance));
58-
} else if (association.isOwner()) {
59-
Object associateId = extractAssociateId(modelField, instance);
60-
if (associateId == null) {
61-
// Skip fields that are not set, so that they are not set to null in the request.
62-
continue;
63-
}
64-
result.put(fieldName, SerializedModel.builder()
65-
.serializedData(Collections.singletonMap("id", associateId))
66-
.modelSchema(null)
67-
.build());
48+
final ModelAssociation association = schema.getAssociations().get(fieldName);
49+
if (association == null) {
50+
if (instance instanceof SerializedModel
51+
&& !((SerializedModel) instance).getSerializedData().containsKey(modelField.getName())) {
52+
// Skip fields that are not set, so that they are not set to null in the request.
53+
continue;
54+
}
55+
result.put(fieldName, extractFieldValue(modelField.getName(), instance, schema));
56+
} else if (association.isOwner()) {
57+
Object associateId = extractAssociateId(modelField, instance, schema);
58+
if (associateId == null) {
59+
// Skip fields that are not set, so that they are not set to null in the request.
60+
continue;
6861
}
69-
// Ignore if field is associated, but is not a "belongsTo" relationship
70-
} catch (Exception exception) {
71-
throw new AmplifyException(
72-
"An invalid field was provided. " + fieldName + " is not present in " + schema.getName(),
73-
exception,
74-
"Check if this model schema is a correct representation of the fields in the provided Object");
62+
result.put(fieldName, SerializedModel.builder()
63+
.serializedData(Collections.singletonMap("id", associateId))
64+
.modelSchema(null)
65+
.build());
7566
}
67+
// Ignore if field is associated, but is not a "belongsTo" relationship
7668
}
7769
return result;
7870
}
7971

80-
private static Object extractAssociateId(ModelField modelField, Model instance)
81-
throws NoSuchFieldException, IllegalAccessException {
82-
final Object fieldValue = extractFieldValue(modelField, instance);
72+
private static Object extractAssociateId(ModelField modelField, Model instance, ModelSchema schema)
73+
throws AmplifyException {
74+
final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema);
8375
if (modelField.isModel() && fieldValue instanceof Model) {
8476
return ((Model) fieldValue).getId();
8577
} else if (modelField.isModel() && fieldValue instanceof Map) {
@@ -91,15 +83,22 @@ private static Object extractAssociateId(ModelField modelField, Model instance)
9183
}
9284
}
9385

94-
private static Object extractFieldValue(ModelField modelField, Model instance)
95-
throws NoSuchFieldException, IllegalAccessException {
86+
private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema)
87+
throws AmplifyException {
9688
if (instance instanceof SerializedModel) {
9789
SerializedModel serializedModel = (SerializedModel) instance;
9890
Map<String, Object> serializedData = serializedModel.getSerializedData();
99-
return serializedData.get(modelField.getName());
91+
return serializedData.get(fieldName);
92+
}
93+
try {
94+
Field privateField = instance.getClass().getDeclaredField(fieldName);
95+
privateField.setAccessible(true);
96+
return privateField.get(instance);
97+
} catch (Exception exception) {
98+
throw new AmplifyException(
99+
"An invalid field was provided. " + fieldName + " is not present in " + schema.getName(),
100+
exception,
101+
"Check if this model schema is a correct representation of the fields in the provided Object");
100102
}
101-
Field privateField = instance.getClass().getDeclaredField(modelField.getName());
102-
privateField.setAccessible(true);
103-
return privateField.get(instance);
104103
}
105104
}

aws-api-appsync/src/main/java/com/amplifyframework/datastore/appsync/SerializedModel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.amplifyframework.util.Immutable;
2727

2828
import java.util.HashMap;
29+
import java.util.List;
2930
import java.util.Map;
3031
import java.util.Objects;
3132

@@ -80,7 +81,8 @@ public static <T extends Model> SerializedModel difference(T updated, T original
8081
Map<String, Object> originalMap = ModelConverter.toMap(original, modelSchema);
8182
Map<String, Object> patchMap = new HashMap<>();
8283
for (String key : updatedMap.keySet()) {
83-
if ("id".equals(key) || !ObjectsCompat.equals(originalMap.get(key), updatedMap.get(key))) {
84+
List<String> primaryIndexFields = modelSchema.getPrimaryIndexFields();
85+
if (primaryIndexFields.contains(key) || !ObjectsCompat.equals(originalMap.get(key), updatedMap.get(key))) {
8486
patchMap.put(key, updatedMap.get(key));
8587
}
8688
}

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

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public static <R, T extends Model> GraphQLRequest<R> buildMutation(
206206
"Input!"; // CreateTodoInput
207207

208208
if (MutationType.DELETE.equals(type)) {
209-
builder.variable("input", inputType, Collections.singletonMap("id", model.getId()));
209+
builder.variable("input", inputType, getDeleteMutationInputMap(schema, model));
210210
} else {
211211
builder.variable("input", inputType, getMapOfFieldNameAndValues(schema, model));
212212
}
@@ -352,6 +352,15 @@ private static Object appSyncOpValue(QueryOperator<?> qOp) {
352352
}
353353
}
354354

355+
private static Map<String, Object> getDeleteMutationInputMap(
356+
@NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException {
357+
final Map<String, Object> input = new HashMap<>();
358+
for (String fieldName : schema.getPrimaryIndexFields()) {
359+
input.put(fieldName, extractFieldValue(fieldName, instance, schema));
360+
}
361+
return input;
362+
}
363+
355364
private static Map<String, Object> getMapOfFieldNameAndValues(
356365
@NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException {
357366
if (!instance.getClass().getSimpleName().equals(schema.getName())) {
@@ -367,24 +376,15 @@ private static Map<String, Object> getMapOfFieldNameAndValues(
367376
continue;
368377
}
369378
String fieldName = modelField.getName();
370-
try {
371-
Field privateField = instance.getClass().getDeclaredField(modelField.getName());
372-
privateField.setAccessible(true);
373-
Object fieldValue = privateField.get(instance);
374-
final ModelAssociation association = schema.getAssociations().get(fieldName);
375-
if (association == null) {
376-
result.put(fieldName, fieldValue);
377-
} else if (association.isOwner()) {
378-
Model target = (Model) Objects.requireNonNull(fieldValue);
379-
result.put(association.getTargetName(), target.getId());
380-
}
381-
// Ignore if field is associated, but is not a "belongsTo" relationship
382-
} catch (Exception exception) {
383-
throw new AmplifyException(
384-
"An invalid field was provided. " + fieldName + " is not present in " + schema.getName(),
385-
exception,
386-
"Check if this model schema is a correct representation of the fields in the provided Object");
379+
Object fieldValue = extractFieldValue(fieldName, instance, schema);
380+
final ModelAssociation association = schema.getAssociations().get(fieldName);
381+
if (association == null) {
382+
result.put(fieldName, fieldValue);
383+
} else if (association.isOwner()) {
384+
Model target = (Model) Objects.requireNonNull(fieldValue);
385+
result.put(association.getTargetName(), target.getId());
387386
}
387+
// Ignore if field is associated, but is not a "belongsTo" relationship
388388
}
389389

390390
/*
@@ -403,4 +403,18 @@ private static Map<String, Object> getMapOfFieldNameAndValues(
403403

404404
return result;
405405
}
406+
407+
private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema)
408+
throws AmplifyException {
409+
try {
410+
Field privateField = instance.getClass().getDeclaredField(fieldName);
411+
privateField.setAccessible(true);
412+
return privateField.get(instance);
413+
} catch (Exception exception) {
414+
throw new AmplifyException(
415+
"An invalid field was provided. " + fieldName + " is not present in " + schema.getName(),
416+
exception,
417+
"Check if this model schema is a correct representation of the fields in the provided Object");
418+
}
419+
}
406420
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import androidx.annotation.NonNull;
1919

20+
import com.amplifyframework.AmplifyException;
2021
import com.amplifyframework.api.graphql.GraphQLRequest;
2122
import com.amplifyframework.api.graphql.MutationType;
2223
import com.amplifyframework.api.graphql.SubscriptionType;
@@ -26,6 +27,9 @@
2627
import com.amplifyframework.core.model.annotations.ModelConfig;
2728
import com.amplifyframework.core.model.query.predicate.QueryPredicates;
2829
import com.amplifyframework.core.model.temporal.Temporal;
30+
import com.amplifyframework.datastore.DataStoreException;
31+
import com.amplifyframework.testmodels.ecommerce.Item;
32+
import com.amplifyframework.testmodels.ecommerce.Status;
2933
import com.amplifyframework.testmodels.meeting.Meeting;
3034
import com.amplifyframework.testmodels.personcar.MaritalStatus;
3135
import com.amplifyframework.testmodels.personcar.Person;
@@ -122,6 +126,27 @@ public void buildDeleteMutationFromPredicate() throws JSONException {
122126
);
123127
}
124128

129+
/**
130+
* Checks that we're getting the expected output for a delete mutation for an object with a custom primary key.
131+
* @throws DataStoreException If the output does not match.
132+
* @throws AmplifyException On failure to parse ModelSchema from model class
133+
* @throws JSONException from JSONAssert.assertEquals.
134+
*/
135+
@Test
136+
public void validateDeleteWithCustomPrimaryKey() throws AmplifyException, JSONException {
137+
final Item item = Item.builder()
138+
.orderId("123a7asa")
139+
.status(Status.IN_TRANSIT)
140+
.createdAt(new Temporal.DateTime("2021-04-20T15:20:32.651Z"))
141+
.name("Gummy Bears")
142+
.build();
143+
JSONAssert.assertEquals(
144+
Resources.readAsString("delete-item.txt"),
145+
AppSyncGraphQLRequestFactory.buildMutation(item, QueryPredicates.all(), MutationType.DELETE).getContent(),
146+
true
147+
);
148+
}
149+
125150
/**
126151
* Validates construction of an update mutation query from a Person instance, a predicate.
127152
* @throws JSONException from JSONAssert.assertEquals
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"query": "mutation DeleteItem($input: DeleteItemInput!) {\n deleteItem(input: $input) {
3+
createdAt
4+
id
5+
name
6+
orderId
7+
status
8+
}
9+
}
10+
",
11+
"variables": {
12+
"input" : {
13+
"createdAt":"2021-04-20T15:20:32.651Z",
14+
"orderId":"123a7asa",
15+
"status":"IN_TRANSIT"
16+
}
17+
}
18+
}

aws-datastore/src/androidTest/java/com/amplifyframework/datastore/AppSyncClientInstrumentationTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public void testAllOperations() throws AmplifyException, NoSuchFieldException, I
198198
assertTrue(updatedBlogLastChangedAt.getSecondsSinceEpoch() > updateBlogStartTimeSeconds);
199199

200200
// Delete one of the posts
201-
ModelWithMetadata<Post> post1DeleteResult = delete(postSchema, post1.getId(), 1);
201+
ModelWithMetadata<Post> post1DeleteResult = delete(post1, postSchema, 1);
202202
assertEquals(
203203
post1.copyOfBuilder()
204204
.blog(Blog.justId(blog.getId()))
@@ -209,7 +209,7 @@ public void testAllOperations() throws AmplifyException, NoSuchFieldException, I
209209
assertEquals(Boolean.TRUE, isDeleted);
210210

211211
// Try to delete a post with a bad version number
212-
List<GraphQLResponse.Error> post2DeleteErrors = deleteExpectingResponseErrors(postSchema, post2.getId(), 0);
212+
List<GraphQLResponse.Error> post2DeleteErrors = deleteExpectingResponseErrors(post2, postSchema, 0);
213213
assertEquals("Conflict resolver rejects mutation.", post2DeleteErrors.get(0).getMessage());
214214

215215
// Run sync on Blogs
@@ -268,43 +268,43 @@ private <T extends Model> ModelWithMetadata<T> update(
268268

269269
/**
270270
* Deletes an instance of a model.
271+
* @param model The the model instance to delete
271272
* @param schema The schema of model being deleted
272-
* @param modelId The ID of the model instance to delete
273273
* @param version The version of the model being deleted as understood by client
274274
* @param <T> Type of model being deleted
275275
* @return Model hat was deleted from endpoint, coupled with metadata about the deletion
276276
* @throws DataStoreException If API delete call fails to render any response from AppSync endpoint
277277
*/
278278
@NonNull
279279
private <T extends Model> ModelWithMetadata<T> delete(
280-
@NonNull ModelSchema schema, String modelId, int version)
280+
@NonNull T model, @NonNull ModelSchema schema, int version)
281281
throws DataStoreException {
282-
return delete(schema, modelId, version, QueryPredicates.all());
282+
return delete(model, schema, version, QueryPredicates.all());
283283
}
284284

285285
@NonNull
286286
private <T extends Model> ModelWithMetadata<T> delete(
287-
@NonNull ModelSchema schema, String modelId, int version, QueryPredicate predicate)
287+
@NonNull T model, @NonNull ModelSchema schema, int version, QueryPredicate predicate)
288288
throws DataStoreException {
289289
return awaitResponseData((onResult, onError) ->
290-
api.delete(schema, modelId, version, predicate, onResult, onError));
290+
api.delete(model, schema, version, predicate, onResult, onError));
291291
}
292292

293293
/**
294294
* Try to delete an item, but expect it to error.
295295
* Return the errors that were contained in the GraphQLResponse returned from endpoint.
296+
* @param model item for which delete is attempted
296297
* @param schema Schema of item for which a delete is attempted
297-
* @param modelId ID of item for which delete is attempted
298298
* @param version Version of item for which deleted is attempted
299299
* @param <T> Type of item for which delete is attempted
300300
* @return List of GraphQLResponse.Error which explain why delete failed
301301
* @throws DataStoreException If API delete call fails to render any response from AppSync endpoint
302302
*/
303303
private <T extends Model> List<GraphQLResponse.Error> deleteExpectingResponseErrors(
304-
@NonNull ModelSchema schema, String modelId, int version) throws DataStoreException {
304+
@NonNull T model, @NonNull ModelSchema schema, int version) throws DataStoreException {
305305
return awaitResponseErrors((Consumer<GraphQLResponse<ModelWithMetadata<T>>> onResult,
306306
Consumer<DataStoreException> onError) ->
307-
api.delete(schema, modelId, version, onResult, onError)
307+
api.delete(model, schema, version, onResult, onError)
308308
);
309309
}
310310

aws-datastore/src/androidTest/java/com/amplifyframework/datastore/appsync/SynchronousAppSync.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,23 +125,23 @@ public <T extends Model> GraphQLResponse<ModelWithMetadata<T>> update(
125125

126126
/**
127127
* Uses Amplify API to make a mutation which will only apply if the version sent matches the server version.
128+
* @param object model to delete
128129
* @param schema The schema of the Model we are deleting
129-
* @param objectId Id of the object to delete
130130
* @param version The version of the model we have
131131
* @param <T> The type of data in the response. Must extend Model.
132132
* @return Response data from AppSync.
133133
* @throws DataStoreException On failure to obtain response data
134134
*/
135135
@NonNull
136136
<T extends Model> GraphQLResponse<ModelWithMetadata<T>> delete(
137-
@NonNull ModelSchema schema, @NonNull String objectId, @NonNull Integer version) throws DataStoreException {
138-
return delete(schema, objectId, version, QueryPredicates.all());
137+
@NonNull T object, @NonNull ModelSchema schema, @NonNull Integer version) throws DataStoreException {
138+
return delete(object, schema, version, QueryPredicates.all());
139139
}
140140

141141
/**
142142
* Uses Amplify API to make a mutation which will only apply if the version sent matches the server version.
143+
* @param object model to delete
143144
* @param schema The schema of the Model we are deleting
144-
* @param objectId Id of the object to delete
145145
* @param version The version of the model we have
146146
* @param predicate The condition to be applied to the delete.
147147
* @param <T> The type of data in the response. Must extend Model.
@@ -150,12 +150,12 @@ <T extends Model> GraphQLResponse<ModelWithMetadata<T>> delete(
150150
*/
151151
@NonNull
152152
<T extends Model> GraphQLResponse<ModelWithMetadata<T>> delete(
153+
@NonNull T object,
153154
@NonNull ModelSchema schema,
154-
@NonNull String objectId,
155155
@NonNull Integer version,
156156
@NonNull QueryPredicate predicate) throws DataStoreException {
157157
return Await.<GraphQLResponse<ModelWithMetadata<T>>, DataStoreException>result((onResult, onError) ->
158-
appSync.delete(schema, objectId, version, predicate, onResult, onError)
158+
appSync.delete(object, schema, version, predicate, onResult, onError)
159159
);
160160
}
161161

0 commit comments

Comments
 (0)