aws-amplify / aws-amplify/amplify-android
[DataStore] Invalid AWSPhone values cause silent failure when saving model to DynamoDB
- 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
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