use of com.amplifyframework.datastore.appsync.ModelMetadata in project amplify-android by aws-amplify.
the class AWSDataStorePluginTest method stopStopsSyncUntilNextInteraction.
/**
* Verify that when the stop method is called, the following happens
* - All remote synchronization processes are stopped
* - On the next interaction with the DataStore, the synchronization processes are restarted.
* @throws JSONException on failure to arrange plugin config
* @throws AmplifyException on failure to arrange API plugin via Amplify facade
*/
@Test
public void stopStopsSyncUntilNextInteraction() throws AmplifyException, JSONException {
ApiCategory mockApiCategory = mockApiCategoryWithGraphQlApi();
ApiPlugin<?> mockApiPlugin = mockApiCategory.getPlugin(MOCK_API_PLUGIN_NAME);
JSONObject dataStorePluginJson = new JSONObject().put("syncIntervalInMinutes", 60);
AWSDataStorePlugin awsDataStorePlugin = AWSDataStorePlugin.builder().modelProvider(modelProvider).apiCategory(mockApiCategory).build();
SynchronousDataStore synchronousDataStore = SynchronousDataStore.delegatingTo(awsDataStorePlugin);
awsDataStorePlugin.configure(dataStorePluginJson, context);
awsDataStorePlugin.initialize(context);
// Trick the DataStore since it's not getting initialized as part of the Amplify.initialize call chain
Amplify.Hub.publish(HubChannel.DATASTORE, HubEvent.create(InitializationStatus.SUCCEEDED));
// Setup objects
Person person1 = createPerson("Test", "Dummy I");
Person person2 = createPerson("Test", "Dummy II");
// Mock responses for person 1
doAnswer(invocation -> {
int indexOfResponseConsumer = 1;
Consumer<GraphQLResponse<ModelWithMetadata<Person>>> onResponse = invocation.getArgument(indexOfResponseConsumer);
ModelMetadata modelMetadata = new ModelMetadata(person1.getId(), false, 1, Temporal.Timestamp.now());
ModelWithMetadata<Person> modelWithMetadata = new ModelWithMetadata<>(person1, modelMetadata);
onResponse.accept(new GraphQLResponse<>(modelWithMetadata, Collections.emptyList()));
return mock(GraphQLOperation.class);
}).when(mockApiPlugin).mutate(any(), any(), any());
HubAccumulator apiInteractionObserver = HubAccumulator.create(HubChannel.DATASTORE, DataStoreChannelEventName.OUTBOX_MUTATION_PROCESSED, 1).start();
// Save person 1
synchronousDataStore.save(person1);
Person result1 = synchronousDataStore.get(Person.class, person1.getId());
assertEquals(person1, result1);
apiInteractionObserver.await();
verify(mockApiCategory).mutate(argThat(getMatcherFor(person1)), any(), any());
// Mock responses for person 2
doAnswer(invocation -> {
int indexOfResponseConsumer = 1;
Consumer<GraphQLResponse<ModelWithMetadata<Person>>> onResponse = invocation.getArgument(indexOfResponseConsumer);
ModelMetadata modelMetadata = new ModelMetadata(person2.getId(), false, 1, Temporal.Timestamp.now());
ModelWithMetadata<Person> modelWithMetadata = new ModelWithMetadata<>(person2, modelMetadata);
onResponse.accept(new GraphQLResponse<>(modelWithMetadata, Collections.emptyList()));
return mock(GraphQLOperation.class);
}).when(mockApiPlugin).mutate(any(), any(), any());
// Do the thing!
synchronousDataStore.stop();
assertRemoteSubscriptionsCancelled();
apiInteractionObserver = HubAccumulator.create(HubChannel.DATASTORE, DataStoreChannelEventName.OUTBOX_MUTATION_PROCESSED, 1).start();
HubAccumulator orchestratorInitObserver = HubAccumulator.create(HubChannel.DATASTORE, DataStoreChannelEventName.READY, 1).start();
// Interact with the DataStore after the stop
synchronousDataStore.save(person2);
// Verify person 2 was published to the cloud
apiInteractionObserver.await();
// Verify the orchestrator started back up and subscriptions are active.
orchestratorInitObserver.await();
assertRemoteSubscriptionsStarted();
// Verify person 1 and 2 are in the DataStore
List<Person> results = synchronousDataStore.list(Person.class);
assertEquals(Arrays.asList(person1, person2), results);
verify(mockApiCategory, atLeastOnce()).mutate(argThat(getMatcherFor(person2)), any(), any());
}
use of com.amplifyframework.datastore.appsync.ModelMetadata in project amplify-android by aws-amplify.
the class Merger method merge.
/**
* Merge an item back into the local store, using a default strategy.
* TODO: Change this method to return a Maybe, and remove the Consumer argument.
* @param modelWithMetadata A model, combined with metadata about it
* @param changeTypeConsumer A callback invoked when the merge method saves or deletes the model.
* @param <T> Type of model
* @return A completable operation to merge the model
*/
<T extends Model> Completable merge(ModelWithMetadata<T> modelWithMetadata, Consumer<StorageItemChange.Type> changeTypeConsumer) {
AtomicReference<Long> startTime = new AtomicReference<>();
return Completable.defer(() -> {
ModelMetadata metadata = modelWithMetadata.getSyncMetadata();
boolean isDelete = Boolean.TRUE.equals(metadata.isDeleted());
int incomingVersion = metadata.getVersion() == null ? -1 : metadata.getVersion();
T model = modelWithMetadata.getModel();
return versionRepository.findModelVersion(model).onErrorReturnItem(-1).filter(currentVersion -> currentVersion == -1 || incomingVersion > currentVersion).flatMapCompletable(shouldMerge -> {
Completable firstStep;
if (mutationOutbox.hasPendingMutation(model.getId())) {
LOG.info("Mutation outbox has pending mutation for " + model.getId() + ". Saving the metadata, but not model itself.");
firstStep = Completable.complete();
} else {
firstStep = (isDelete ? delete(model, changeTypeConsumer) : save(model, changeTypeConsumer));
}
return firstStep.andThen(save(metadata, NoOpConsumer.create()));
}).doOnComplete(() -> {
announceSuccessfulMerge(modelWithMetadata);
LOG.debug("Remote model update was sync'd down into local storage: " + modelWithMetadata);
}).onErrorComplete(failure -> {
if (!ErrorInspector.contains(failure, SQLiteConstraintException.class)) {
return false;
}
LOG.warn("Sync failed: foreign key constraint violation: " + modelWithMetadata, failure);
return true;
}).doOnError(failure -> LOG.warn("Failed to sync remote model into local storage: " + modelWithMetadata, failure));
}).doOnSubscribe(disposable -> startTime.set(System.currentTimeMillis())).doOnTerminate(() -> {
long duration = System.currentTimeMillis() - startTime.get();
LOG.verbose("Merged a single item in " + duration + " ms.");
});
}
use of com.amplifyframework.datastore.appsync.ModelMetadata in project amplify-android by aws-amplify.
the class VersionRepositoryTest method emitsErrorWhenMetadataHasNullVersion.
/**
* When you try to get the version for a model, and there is metadata for the model
* in the DataStore, BUT the version info is not populated, this should return an
* {@link DataStoreException}.
* @throws DataStoreException
* NOT EXPECTED. This happens on failure to arrange data before test action.
* The expected DataStoreException is communicated via callback, not thrown
* on the calling thread. It's a different thing than this.
* @throws InterruptedException If interrupted while awaiting terminal result in test observer
*/
@Test
public void emitsErrorWhenMetadataHasNullVersion() throws DataStoreException, InterruptedException {
// Arrange a model an metadata into the store, but the metadtaa doesn't contain a valid version
BlogOwner blogOwner = BlogOwner.builder().name("Jameson").build();
ModelMetadata metadata = new ModelMetadata(blogOwner.getModelName() + "|" + blogOwner.getId(), null, null, null);
storageAdapter.save(blogOwner, metadata);
// Act: try to get the version.
TestObserver<Integer> observer = versionRepository.findModelVersion(blogOwner).test();
assertTrue(observer.await(REASONABLE_WAIT_TIME, TimeUnit.MILLISECONDS));
// Assert: the single emitted a DataStoreException.
observer.assertError(error -> {
if (!(error instanceof DataStoreException)) {
return false;
}
String expectedMessage = String.format(Locale.US, "Metadata for item with id = %s had null version.", blogOwner.getId());
return expectedMessage.equals(error.getMessage());
});
}
use of com.amplifyframework.datastore.appsync.ModelMetadata in project amplify-android by aws-amplify.
the class ConflictResolver method resolve.
@NonNull
<T extends Model> Single<ModelWithMetadata<T>> resolve(@NonNull PendingMutation<T> pendingMutation, @NonNull AppSyncConflictUnhandledError<T> conflictUnhandledError) {
final DataStoreConflictHandler conflictHandler;
try {
conflictHandler = configurationProvider.getConfiguration().getConflictHandler();
} catch (DataStoreException badConfigurationProvider) {
return Single.error(badConfigurationProvider);
}
ModelWithMetadata<T> serverData = conflictUnhandledError.getServerVersion();
ModelMetadata metadata = serverData.getSyncMetadata();
T local = getMutatedModelFromSerializedModel(pendingMutation);
T remote = getServerModel(serverData, pendingMutation.getMutatedItem());
ConflictData<T> conflictData = ConflictData.create(local, remote);
return Single.<ConflictResolutionDecision<? extends Model>>create(emitter -> conflictHandler.onConflictDetected(conflictData, emitter::onSuccess)).flatMap(decision -> {
@SuppressWarnings("unchecked") ConflictResolutionDecision<T> typedDecision = (ConflictResolutionDecision<T>) decision;
return resolveModelAndMetadata(conflictData, metadata, typedDecision);
});
}
Aggregations