Skip to content

Commit 456272f

Browse files
fix(aws-datastore): support temporal types in Flutter (#1001)
Resolves: #998
1 parent 82bc0d2 commit 456272f

File tree

4 files changed

+291
-13
lines changed

4 files changed

+291
-13
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"name": "Meeting",
3+
"pluralName": "Meetings",
4+
"authRules": [],
5+
"fields": {
6+
"date": {
7+
"name": "date",
8+
"javaClassForValue": "com.amplifyframework.core.model.temporal.Temporal$Date",
9+
"targetType": "AWSDate",
10+
"isRequired": false,
11+
"isArray": false,
12+
"isEnum": false,
13+
"isModel": false,
14+
"authRules": []
15+
},
16+
"dateTime": {
17+
"name": "dateTime",
18+
"javaClassForValue": "com.amplifyframework.core.model.temporal.Temporal$DateTime",
19+
"targetType": "AWSDateTime",
20+
"isRequired": false,
21+
"isArray": false,
22+
"isEnum": false,
23+
"isModel": false,
24+
"authRules": []
25+
},
26+
"id": {
27+
"name": "id",
28+
"javaClassForValue": "java.lang.String",
29+
"targetType": "ID",
30+
"isRequired": true,
31+
"isArray": false,
32+
"isEnum": false,
33+
"isModel": false,
34+
"authRules": []
35+
},
36+
"name": {
37+
"name": "name",
38+
"javaClassForValue": "java.lang.String",
39+
"targetType": "String",
40+
"isRequired": true,
41+
"isArray": false,
42+
"isEnum": false,
43+
"isModel": false,
44+
"authRules": []
45+
},
46+
"time": {
47+
"name": "time",
48+
"javaClassForValue": "com.amplifyframework.core.model.temporal.Temporal$Time",
49+
"targetType": "AWSTime",
50+
"isRequired": false,
51+
"isArray": false,
52+
"isEnum": false,
53+
"isModel": false,
54+
"authRules": []
55+
},
56+
"timestamp": {
57+
"name": "timestamp",
58+
"javaClassForValue": "com.amplifyframework.core.model.temporal.Temporal$Timestamp",
59+
"targetType": "AWSTimestamp",
60+
"isRequired": false,
61+
"isArray": false,
62+
"isEnum": false,
63+
"isModel": false,
64+
"authRules": []
65+
}
66+
},
67+
"associations": {},
68+
"indexes": {},
69+
"modelClass": "com.amplifyframework.datastore.appsync.SerializedModel"
70+
}
71+
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"run any business logic. A manual workaround exists, by running this cleanup script: " +
8383
"https://gist.github.com/jamesonwilliams/c76169676cb99c51d997ef0817eb9278#quikscript-to-clear-appsync-tables"
8484
)
85-
public final class HybridCloudSyncInstrumentationTest {
85+
public final class HybridAssociationSyncInstrumentationTest {
8686
private static final int TIMEOUT_SECONDS = 30;
8787

8888
private SchemaProvider schemaProvider;
@@ -98,6 +98,7 @@ public final class HybridCloudSyncInstrumentationTest {
9898
* test process with global state. We need an *instance* of the DataStore.
9999
* @throws AmplifyException On failure to configure Amplify, API/DataStore categories.
100100
*/
101+
@Ignore("It passes. Not automating due to operational concerns as noted in class-level @Ignore.")
101102
@Before
102103
public void setup() throws AmplifyException {
103104
Amplify.addPlugin(new AndroidLoggingPlugin(LogLevel.VERBOSE));
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.datastore;
17+
18+
import android.content.Context;
19+
import androidx.annotation.RawRes;
20+
21+
import com.amplifyframework.AmplifyException;
22+
import com.amplifyframework.api.ApiCategory;
23+
import com.amplifyframework.api.ApiException;
24+
import com.amplifyframework.api.aws.AWSApiPlugin;
25+
import com.amplifyframework.core.Amplify;
26+
import com.amplifyframework.core.AmplifyConfiguration;
27+
import com.amplifyframework.core.category.CategoryConfiguration;
28+
import com.amplifyframework.core.category.CategoryType;
29+
import com.amplifyframework.core.model.ModelSchema;
30+
import com.amplifyframework.core.model.temporal.Temporal;
31+
import com.amplifyframework.datastore.appsync.AppSyncClient;
32+
import com.amplifyframework.datastore.appsync.SerializedModel;
33+
import com.amplifyframework.datastore.appsync.SynchronousAppSync;
34+
import com.amplifyframework.hub.HubChannel;
35+
import com.amplifyframework.logging.AndroidLoggingPlugin;
36+
import com.amplifyframework.logging.LogLevel;
37+
import com.amplifyframework.testmodels.meeting.Meeting;
38+
import com.amplifyframework.testutils.HubAccumulator;
39+
import com.amplifyframework.testutils.Resources;
40+
import com.amplifyframework.testutils.sync.SynchronousApi;
41+
42+
import org.junit.Before;
43+
import org.junit.Ignore;
44+
import org.junit.Test;
45+
46+
import java.util.Date;
47+
import java.util.HashMap;
48+
import java.util.List;
49+
import java.util.Map;
50+
import java.util.NoSuchElementException;
51+
import java.util.concurrent.TimeUnit;
52+
53+
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
54+
import static com.amplifyframework.datastore.DataStoreHubEventFilters.publicationOf;
55+
import static com.amplifyframework.datastore.DataStoreHubEventFilters.receiptOf;
56+
import static org.junit.Assert.assertEquals;
57+
58+
/**
59+
* Tests that a model containing temporal types can be synced up and down
60+
* from the cloud, when used from a hybrid platform (Flutter).
61+
*/
62+
@Ignore(
63+
"Over time, this test will create a large DynamoDB table. Even if we delete the content " +
64+
"through the AppSyncClient utility, the database will have lots of tombstone'd rows. " +
65+
"These entries will be synced, the next time this test runs, and the DataStore initializes. " +
66+
"After several runs, that sync will grow large and timeout the test, before the test can " +
67+
"run any business logic. A manual workaround exists, by running this cleanup script: " +
68+
"https://gist.github.com/jamesonwilliams/c76169676cb99c51d997ef0817eb9278#quikscript-to-clear-appsync-tables"
69+
)
70+
public final class HybridTemporalSyncInstrumentationTest {
71+
private static final int TIMEOUT_SECONDS = 30;
72+
73+
private ModelSchema modelSchema;
74+
private SynchronousApi api;
75+
private SynchronousAppSync appSync;
76+
private SynchronousHybridBehaviors hybridBehaviors;
77+
78+
/**
79+
* DataStore is configured with a real AppSync endpoint. API and AppSync clients
80+
* are used to arrange/validate state before/after exercising the DataStore. The {@link Amplify}
81+
* facade is intentionally *not* used, since we don't want to pollute the instrumentation
82+
* test process with global state. We need an *instance* of the DataStore.
83+
* @throws AmplifyException On failure to configure Amplify, API/DataStore categories.
84+
*/
85+
@Ignore("It passes. Not automating due to operational concerns as noted in class-level @Ignore.")
86+
@Before
87+
public void setup() throws AmplifyException {
88+
Amplify.addPlugin(new AndroidLoggingPlugin(LogLevel.VERBOSE));
89+
90+
StrictMode.enable();
91+
Context context = getApplicationContext();
92+
@RawRes int configResourceId = Resources.getRawResourceId(context, "amplifyconfiguration");
93+
94+
// Setup an API
95+
CategoryConfiguration apiCategoryConfiguration =
96+
AmplifyConfiguration.fromConfigFile(context, configResourceId)
97+
.forCategoryType(CategoryType.API);
98+
ApiCategory apiCategory = new ApiCategory();
99+
apiCategory.addPlugin(new AWSApiPlugin());
100+
apiCategory.configure(apiCategoryConfiguration, context);
101+
102+
// To arrange and verify state, we need to access the supporting AppSync API
103+
api = SynchronousApi.delegatingTo(apiCategory);
104+
appSync = SynchronousAppSync.using(AppSyncClient.via(apiCategory));
105+
106+
SchemaProvider schemaProvider = SchemaLoader.loadFromAssetsDirectory("schemas/meeting");
107+
DataStoreCategory dataStoreCategory = DataStoreCategoryConfigurator.begin()
108+
.api(apiCategory)
109+
.clearDatabase(true)
110+
.context(context)
111+
.modelProvider(schemaProvider)
112+
.resourceId(configResourceId)
113+
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
114+
.finish();
115+
AWSDataStorePlugin plugin =
116+
(AWSDataStorePlugin) dataStoreCategory.getPlugin("awsDataStorePlugin");
117+
hybridBehaviors = SynchronousHybridBehaviors.delegatingTo(plugin);
118+
119+
// Get a handle to the Meeting model schema that we loaded into the DataStore in @Before.
120+
String modelName = Meeting.class.getSimpleName();
121+
modelSchema = schemaProvider.modelSchemas().get(modelName);
122+
}
123+
124+
/**
125+
* It is possible to dispatch a model that contain temporal types. After publishing
126+
* such a model to the cloud, we can query AppSync and find it there.
127+
* @throws ApiException on failure to communicate with AppSync API in verification phase of test
128+
*/
129+
@Ignore("It passes. Not automating due to operational concerns as noted in class-level @Ignore.")
130+
@Test
131+
public void temporalTypesAreSyncedUpToCloud() throws ApiException {
132+
// Prepare a SerializedModel that we will save to DataStore.
133+
Meeting meeting = createMeeting();
134+
Map<String, Object> sentData = toMap(meeting);
135+
SerializedModel sentModel = SerializedModel.builder()
136+
.serializedData(sentData)
137+
.modelSchema(modelSchema)
138+
.build();
139+
HubAccumulator publicationAccumulator =
140+
HubAccumulator.create(HubChannel.DATASTORE, publicationOf(sentModel), 1)
141+
.start();
142+
hybridBehaviors.save(sentModel);
143+
publicationAccumulator.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
144+
145+
// Retrieve the model from AppSync.
146+
Meeting remoteMeeting = api.get(Meeting.class, sentModel.getId());
147+
148+
// Inspect the fields of the data in AppSync, and prepare it into a map
149+
// that we can compare with what we sent. Are they the same? They should be.
150+
assertEquals(sentData, toMap(remoteMeeting));
151+
}
152+
153+
/**
154+
* It is possible to receive a model with temporal types over a subscription.
155+
* After receiving such a model, we can query it locally and inspect its fields.
156+
* The temporal values should be the same as what was saved remotely.
157+
* @throws DataStoreException on failure to interact with AppSync
158+
*/
159+
@Ignore("It passes. Not automating due to operational concerns as noted in class-level @Ignore.")
160+
@Test
161+
public void temporalTypesAreSyncedDownFromCloud() throws DataStoreException {
162+
// Save a meeting, remotely. Wait for it to show up locally.
163+
Meeting meeting = createMeeting();
164+
HubAccumulator receiptAccumulator =
165+
HubAccumulator.create(HubChannel.DATASTORE, receiptOf(meeting), 1)
166+
.start();
167+
appSync.create(meeting, modelSchema);
168+
receiptAccumulator.awaitFirst(TIMEOUT_SECONDS, TimeUnit.SECONDS);
169+
170+
// Look through the models that now exist locally.
171+
// One of them should be the thing we just saved to the backend.
172+
// Any others will have come in through the base sync in @Before.
173+
// When we find the clone, validate its fields.
174+
List<SerializedModel> clonedMeetings = hybridBehaviors.list(modelSchema.getName());
175+
SerializedModel clone = findById(clonedMeetings, meeting.getId());
176+
assertEquals(toMap(meeting), clone.getSerializedData());
177+
}
178+
179+
private static SerializedModel findById(List<SerializedModel> haystackModels, String needleId) {
180+
for (SerializedModel serializedModel : haystackModels) {
181+
if (serializedModel.getId().equals(needleId)) {
182+
return serializedModel;
183+
}
184+
}
185+
throw new NoSuchElementException("No model found with id = " + needleId);
186+
}
187+
188+
private static Meeting createMeeting() {
189+
return Meeting.builder()
190+
.name("A great meeting")
191+
.date(new Temporal.Date(new Date()))
192+
.dateTime(new Temporal.DateTime(new Date(), 0))
193+
.time(new Temporal.Time(new Date()))
194+
.timestamp(new Temporal.Timestamp(new Date()))
195+
.build();
196+
}
197+
198+
private static Map<String, Object> toMap(Meeting meeting) {
199+
Map<String, Object> map = new HashMap<>();
200+
map.put("id", meeting.getId());
201+
map.put("name", meeting.getName());
202+
map.put("date", meeting.getDate());
203+
map.put("dateTime", meeting.getDateTime());
204+
map.put("time", meeting.getTime());
205+
map.put("timestamp", meeting.getTimestamp());
206+
return map;
207+
}
208+
}

aws-datastore/src/main/java/com/amplifyframework/datastore/storage/sqlite/SQLiteModelFieldTypeConverter.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,23 @@ public static Object convertRawValueToTarget(
9898
boolean booleanValue = (boolean) value;
9999
return booleanValue ? 1L : 0L;
100100
case MODEL:
101-
if (value instanceof Map) {
102-
Map<?, ?> map = (Map<?, ?>) value;
103-
return (String) map.get("id");
104-
}
105-
return ((Model) value).getId();
101+
return value instanceof Map ? ((Map<?, ?>) value).get("id") : ((Model) value).getId();
106102
case ENUM:
107-
if (value instanceof String) {
108-
return (String) value;
109-
}
110-
return ((Enum<?>) value).name();
103+
return value instanceof String ? value : ((Enum<?>) value).name();
111104
case CUSTOM_TYPE:
112105
return gson.toJson(value);
113106
case DATE:
114-
return ((Temporal.Date) value).format();
107+
return value instanceof String ? value : ((Temporal.Date) value).format();
115108
case DATE_TIME:
116-
return ((Temporal.DateTime) value).format();
109+
return value instanceof String ? value : ((Temporal.DateTime) value).format();
117110
case TIME:
118-
return ((Temporal.Time) value).format();
111+
return value instanceof String ? value : ((Temporal.Time) value).format();
119112
case TIMESTAMP:
113+
if (value instanceof Integer) {
114+
return ((Integer) value).longValue();
115+
} else if (value instanceof Long) {
116+
return value;
117+
}
120118
return ((Temporal.Timestamp) value).getSecondsSinceEpoch();
121119
default:
122120
LOGGER.warn(String.format("Field of type %s is not supported. Fallback to null.", fieldType));

0 commit comments

Comments
 (0)