aws-amplify / aws-amplify/amplify-android

[DataStore] Invalid AWSPhone values cause silent failure when saving model to DynamoDB

Open
#1,462 4 comments 0 reactions 0 assignees View on GitHub
api bug datastore GraphQL API
Dominant language
Java
Stars
287
Forks
132
Avg merge
2d 2h
Merged PRs (30d)
45

Description

### Before opening, please confirm:

- [X] I have [searched for duplicate or closed issues](https://github.com/aws-amplify/amplify-android/issues?q=is%3Aissue+) and [discussions](https://github.com/aws-amplify/amplify-android/discussions).

### Language and Async Model

Java, Kotlin

### Amplify Categories

DataStore

### Gradle script dependencies

```groovy
// Put output below this line

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.amplifyframework:aws-api:1.24.1'
implementation 'com.amplifyframework:aws-datastore:1.24.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
```

### Environment information

```
# Put output below this line
------------------------------------------------------------
Gradle 6.7.1
------------------------------------------------------------

Build time: 2020-11-16 17:09:24 UTC
Revision: 2972ff02f3210d2ceed2f1ea880f026acfbab5c0

Kotlin: 1.3.72
Groovy: 2.5.12
Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM: 1.8.0_292 (AdoptOpenJDK 25.292-b10)
OS: Mac OS X 10.16 x86_64

```

### Please include any relevant guides or documentation you're referencing

_No response_

### Describe the bug

While using schemas with `AWSPhone` typed fields, if invalid phone numbers are provided, `save` API call succeeded, model saved into local DB, modal is not saved into DynamoDB due to DynamoDB value validation raised GraphQL error.

Expected behavior: `save` API call should return error with clear error message. Invalid data should not persisted in local DB.

Original issue: https://github.com/aws-amplify/amplify-flutter/issues/822#issuecomment-904919041
amplify-ios issue: https://github.com/aws-amplify/amplify-ios/issues/1393

Schema example
```graphql
type Person @model {
id: ID!
name: String!
phone: AWSPhone
}
```

### Reproduction steps (if applicable)

1. create a minimal app using amplify-android with above listed schema
2. Save model with invalid phone number
3. Observe

### Code Snippet

Java code example
```java
Person person = Person.builder()
.name("Test with phone")
.phone("1110004444")
.build();
Amplify.DataStore.save(person,
success -> Log.i("Tutorial", "Saved item: " + success.item().getPhone()),
error -> Log.e("Tutorial", "Could not save item to DataStore", error)
);
```

### Log output

```
// Put your logs below this line
I/Tutorial: Person {id=f153f9f6-f62d-4b65-8360-3f693cd1494e, name=Test with phone, department=null, phone=1110004444, createdAt=null, updatedAt=null}
I/Tutorial: Saved item: 1110004444
W/System.err: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
I/Tutorial: Record{id='d6ccd81c-051a-11ec-b8e4-4785562e7c29', containedModelId='f153f9f6-f62d-4b65-8360-3f693cd1494e', serializedMutationData='{"modelSchema":{"associations":{"department":{"associatedName":null,"associatedType":"Department","name":"BelongsTo","targetName":"departmentId"},"posts":{"associatedName":"author","associatedType":"Post","name":"HasMany","targetName":null}},"authRules":[],"fields":{"createdAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"createdAt","targetType":"AWSDateTime"},"department":{"authRules":[],"isArray":false,"isEnum":false,"isModel":true,"isReadOnly":false,"isRequired":false,"javaClassForValue":"com.amplifyframework.datastore.generated.model.Department","name":"department","targetType":"Department"},"id":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"id","targetType":"ID"},"name":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"name","targetType":"String"},"phone":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.lang.String","name":"phone","targetType":"AWSPhone"},"posts":{"authRules":[],"isArray":true,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.util.List","name":"posts","targetType":"Post"},"updatedAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"updatedAt","targetType":"AWSDateTime"}},"indexes":{"byDepartmentId":{"indexFieldNames":["departmentId"],"indexName":"byDepartmentId"}},"modelClass":"com.amplifyframework.datastore.generated.model.Person","name":"Person","pluralName":"People"},"mutatedItem":{"id":"f153f9f6-f62d-4b65-8360-3f693cd1494e","modelSchema":{"associations":{"department":{"associatedName":null,"associatedType":"Department","name":"BelongsTo","targetName":"departmentId"},"posts":{"associatedName":"author","associatedType":"Post","name":"HasMany","targetName":null}},"authRules":[],"fields":{"createdAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"createdAt","targetType":"AWSDateTime"},"department":{"authRules":[],"isArray":false,"isEnum":false,"isModel":true,"isReadOnly":false,"isRequired":false,"javaClassForValue":"com.amplifyframework.datastore.generated.model.Department","name":"department","targetType":"Department"},"id":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"id","targetType":"ID"},"name":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"name","targetType":"String"},"phone":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.lang.String","name":"phone","targetType":"AWSPhone"},"posts":{"authRules":[],"isArray":true,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.util.List","name":"posts","targetType":"Post"},"updatedAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"updatedAt","targetType":"AWSDateTime"}},"indexes":{"byDepartmentId":{"indexFieldNames":["departmentId"],"indexName":"byDepartmentId"}},"modelClass":"com.amplifyframework.datastore.generated.model.Person","name":"Person","pluralName":"People"},"serializedData":{"name":"Test w
I/amplify:aws-datastore: Successfully enqueued PendingMutation{mutatedItem=SerializedModel{id='f153f9f6-f62d-4b65-8360-3f693cd1494e', serializedData={name=Test with phone, createdAt=null, id=f153f9f6-f62d-4b65-8360-3f693cd1494e, phone=1110004444, updatedAt=null}, modelName=Person}, mutationType=CREATE, mutationId=d6ccd81c-051a-11ec-b8e4-4785562e7c29, predicate=MatchAllQueryPredicate}
I/Tutorial: Record{id='d6ccd81c-051a-11ec-b8e4-4785562e7c29', containedModelId='f153f9f6-f62d-4b65-8360-3f693cd1494e', serializedMutationData='{"modelSchema":{"associations":{"department":{"associatedName":null,"associatedType":"Department","name":"BelongsTo","targetName":"departmentId"},"posts":{"associatedName":"author","associatedType":"Post","name":"HasMany","targetName":null}},"authRules":[],"fields":{"createdAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"createdAt","targetType":"AWSDateTime"},"department":{"authRules":[],"isArray":false,"isEnum":false,"isModel":true,"isReadOnly":false,"isRequired":false,"javaClassForValue":"com.amplifyframework.datastore.generated.model.Department","name":"department","targetType":"Department"},"id":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"id","targetType":"ID"},"name":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"name","targetType":"String"},"phone":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.lang.String","name":"phone","targetType":"AWSPhone"},"posts":{"authRules":[],"isArray":true,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.util.List","name":"posts","targetType":"Post"},"updatedAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"updatedAt","targetType":"AWSDateTime"}},"indexes":{"byDepartmentId":{"indexFieldNames":["departmentId"],"indexName":"byDepartmentId"}},"modelClass":"com.amplifyframework.datastore.generated.model.Person","name":"Person","pluralName":"People"},"mutatedItem":{"id":"f153f9f6-f62d-4b65-8360-3f693cd1494e","modelSchema":{"associations":{"department":{"associatedName":null,"associatedType":"Department","name":"BelongsTo","targetName":"departmentId"},"posts":{"associatedName":"author","associatedType":"Post","name":"HasMany","targetName":null}},"authRules":[],"fields":{"createdAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"createdAt","targetType":"AWSDateTime"},"department":{"authRules":[],"isArray":false,"isEnum":false,"isModel":true,"isReadOnly":false,"isRequired":false,"javaClassForValue":"com.amplifyframework.datastore.generated.model.Department","name":"department","targetType":"Department"},"id":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"id","targetType":"ID"},"name":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":true,"javaClassForValue":"java.lang.String","name":"name","targetType":"String"},"phone":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.lang.String","name":"phone","targetType":"AWSPhone"},"posts":{"authRules":[],"isArray":true,"isEnum":false,"isModel":false,"isReadOnly":false,"isRequired":false,"javaClassForValue":"java.util.List","name":"posts","targetType":"Post"},"updatedAt":{"authRules":[],"isArray":false,"isEnum":false,"isModel":false,"isReadOnly":true,"isRequired":false,"javaClassForValue":"com.amplifyframework.core.model.temporal.Temporal$DateTime","name":"updatedAt","targetType":"AWSDateTime"}},"indexes":{"byDepartmentId":{"indexFieldNames":["departmentId"],"indexName":"byDepartmentId"}},"modelClass":"com.amplifyframework.datastore.generated.model.Person","name":"Person","pluralName":"People"},"serializedData":{"name":"Test w
I/amplify:aws-datastore: Successfully removed from mutations outboxPendingMutation{mutatedItem=SerializedModel{id='f153f9f6-f62d-4b65-8360-3f693cd1494e', serializedData={name=Test with phone, createdAt=null, id=f153f9f6-f62d-4b65-8360-3f693cd1494e, phone=1110004444, updatedAt=null}, modelName=Person}, mutationType=CREATE, mutationId=d6ccd81c-051a-11ec-b8e4-4785562e7c29, predicate=MatchAllQueryPredicate}

```

### amplifyconfiguration.json

_No response_

### GraphQL Schema

```graphql
// Put your schema below this line

```

### Additional information and screenshots

GraphQL request
```json
{
"query": "mutation CreatePerson($input: CreatePersonInput!) {\n createPerson(input: $input) {\n _deleted\n _lastChangedAt\n _version\n createdAt\n department {\n id\n }\n id\n name\n phone\n posts {\n items {\n id\n }\n nextToken\n startedAt\n }\n updatedAt\n }\n}\n",
"variables": {
"input": {
"name": "Test with phone",
"id": "f153f9f6-f62d-4b65-8360-3f693cd1494e",
"phone": "1110004444"
}
}
}
```

GraphQL response
```json
{
"data": null,
"errors": [{
"path": null,
"locations": [{
"line": 1,
"column": 23,
"sourceName": null
}],
"message": "Variable 'phone' has an invalid value. Unable to parse `1110004444` as a valid phone number."
}]
}
```

Contributor guide

Open the contributing guide

Research direction

Start at the Amplify.DataStore.save API and reproduce the AWSPhone case from the issue using the Person schema and invalid value 1110004444. Trace how the item is written to the local database and mutation outbox before DynamoDB validation, then verify that save reports a clear error and the invalid item is not persisted locally.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, aws, graphql, java, kotlin
Domain
backend, databases, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.