From 55936a2f242609f559c4002ae556d6d9b95ca4c5 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 6 Dec 2023 12:32:21 -0800 Subject: [PATCH] fix(datastore): sync pending mutation events with latest synced metadata --- .../Model/Internal/Model+Codable.swift | 1 + .../Internal/Schema/ModelValueConverter.swift | 4 +- ...atabaseAdapter+MutationEventIngester.swift | 18 - .../OutgoingMutationQueue.swift | 10 +- ...ocessMutationErrorFromCloudOperation.swift | 4 +- .../SyncMutationToCloudOperation.swift | 11 +- .../ReconcileAndLocalSaveOperation.swift | 144 ++++-- .../RemoteSyncReconciler.swift | 15 + .../Support/MutationEvent+Extensions.swift | 103 ----- .../DataStoreEndToEndTests.swift | 2 +- .../Models/SQLModelValueConverterTests.swift | 2 +- ...MutationErrorFromCloudOperationTests.swift | 2 +- .../SyncMutationToCloudOperationTests.swift | 16 +- .../MutationEventExtensionsTests.swift | 415 ------------------ .../project.pbxproj | 10 +- AmplifyPlugins/DataStore/Podfile | 1 - AmplifyPlugins/DataStore/Podfile.lock | 46 +- .../CoreTests/Model+CodableTests.swift | 4 +- Podfile.lock | 6 +- 19 files changed, 190 insertions(+), 624 deletions(-) delete mode 100644 AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/Support/MutationEvent+Extensions.swift delete mode 100644 AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/MutationEventExtensionsTests.swift diff --git a/Amplify/Categories/DataStore/Model/Internal/Model+Codable.swift b/Amplify/Categories/DataStore/Model/Internal/Model+Codable.swift index 1fd055d95b..571c36d1b7 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Model+Codable.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Model+Codable.swift @@ -76,6 +76,7 @@ extension Model where Self: Codable { resolvedEncoder = encoder } else { resolvedEncoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy) + resolvedEncoder.outputFormatting = .sortedKeys } let data = try resolvedEncoder.encode(self) diff --git a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelValueConverter.swift b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelValueConverter.swift index e2ad3fb0f6..de961101ec 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelValueConverter.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelValueConverter.swift @@ -44,7 +44,9 @@ extension ModelValueConverter { } static var jsonEncoder: JSONEncoder { - JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy) + let encoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy) + encoder.outputFormatting = .sortedKeys + return encoder } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/AWSMutationDatabaseAdapter/AWSMutationDatabaseAdapter+MutationEventIngester.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/AWSMutationDatabaseAdapter/AWSMutationDatabaseAdapter+MutationEventIngester.swift index c9dae7504f..e936f78dea 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/AWSMutationDatabaseAdapter/AWSMutationDatabaseAdapter+MutationEventIngester.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/AWSMutationDatabaseAdapter/AWSMutationDatabaseAdapter+MutationEventIngester.swift @@ -34,22 +34,6 @@ extension AWSMutationDatabaseAdapter: MutationEventIngester { func resolveConflictsThenSave(mutationEvent: MutationEvent, storageAdapter: StorageEngineAdapter, completionPromise: @escaping Future.Promise) { - - // We don't want to query MutationSync because a) we already have the model, and b) delete mutations - // are submitted *after* the delete has already been applied to the local data store, meaning there is no model - // to query. - var mutationEvent = mutationEvent - do { - // swiftlint:disable:next todo - // TODO: Refactor this so that it's clear that the storage engine is not responsible for setting the version - // perhaps as simple as renaming to `submit(unversionedMutationEvent:)` or similar - let syncMetadata = try storageAdapter.queryMutationSyncMetadata(for: mutationEvent.modelId, - modelName: mutationEvent.modelName) - mutationEvent.version = syncMetadata?.version - } catch { - completionPromise(.failure(DataStoreError(error: error))) - } - MutationEvent.pendingMutationEvents( forMutationEvent: mutationEvent, storageAdapter: storageAdapter) { result in @@ -201,8 +185,6 @@ extension AWSMutationDatabaseAdapter: MutationEventIngester { } resolvedEvent.mutationType = updatedMutationType - resolvedEvent.version = candidate.version - return resolvedEvent } diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift index e9305d164a..0c1905288c 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift @@ -207,6 +207,9 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { let syncMutationToCloudOperation = SyncMutationToCloudOperation( mutationEvent: mutationEvent, + getLatestSyncMetadata: { + try? self.storageAdapter.queryMutationSyncMetadata(for: mutationEvent.modelId, modelName: mutationEvent.modelName) + }, api: api, authModeStrategy: authModeStrategy ) { [weak self] result in @@ -259,12 +262,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { return } reconciliationQueue.offer([mutationSync], modelName: mutationEvent.modelName) - MutationEvent.reconcilePendingMutationEventsVersion( - sent: mutationEvent, - received: mutationSync, - storageAdapter: storageAdapter) { _ in - self.completeProcessingEvent(mutationEvent, mutationSync: mutationSync) - } + completeProcessingEvent(mutationEvent, mutationSync: mutationSync) } else { completeProcessingEvent(mutationEvent) } diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift index 76b7daae21..35e2bbc41f 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift @@ -30,7 +30,7 @@ class ProcessMutationErrorFromCloudOperation: AsynchronousOperation { private var mutationOperation: AtomicValue>?> private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? - + init(dataStoreConfiguration: DataStoreConfiguration, mutationEvent: MutationEvent, api: APICategoryGraphQLBehavior, @@ -334,7 +334,7 @@ class ProcessMutationErrorFromCloudOperation: AsynchronousOperation { finish(result: .failure(dataStoreError)) return } - + reconciliationQueue.offer([graphQLResult], modelName: mutationEvent.modelName) } } diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift index 8c7a9fb568..8dc4bbc12b 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift @@ -19,6 +19,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { typealias MutationSyncCloudResult = GraphQLOperation>.OperationResult private weak var api: APICategoryGraphQLBehavior? + private let getLatestSyncMetadata: () -> MutationSyncMetadata? private let mutationEvent: MutationEvent private let completion: GraphQLOperation>.ResultListener private let requestRetryablePolicy: RequestRetryablePolicy @@ -32,6 +33,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { private var authTypesIterator: AWSAuthorizationTypeIterator? init(mutationEvent: MutationEvent, + getLatestSyncMetadata: @escaping () -> MutationSyncMetadata?, api: APICategoryGraphQLBehavior, authModeStrategy: AuthModeStrategy, networkReachabilityPublisher: AnyPublisher? = nil, @@ -39,6 +41,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { requestRetryablePolicy: RequestRetryablePolicy? = RequestRetryablePolicy(), completion: @escaping GraphQLOperation>.ResultListener) { self.mutationEvent = mutationEvent + self.getLatestSyncMetadata = getLatestSyncMetadata self.api = api self.networkReachabilityPublisher = networkReachabilityPublisher self.completion = completion @@ -109,7 +112,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { authType: AWSAuthorizationType? = nil ) -> GraphQLRequest>? { var request: GraphQLRequest> - + let version = getLatestSyncMetadata()?.version do { var graphQLFilter: GraphQLFilter? if let graphQLFilterJSON = mutationEvent.graphQLFilterJSON { @@ -128,7 +131,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { request = GraphQLRequest.deleteMutation(of: model, modelSchema: modelSchema, where: graphQLFilter, - version: mutationEvent.version) + version: version) case .update: let model = try mutationEvent.decodeModel() guard let modelSchema = ModelRegistry.modelSchema(from: mutationEvent.modelName) else { @@ -140,7 +143,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { request = GraphQLRequest.updateMutation(of: model, modelSchema: modelSchema, where: graphQLFilter, - version: mutationEvent.version) + version: version) case .create: let model = try mutationEvent.decodeModel() guard let modelSchema = ModelRegistry.modelSchema(from: mutationEvent.modelName) else { @@ -151,7 +154,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { } request = GraphQLRequest.createMutation(of: model, modelSchema: modelSchema, - version: mutationEvent.version) + version: version) } } catch { let apiError = APIError.unknown("Couldn't decode model", "", error) diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift index 1e0ebf4a16..dec80a8b23 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift @@ -106,6 +106,14 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { // MARK: - Responder methods + /// The reconcile function incorporates incoming mutation events into the local database through the following steps: + /// 1. Retrieve the local metadata of the models. + /// 2. Generate dispositions based on incoming mutation events and local metadata. + /// 3. Categorize dispositions into: + /// 3.1 Apply metadata only for those with existing pending mutations. + /// 3.1.1 Notify the count of these incoming mutation events as dropped items. + /// 3.2 Apply incoming mutation and metadata for those without existing pending mutations. + /// 4. Notify the final result. func reconcile(remoteModels: [RemoteModel]) { guard !isCancelled else { log.info("\(#function) - cancelled, aborting") @@ -126,16 +134,24 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { do { try storageAdapter.transaction { - queryPendingMutations(forModels: remoteModels.map(\.model)) + queryLocalMetadata(remoteModels) .subscribe(on: workQueue) - .flatMap { mutationEvents -> Future<([RemoteModel], [LocalMetadata]), DataStoreError> in - let remoteModelsToApply = self.reconcile(remoteModels, pendingMutations: mutationEvents) - return self.queryLocalMetadata(remoteModelsToApply) + .map { remoteModels, localMetadatas in + self.getDispositions(for: remoteModels, localMetadatas: localMetadatas) } - .flatMap { (remoteModelsToApply, localMetadatas) -> Future in - let dispositions = self.getDispositions(for: remoteModelsToApply, - localMetadatas: localMetadatas) - return self.applyRemoteModelsDispositions(dispositions) + .flatMap { dispositions in + self.queryPendingMutations(forModels: dispositions.map(\.remoteModel.model)).map { pendingMutations in + (pendingMutations, dispositions) + } + } + .map { pendingMutations, dispositions in + self.separateDispositions(pendingMutations: pendingMutations, dispositions: dispositions) + } + .flatMap { dispositions, dispositionsOnlyApplyMetadata in + self.waitAllPublisherFinishes(publishers: dispositionsOnlyApplyMetadata.map(self.saveMetadata(disposition:))) + .flatMap { _ in + self.applyRemoteModelsDispositions(dispositions) + } } .sink( receiveCompletion: { @@ -195,6 +211,28 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } } + func separateDispositions( + pendingMutations: [MutationEvent], + dispositions: [RemoteSyncReconciler.Disposition] + ) -> ([RemoteSyncReconciler.Disposition], [RemoteSyncReconciler.Disposition]) { + if dispositions.isEmpty { + return ([], []) + } + + let pendingMutationModelIds = Set(pendingMutations.map(\.modelId)) + + let dispositionsToApply = dispositions.filter { + !pendingMutationModelIds.contains($0.remoteModel.model.identifier) + } + + let dispositionsOnlyApplyMetadata = dispositions.filter { + pendingMutationModelIds.contains($0.remoteModel.model.identifier) + } + + notifyDropped(count: dispositionsOnlyApplyMetadata.count) + return (dispositionsToApply, dispositionsOnlyApplyMetadata) + } + func reconcile(_ remoteModels: [RemoteModel], pendingMutations: [MutationEvent]) -> [RemoteModel] { guard !remoteModels.isEmpty else { return [] @@ -284,8 +322,7 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } let publishers = dispositions.map { disposition -> - Publishers.FlatMap, - Future> in + AnyPublisher in switch disposition { case .create(let remoteModel): @@ -296,7 +333,8 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { applyResult: applyResult, mutationType: .create) } - return publisher + + return publisher.eraseToAnyPublisher() case .update(let remoteModel): let publisher = self.save(storageAdapter: storageAdapter, remoteModel: remoteModel) @@ -305,7 +343,7 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { applyResult: applyResult, mutationType: .update) } - return publisher + return publisher.eraseToAnyPublisher() case .delete(let remoteModel): let publisher = self.delete(storageAdapter: storageAdapter, remoteModel: remoteModel) @@ -314,7 +352,7 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { applyResult: applyResult, mutationType: .delete) } - return publisher + return publisher.eraseToAnyPublisher() } } @@ -367,8 +405,10 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } } - private func save(storageAdapter: StorageEngineAdapter, - remoteModel: RemoteModel) -> Future { + private func save( + storageAdapter: StorageEngineAdapter, + remoteModel: RemoteModel + ) -> Future { Future { promise in storageAdapter.save(untypedModel: remoteModel.model.instance) { response in switch response { @@ -396,29 +436,69 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } } - private func saveMetadata(storageAdapter: StorageEngineAdapter, - applyResult: ApplyRemoteModelResult, - mutationType: MutationEvent.MutationType) -> Future { - Future { promise in - guard case let .applied(inProcessModel) = applyResult else { - promise(.successfulVoid) - return - } + private func saveMetadata( + disposition: RemoteSyncReconciler.Disposition + ) -> AnyPublisher { + guard let storageAdapter = self.storageAdapter else { + return Just(()).eraseToAnyPublisher() + } - storageAdapter.save(inProcessModel.syncMetadata, condition: nil) { result in - switch result { - case .failure(let dataStoreError): - self.notifyDropped(error: dataStoreError) - promise(.failure(dataStoreError)) - case .success(let syncMetadata): + return saveMetadata( + storageAdapter: storageAdapter, + remoteModel: disposition.remoteModel, + mutationType: disposition.mutationType + ) + .map { _ in () } + .catch { _ in Just(()) } + .eraseToAnyPublisher() + } + + private func saveMetadata( + storageAdapter: StorageEngineAdapter, + remoteModel: RemoteModel, + mutationType: MutationEvent.MutationType + ) -> Future { + Future { promise in + storageAdapter.save( + remoteModel.syncMetadata, + condition: nil) { result in + promise(result) + } + } + } + + private func saveMetadata( + storageAdapter: StorageEngineAdapter, + applyResult: ApplyRemoteModelResult, + mutationType: MutationEvent.MutationType + ) -> AnyPublisher { + if case .applied(let inProcessModel) = applyResult { + return self.saveMetadata(storageAdapter: storageAdapter, remoteModel: inProcessModel, mutationType: mutationType) + .handleEvents(receiveOutput: { syncMetadata in let appliedModel = MutationSync(model: inProcessModel.model, syncMetadata: syncMetadata) self.notify(savedModel: appliedModel, mutationType: mutationType) - promise(.successfulVoid) - } - } + }, receiveCompletion: { completion in + if case .failure(let error) = completion { + self.notifyDropped(error: error) + } + }) + .map { _ in () } + .eraseToAnyPublisher() } + return Just(()).setFailureType(to: DataStoreError.self).eraseToAnyPublisher() } + private func waitAllPublisherFinishes(publishers: [AnyPublisher]) -> Future { + Future { promise in + Publishers.MergeMany(publishers) + .collect() + .sink(receiveCompletion: { _ in + promise(.successfulVoid) + }, receiveValue: { _ in }) + .store(in: &self.cancellables) + } + } + private func notifyDropped(count: Int = 1, error: DataStoreError? = nil) { for _ in 0 ..< count { mutationEventPublisher.send(.mutationEventDropped(modelName: modelSchema.name, error: error)) diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/RemoteSyncReconciler.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/RemoteSyncReconciler.swift index 962e43cb63..903d354d4a 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/RemoteSyncReconciler.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/RemoteSyncReconciler.swift @@ -17,6 +17,21 @@ struct RemoteSyncReconciler { case create(RemoteModel) case update(RemoteModel) case delete(RemoteModel) + + var remoteModel: RemoteModel { + switch self { + case .create(let model), .update(let model), .delete(let model): + return model + } + } + + var mutationType: MutationEvent.MutationType { + switch self { + case .create: return .create + case .update: return .update + case .delete: return .delete + } + } } /// Filter the incoming `remoteModels` against the pending mutations. diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/Support/MutationEvent+Extensions.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/Support/MutationEvent+Extensions.swift deleted file mode 100644 index 3d99be0a03..0000000000 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/Support/MutationEvent+Extensions.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Amplify -import Dispatch -import AWSPluginsCore - -extension MutationEvent { - // Consecutive operations that modify a model results in a sequence of pending mutation events that - // have the current version of the model. The first mutation event has the correct version of the model, - // while the subsequent events will have lower versions if the first mutation event is successfully synced - // to the cloud. By reconciling the pending mutation events after syncing the first mutation event, - // we attempt to update the pending version to the latest version from the response. - // The before and after conditions for consecutive update scenarios are as below: - // - Save, then immediately update - // Queue Before - [(version: nil, inprocess: true, type: .create), - // (version: nil, inprocess: false, type: .update)] - // Response - [version: 1, type: .create] - // Queue After - [(version: 1, inprocess: false, type: .update)] - // - Save, then immediately delete - // Queue Before - [(version: nil, inprocess: true, type: .create), - // (version: nil, inprocess: false, type: .delete)] - // Response - [version: 1, type: .create] - // Queue After - [(version: 1, inprocess: false, type: .delete)] - // - Save, sync, then immediately update and delete - // Queue Before (After save, sync) - // - [(version: 1, inprocess: true, type: .update), (version: 1, inprocess: false, type: .delete)] - // Response - [version: 2, type: .update] - // Queue After - [(version: 2, inprocess: false, type: .delete)] - // - // For a given model `id`, checks the version of the head of pending mutation event queue - // against the API response version in `mutationSync` and saves it in the mutation event table if - // the response version is a newer one - static func reconcilePendingMutationEventsVersion(sent mutationEvent: MutationEvent, - received mutationSync: MutationSync, - storageAdapter: StorageEngineAdapter, - completion: @escaping DataStoreCallback) { - MutationEvent.pendingMutationEvents( - forMutationEvent: mutationEvent, - storageAdapter: storageAdapter) { queryResult in - switch queryResult { - case .failure(let dataStoreError): - completion(.failure(dataStoreError)) - case .success(let localMutationEvents): - guard let existingEvent = localMutationEvents.first else { - completion(.success(())) - return - } - - guard let reconciledEvent = reconcile(pendingMutationEvent: existingEvent, - with: mutationEvent, - responseMutationSync: mutationSync) else { - completion(.success(())) - return - } - - storageAdapter.save(reconciledEvent, condition: nil) { result in - switch result { - case .failure(let dataStoreError): - completion(.failure(dataStoreError)) - case .success: - completion(.success(())) - } - } - } - } - } - - static func reconcile(pendingMutationEvent: MutationEvent, - with requestMutationEvent: MutationEvent, - responseMutationSync: MutationSync) -> MutationEvent? { - // return if version of the pending mutation event is not nil and - // is >= version contained in the response - if pendingMutationEvent.version != nil && - pendingMutationEvent.version! >= responseMutationSync.syncMetadata.version { - return nil - } - - do { - let responseModel = responseMutationSync.model.instance - let requestModel = try requestMutationEvent.decodeModel() - - // check if the data sent in the request is the same as the response - // if it is, update the pending mutation event version to the response version - guard let modelSchema = ModelRegistry.modelSchema(from: requestMutationEvent.modelName), - modelSchema.compare(responseModel, requestModel) else { - return nil - } - - var pendingMutationEvent = pendingMutationEvent - pendingMutationEvent.version = responseMutationSync.syncMetadata.version - return pendingMutationEvent - } catch { - Amplify.log.verbose("Error decoding models: \(error)") - return nil - } - } - -} diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreEndToEndTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreEndToEndTests.swift index 92e41f3c25..afc3c6ae75 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreEndToEndTests.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreEndToEndTests.swift @@ -302,7 +302,7 @@ class DataStoreEndToEndTests: SyncEngineIntegrationTestBase { } else if payload.eventName == HubPayload.EventName.DataStore.conditionalSaveFailed { if mutationEvent.mutationType == GraphQLMutationType.update.rawValue { XCTAssertEqual(post.title, updatedPost.title) - XCTAssertEqual(mutationEvent.version, 1) + XCTAssertEqual(mutationEvent.version, nil) conditionalReceived.fulfill() return } diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Models/SQLModelValueConverterTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Models/SQLModelValueConverterTests.swift index b4aba9e6d6..619265b00e 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Models/SQLModelValueConverterTests.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Models/SQLModelValueConverterTests.swift @@ -52,7 +52,7 @@ class SQLModelValueConverterTests: BaseDataStoreTests { /// - bool must be `Int` (1 or 0) /// - the remaining types should not change func testModelWithEveryTypeConversionToBindings() { - let nonModelJSON = "{\"someString\":\"string\",\"someEnum\":\"foo\"}" + let nonModelJSON = "{\"someEnum\":\"foo\",\"someString\":\"string\"}" let example = exampleModel // convert model to SQLite Bindings diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift index 53204467b3..6733402a35 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift @@ -26,7 +26,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { var localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) let queue = OperationQueue() let reconciliationQueue = MockReconciliationQueue() - + override func setUp() { tryOrFail { try setUpWithAPI() diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift index 66b6b89760..4983fec08f 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift @@ -46,8 +46,12 @@ class SyncMutationToCloudOperationTests: XCTestCase { var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { _, eventListener in + let responder = MutateRequestListenerResponder> { request, eventListener in if numberOfTimesEntered == 0 { + let requestInputVersion = request.variables + .flatMap { $0["input"] as? [String: Any] } + .flatMap { $0["_version"] as? Int } + XCTAssertEqual(requestInputVersion, 12) listenerFromFirstRequestOptional = eventListener expectFirstCallToAPIMutate.fulfill() } else if numberOfTimesEntered == 1 { @@ -68,7 +72,14 @@ class SyncMutationToCloudOperationTests: XCTestCase { expectMutationRequestCompletion.fulfill() } + let model = MockSynced(id: "id-1") let operation = SyncMutationToCloudOperation(mutationEvent: mutationEvent, + getLatestSyncMetadata: { MutationSyncMetadata( + modelId: model.id, + modelName: model.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 12) }, api: mockAPIPlugin, authModeStrategy: AWSDefaultAuthModeStrategy(), networkReachabilityPublisher: publisher, @@ -91,7 +102,6 @@ class SyncMutationToCloudOperationTests: XCTestCase { return } - let model = MockSynced(id: "id-1") let anyModel = try model.eraseToAnyModel() let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, modelName: model.modelName, @@ -141,6 +151,7 @@ class SyncMutationToCloudOperationTests: XCTestCase { expectMutationRequestCompletion.fulfill() } let operation = SyncMutationToCloudOperation(mutationEvent: mutationEvent, + getLatestSyncMetadata: { nil }, api: mockAPIPlugin, authModeStrategy: AWSDefaultAuthModeStrategy(), networkReachabilityPublisher: publisher, @@ -214,6 +225,7 @@ class SyncMutationToCloudOperationTests: XCTestCase { } } let operation = SyncMutationToCloudOperation(mutationEvent: mutationEvent, + getLatestSyncMetadata: { nil }, api: mockAPIPlugin, authModeStrategy: AWSDefaultAuthModeStrategy(), networkReachabilityPublisher: publisher, diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/MutationEventExtensionsTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/MutationEventExtensionsTests.swift deleted file mode 100644 index 7c676d68cb..0000000000 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/MutationEventExtensionsTests.swift +++ /dev/null @@ -1,415 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation -import SQLite -import XCTest - -@testable import Amplify -@testable import AmplifyTestCommon -@testable import AWSDataStoreCategoryPlugin -@testable import AWSPluginsCore - -// swiftlint:disable:next todo -// TODO: This flaky test has been disabled, tracking issue: https://github.com/aws-amplify/amplify-ios/issues/1831 -// swiftlint:disable type_body_length -class MutationEventExtensionsTest: BaseDataStoreTests { - - /// - Given: A pending mutation events queue with event containing `nil` version, a sent mutation - /// event model that matches the received mutation sync model. The received mutation sync has version 1. - /// - When: The sent model matches the received model and the first pending mutation event version is `nil`. - /// - Then: The pending mutation event version should be updated to the received model version of 1. - func testSentModelWithNilVersion_Reconciled() throws { - throw XCTSkip("TODO: fix this test") - let modelId = UUID().uuidString - let post = Post(id: modelId, title: "title", content: "content", createdAt: .now()) - let requestMutationEvent = try createMutationEvent(model: post, - mutationType: .create, - createdAt: .now(), - version: nil, - inProcess: true) - let pendingMutationEvent = try createMutationEvent(model: post, - mutationType: .update, - createdAt: .now().add(value: 1, to: .second), - version: nil) - let responseMutationSync = createMutationSync(model: post, version: 1) - - setUpPendingMutationQueue(post, [requestMutationEvent, pendingMutationEvent], pendingMutationEvent) - - let reconciledEvent = MutationEvent.reconcile(pendingMutationEvent: pendingMutationEvent, - with: requestMutationEvent, - responseMutationSync: responseMutationSync) - XCTAssertNotNil(reconciledEvent) - XCTAssertEqual(reconciledEvent?.version, responseMutationSync.syncMetadata.version) - - let queryAfterUpdatingVersionExpectation = expectation(description: "update mutation should be latest version") - let updatingVersionExpectation = expectation(description: "update latest mutation event with response version") - - // update the version of head of mutation event table for given model id to the version of `mutationSync` - MutationEvent.reconcilePendingMutationEventsVersion(sent: requestMutationEvent, - received: responseMutationSync, - storageAdapter: storageAdapter) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success: - updatingVersionExpectation.fulfill() - } - } - wait(for: [updatingVersionExpectation], timeout: 1) - - // query for head of mutation event table for given model id and check if it has the updated version - MutationEvent.pendingMutationEvents( - forModel: post, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let mutationEvents): - guard !mutationEvents.isEmpty, let head = mutationEvents.first else { - XCTFail("Failure while updating version") - return - } - XCTAssertEqual(head.version, responseMutationSync.syncMetadata.version) - XCTAssertEqual(head.mutationType, MutationEvent.MutationType.update.rawValue) - queryAfterUpdatingVersionExpectation.fulfill() - } - } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) - } - - /// - Given: A pending mutation events queue with two events(update and delete) containing `nil` version, - /// a sent mutation event model that matches the received mutation sync model. The received mutation - /// sync has version 1. - /// - When: The sent model matches the received model, the first pending mutation event(update) version is `nil` and - /// the second pending mutation event(delete) version is `nil`. - /// - Then: The first pending mutation event(update) version should be updated to the received model version of 1 - /// and the second pending mutation event version(delete) should not be updated. - func testSentModelWithNilVersion_SecondPendingEventNotReconciled() throws { - throw XCTSkip("TODO: fix this test") - let modelId = UUID().uuidString - let post = Post(id: modelId, title: "title", content: "content", createdAt: .now()) - let requestMutationEvent = try createMutationEvent(model: post, - mutationType: .create, - createdAt: .now(), - version: nil, - inProcess: true) - let pendingUpdateMutationEvent = try createMutationEvent(model: post, - mutationType: .update, - createdAt: .now().add(value: 1, to: .second), - version: nil) - let pendingDeleteMutationEvent = try createMutationEvent(model: post, - mutationType: .delete, - createdAt: .now().add(value: 2, to: .second), - version: nil) - let responseMutationSync = createMutationSync(model: post, version: 1) - - setUpPendingMutationQueue(post, - [requestMutationEvent, pendingUpdateMutationEvent, pendingDeleteMutationEvent], - pendingUpdateMutationEvent) - - let reconciledEvent = MutationEvent.reconcile(pendingMutationEvent: pendingUpdateMutationEvent, - with: requestMutationEvent, - responseMutationSync: responseMutationSync) - XCTAssertNotNil(reconciledEvent) - XCTAssertEqual(reconciledEvent?.version, responseMutationSync.syncMetadata.version) - - let queryAfterUpdatingVersionExpectation = expectation(description: "update mutation should be latest version") - let updatingVersionExpectation = expectation(description: "update latest mutation event with response version") - - // update the version of head of mutation event table for given model id to the version of `mutationSync` - MutationEvent.reconcilePendingMutationEventsVersion(sent: requestMutationEvent, - received: responseMutationSync, - storageAdapter: storageAdapter) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success: - updatingVersionExpectation.fulfill() - } - } - wait(for: [updatingVersionExpectation], timeout: 1) - - // query for head of mutation event table for given model id and check if it has the updated version - MutationEvent.pendingMutationEvents( - forModel: post, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let mutationEvents): - guard !mutationEvents.isEmpty, let head = mutationEvents.first, let last = mutationEvents.last else { - XCTFail("Failure while updating version") - return - } - XCTAssertEqual(head.version, responseMutationSync.syncMetadata.version) - XCTAssertEqual(head.mutationType, MutationEvent.MutationType.update.rawValue) - XCTAssertEqual(last, pendingDeleteMutationEvent) - queryAfterUpdatingVersionExpectation.fulfill() - } - } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) - } - - /// - Given: A pending mutation events queue with event containing version 2, a sent mutation event model - /// that matches the received mutation sync model having version 2. The received mutation sync has - /// version 1. - /// - When: The sent model matches the received model and the first pending mutation event version is 2. - /// - Then: The first pending mutation event version should NOT be updated. - func testSentModelVersionNewerThanResponseVersion_PendingEventNotReconciled() throws { - throw XCTSkip("TODO: fix this test") - let modelId = UUID().uuidString - let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) - let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) - let requestMutationEvent = try createMutationEvent(model: post1, - mutationType: .create, - createdAt: .now(), - version: 2, - inProcess: true) - let pendingMutationEvent = try createMutationEvent(model: post2, - mutationType: .update, - createdAt: .now().add(value: 1, to: .second), - version: 2) - let responseMutationSync = createMutationSync(model: post1, version: 1) - - setUpPendingMutationQueue(post1, [requestMutationEvent, pendingMutationEvent], pendingMutationEvent) - - let reconciledEvent = MutationEvent.reconcile(pendingMutationEvent: pendingMutationEvent, - with: requestMutationEvent, - responseMutationSync: responseMutationSync) - XCTAssertNil(reconciledEvent) - - let queryAfterUpdatingVersionExpectation = expectation(description: "update mutation should have version 2") - let updatingVersionExpectation = - expectation(description: "don't update latest mutation event with response version") - - MutationEvent.reconcilePendingMutationEventsVersion(sent: requestMutationEvent, - received: responseMutationSync, - storageAdapter: storageAdapter) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success: - updatingVersionExpectation.fulfill() - } - } - wait(for: [updatingVersionExpectation], timeout: 1) - - // query for head of mutation event table for given model id and check if it has the correct version - MutationEvent.pendingMutationEvents( - forModel: post1, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let mutationEvents): - guard !mutationEvents.isEmpty, let head = mutationEvents.first else { - XCTFail("Failure while updating version") - return - } - XCTAssertNotEqual(head.version, responseMutationSync.syncMetadata.version) - XCTAssertEqual(head, pendingMutationEvent) - queryAfterUpdatingVersionExpectation.fulfill() - } - } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) - } - - /// - Given: A pending mutation events queue with event containing version 1, a sent mutation event model - /// that doesn't match the received mutation sync model having version 1. The received mutation - /// sync has version 2. - /// - When: The sent model doesn't match the received model and the first pending mutation event version is 1. - /// - Then: The first pending mutation event version should NOT be updated. - func testSentModelNotEqualToResponseModel_PendingEventNotReconciled() throws { - throw XCTSkip("TODO: fix this test") - let modelId = UUID().uuidString - let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) - let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) - let post3 = Post(id: modelId, title: "title3", content: "content3", createdAt: .now()) - let requestMutationEvent = try createMutationEvent(model: post1, - mutationType: .update, - createdAt: .now(), - version: 1, - inProcess: true) - let pendingMutationEvent = try createMutationEvent(model: post2, - mutationType: .update, - createdAt: .now().add(value: 1, to: .second), - version: 1) - let responseMutationSync = createMutationSync(model: post3, version: 2) - - setUpPendingMutationQueue(post1, [requestMutationEvent, pendingMutationEvent], pendingMutationEvent) - - let reconciledEvent = MutationEvent.reconcile(pendingMutationEvent: pendingMutationEvent, - with: requestMutationEvent, - responseMutationSync: responseMutationSync) - XCTAssertNil(reconciledEvent) - - let queryAfterUpdatingVersionExpectation = expectation(description: "update mutation should have version 1") - let updatingVersionExpectation = - expectation(description: "don't update latest mutation event with response version") - - MutationEvent.reconcilePendingMutationEventsVersion(sent: requestMutationEvent, - received: responseMutationSync, - storageAdapter: storageAdapter) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success: - updatingVersionExpectation.fulfill() - } - } - wait(for: [updatingVersionExpectation], timeout: 1) - - // query for head of mutation event table for given model id and check if it has the correct version - MutationEvent.pendingMutationEvents( - forModel: post1, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let mutationEvents): - guard !mutationEvents.isEmpty, let head = mutationEvents.first else { - XCTFail("Failure while updating version") - return - } - XCTAssertNotEqual(head.version, responseMutationSync.syncMetadata.version) - XCTAssertEqual(head, pendingMutationEvent) - queryAfterUpdatingVersionExpectation.fulfill() - } - } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) - } - - /// - Given: A pending mutation events queue with event containing version 1, a sent mutation event model - /// that matches the received mutation sync model having version 1. The received mutation sync - /// has version 2. - /// - When: The sent model matches the received model and the first pending mutation event version is 1. - /// - Then: The first pending mutation event version should be updated to received mutation sync version i.e. 2. - func testPendingVersionReconciledSuccess() throws { - throw XCTSkip("TODO: fix this test") - let modelId = UUID().uuidString - let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) - let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) - let requestMutationEvent = try createMutationEvent(model: post1, - mutationType: .update, - createdAt: .now(), - version: 1, - inProcess: true) - let pendingMutationEvent = try createMutationEvent(model: post2, - mutationType: .update, - createdAt: .now().add(value: 1, to: .second), - version: 1) - let responseMutationSync = createMutationSync(model: post1, version: 2) - - setUpPendingMutationQueue(post1, [requestMutationEvent, pendingMutationEvent], pendingMutationEvent) - - let reconciledEvent = MutationEvent.reconcile(pendingMutationEvent: pendingMutationEvent, - with: requestMutationEvent, - responseMutationSync: responseMutationSync) - XCTAssertNotNil(reconciledEvent) - XCTAssertEqual(reconciledEvent?.version, responseMutationSync.syncMetadata.version) - - let queryAfterUpdatingVersionExpectation = expectation(description: "update mutation should have version 2") - let updatingVersionExpectation = expectation(description: "update latest mutation event with response version") - - MutationEvent.reconcilePendingMutationEventsVersion(sent: requestMutationEvent, - received: responseMutationSync, - storageAdapter: storageAdapter) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success: - updatingVersionExpectation.fulfill() - } - } - wait(for: [updatingVersionExpectation], timeout: 1) - - // query for head of mutation event table for given model id and check if it has the correct version - MutationEvent.pendingMutationEvents( - forModel: post1, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let mutationEvents): - guard !mutationEvents.isEmpty, let head = mutationEvents.first else { - XCTFail("Failure while updating version") - return - } - XCTAssertEqual(head.version, responseMutationSync.syncMetadata.version) - XCTAssertEqual(head.mutationType, MutationEvent.MutationType.update.rawValue) - queryAfterUpdatingVersionExpectation.fulfill() - } - } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) - } - - private func createMutationEvent(model: Model, - mutationType: MutationEvent.MutationType, - createdAt: Temporal.DateTime, - version: Int? = nil, - inProcess: Bool = false) throws -> MutationEvent { - return MutationEvent(id: UUID().uuidString, - modelId: model.identifier(schema: MutationEvent.schema).stringValue, - modelName: model.modelName, - json: try model.toJSON(), - mutationType: mutationType, - createdAt: createdAt, - version: version, - inProcess: inProcess) - } - - private func createMutationSync(model: Model, version: Int = 1) -> MutationSync { - let metadata = MutationSyncMetadata(modelId: model.identifier(schema: MutationEvent.schema).stringValue, - modelName: model.modelName, - deleted: false, - lastChangedAt: Int(Date().timeIntervalSince1970), - version: version) - return MutationSync(model: AnyModel(model), syncMetadata: metadata) - } - - private func setUpPendingMutationQueue(_ model: Model, - _ mutationEvents: [MutationEvent], - _ expectedHeadOfQueue: MutationEvent) { - for mutationEvent in mutationEvents { - let mutationEventSaveExpectation = expectation(description: "save mutation event success") - storageAdapter.save(mutationEvent) { result in - guard case .success = result else { - XCTFail("Failed to save metadata") - return - } - mutationEventSaveExpectation.fulfill() - } - wait(for: [mutationEventSaveExpectation], timeout: 1) - } - - // verify the head of queue is expected - let headOfQueueExpectation = expectation(description: "head of mutation event queue is as expected") - MutationEvent.pendingMutationEvents( - forModel: model, - storageAdapter: storageAdapter - ) { result in - switch result { - case .failure(let error): - XCTFail("Error : \(error)") - case .success(let events): - guard !events.isEmpty, let head = events.first else { - XCTFail("Failure while fetching mutation events") - return - } - XCTAssertEqual(head, expectedHeadOfQueue) - headOfQueueExpectation.fulfill() - } - } - wait(for: [headOfQueueExpectation], timeout: 1) - } -} // swiftlint:disable:this file_length diff --git a/AmplifyPlugins/DataStore/DataStoreCategoryPlugin.xcodeproj/project.pbxproj b/AmplifyPlugins/DataStore/DataStoreCategoryPlugin.xcodeproj/project.pbxproj index e07e2cfb95..b9b8078505 100644 --- a/AmplifyPlugins/DataStore/DataStoreCategoryPlugin.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/DataStore/DataStoreCategoryPlugin.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -241,8 +241,6 @@ 9728F2B02683D98D00A506A8 /* DataStoreConsecutiveUpdatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9728F2AF2683D98D00A506A8 /* DataStoreConsecutiveUpdatesTests.swift */; }; 973AF1AF26E016EC00BED353 /* ModelCompareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973AF1AE26E016EC00BED353 /* ModelCompareTests.swift */; }; 97406B382666DC0200C41E19 /* DataStoreCustomPrimaryKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97406B372666DC0200C41E19 /* DataStoreCustomPrimaryKeyTests.swift */; }; - 97DB735426B49ED6004708B8 /* MutationEvent+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB735326B49ED6004708B8 /* MutationEvent+Extensions.swift */; }; - 97DB735B26B4A229004708B8 /* MutationEventExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB735A26B4A229004708B8 /* MutationEventExtensionsTests.swift */; }; 97ED948A26DEC90A0025FA43 /* Model+Compare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97ED948926DEC90A0025FA43 /* Model+Compare.swift */; }; 97F793BC27AC934D000153D6 /* SQLStatement+CreateIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F793BB27AC934D000153D6 /* SQLStatement+CreateIndex.swift */; }; 97F793DC27BC7882000153D6 /* StorageEngineTestsSQLiteIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F793DB27BC7882000153D6 /* StorageEngineTestsSQLiteIndex.swift */; }; @@ -698,8 +696,6 @@ 9728F2AF2683D98D00A506A8 /* DataStoreConsecutiveUpdatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreConsecutiveUpdatesTests.swift; sourceTree = ""; }; 973AF1AE26E016EC00BED353 /* ModelCompareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelCompareTests.swift; sourceTree = ""; }; 97406B372666DC0200C41E19 /* DataStoreCustomPrimaryKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreCustomPrimaryKeyTests.swift; sourceTree = ""; }; - 97DB735326B49ED6004708B8 /* MutationEvent+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MutationEvent+Extensions.swift"; sourceTree = ""; }; - 97DB735A26B4A229004708B8 /* MutationEventExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutationEventExtensionsTests.swift; sourceTree = ""; }; 97ED948926DEC90A0025FA43 /* Model+Compare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+Compare.swift"; sourceTree = ""; }; 97F793BB27AC934D000153D6 /* SQLStatement+CreateIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLStatement+CreateIndex.swift"; sourceTree = ""; }; 97F793DB27BC7882000153D6 /* StorageEngineTestsSQLiteIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageEngineTestsSQLiteIndex.swift; sourceTree = ""; }; @@ -1139,7 +1135,6 @@ children = ( 210E218126601C1C00D90ED8 /* MutationEventQueryTests.swift */, 214B6B67264B157500A9311D /* StopwatchTests.swift */, - 97DB735A26B4A229004708B8 /* MutationEventExtensionsTests.swift */, 973AF1AE26E016EC00BED353 /* ModelCompareTests.swift */, ); path = Support; @@ -1605,7 +1600,6 @@ FA8F4D212395B11700861D91 /* MutationEvent+Query.swift */, FA3841E823889D440070AD5B /* StateMachine.swift */, 214B6B64264B0D6700A9311D /* Stopwatch.swift */, - 97DB735326B49ED6004708B8 /* MutationEvent+Extensions.swift */, 212B4685269FB10500A0AEE7 /* SQLiteResultError.swift */, 97ED948926DEC90A0025FA43 /* Model+Compare.swift */, ); @@ -2672,7 +2666,6 @@ FACBA78F23949C75006349C8 /* AWSMutationDatabaseAdapter.swift in Sources */, FA55A54D2391F96E002AFF2D /* AWSMutationDatabaseAdapter+MutationEventSource.swift in Sources */, D88666A425070FC6000F7A14 /* OutboxMutationEvent.swift in Sources */, - 97DB735426B49ED6004708B8 /* MutationEvent+Extensions.swift in Sources */, 2102DD47260D87BC00B80FE2 /* ReconcileAndLocalSaveQueue.swift in Sources */, 2149E5CE2388684F00873955 /* SQLStatement+CreateTable.swift in Sources */, 6B3CC68023F87FA10008ECBC /* RemoteSyncEngine+Retryable.swift in Sources */, @@ -2727,7 +2720,6 @@ 6B7743E9259071F5001469F5 /* MockRemoteSyncEngine.swift in Sources */, 97F793DC27BC7882000153D6 /* StorageEngineTestsSQLiteIndex.swift in Sources */, FAE4146C239AA40600CE94C2 /* MockSQLiteStorageEngineAdapter.swift in Sources */, - 97DB735B26B4A229004708B8 /* MutationEventExtensionsTests.swift in Sources */, B4D9B9E424DF90CD0049484F /* DynamicModel.swift in Sources */, B912D1BA242984D10028F05C /* QueryPaginationInputTests.swift in Sources */, FA8D932F239EA5C4001ED336 /* NoOpInitialSyncOrchestrator.swift in Sources */, diff --git a/AmplifyPlugins/DataStore/Podfile b/AmplifyPlugins/DataStore/Podfile index 3f7882fed5..62bd4fe194 100644 --- a/AmplifyPlugins/DataStore/Podfile +++ b/AmplifyPlugins/DataStore/Podfile @@ -4,7 +4,6 @@ platform :ios, '11.0' use_frameworks! include_build_tools! - target 'AWSDataStoreCategoryPlugin' do pod 'Amplify', :path => '../../' pod 'AWSPluginsCore', :path => '../../' diff --git a/AmplifyPlugins/DataStore/Podfile.lock b/AmplifyPlugins/DataStore/Podfile.lock index 3df8d9c31e..ca80e0ef1a 100644 --- a/AmplifyPlugins/DataStore/Podfile.lock +++ b/AmplifyPlugins/DataStore/Podfile.lock @@ -23,21 +23,21 @@ PODS: - AWSMobileClient (~> 2.33.0) - AWSPluginsCore (= 1.30.4) - CwlPreconditionTesting (~> 2.0) - - AppSyncRealTimeClient (3.1.0): - - Starscream (~> 4.0.4) - - AWSAuthCore (2.33.0): - - AWSCore (= 2.33.0) - - AWSCognitoIdentityProvider (2.33.0): - - AWSCognitoIdentityProviderASF (= 2.33.0) - - AWSCore (= 2.33.0) - - AWSCognitoIdentityProviderASF (2.33.0): - - AWSCore (= 2.33.0) - - AWSCore (2.33.0) - - AWSMobileClient (2.33.0): - - AWSAuthCore (= 2.33.0) - - AWSCognitoIdentityProvider (= 2.33.0) - - AWSCognitoIdentityProviderASF (= 2.33.0) - - AWSCore (= 2.33.0) + - AppSyncRealTimeClient (3.1.2): + - Starscream (= 4.0.4) + - AWSAuthCore (2.33.5): + - AWSCore (= 2.33.5) + - AWSCognitoIdentityProvider (2.33.5): + - AWSCognitoIdentityProviderASF (= 2.33.5) + - AWSCore (= 2.33.5) + - AWSCognitoIdentityProviderASF (2.33.5): + - AWSCore (= 2.33.5) + - AWSCore (2.33.5) + - AWSMobileClient (2.33.5): + - AWSAuthCore (= 2.33.5) + - AWSCognitoIdentityProvider (= 2.33.5) + - AWSCognitoIdentityProviderASF (= 2.33.5) + - AWSCore (= 2.33.5) - AWSPluginsCore (1.30.4): - Amplify (= 1.30.4) - AWSCore (~> 2.33.0) @@ -109,12 +109,12 @@ SPEC CHECKSUMS: Amplify: 4e41668c8801667e174ab50c12bc3099c71a257f AmplifyPlugins: 1335f0919aa07329df58685a020f2311c75a5176 AmplifyTestCommon: fef1ca37417681cc246d0860cd18e4c6723fbbf7 - AppSyncRealTimeClient: 49901c6f21e541bec09281854c5695a6c1309bf7 - AWSAuthCore: 544324f7c595cba78440027039c4429e6ce588fa - AWSCognitoIdentityProvider: f7364fdae1109e6f326c19e98f5ed5b5d413d929 - AWSCognitoIdentityProviderASF: 2023303dc28588dfe95a4152e588bcea5e1f57ec - AWSCore: ff962198e35c750cb1d59cd459c51f1990e3ec68 - AWSMobileClient: 9fab7504e6403dda5988471aa82cfd33ef8a8b78 + AppSyncRealTimeClient: 36ff2f65f330bce7f1d0d2157a4ae5d255483dac + AWSAuthCore: f80d57f22ad0c781082658de8f941393ea21b955 + AWSCognitoIdentityProvider: 5eeb67304ea7ac853836d685f2b50c08d3194155 + AWSCognitoIdentityProviderASF: aaa2eb46b9775090081b33fb5134090ba253e889 + AWSCore: 6d8aa46c210e37f15c10509437a9c8570f7290bd + AWSMobileClient: 07c4cf156a1b3715cdf15493cdf11aade97d2369 AWSPluginsCore: f2188458c27e6cb09597c67876f80611d14275d7 CwlCatchException: 86760545af2a490a23e964d76d7c77442dbce79b CwlCatchExceptionSupport: a004322095d7101b945442c86adc7cec0650f676 @@ -126,6 +126,6 @@ SPEC CHECKSUMS: SwiftFormat: 3b5caa6389b2b9adbc00e133b3ccc8c6e687a6a4 SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5 -PODFILE CHECKSUM: 0bab7193bebdf470839514f327440893b0d26090 +PODFILE CHECKSUM: 8caac7d134172f8f6778aa33b0ca9cf256ddc666 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/AmplifyTests/CoreTests/Model+CodableTests.swift b/AmplifyTests/CoreTests/Model+CodableTests.swift index 5096ddca87..b933d2552f 100644 --- a/AmplifyTests/CoreTests/Model+CodableTests.swift +++ b/AmplifyTests/CoreTests/Model+CodableTests.swift @@ -11,11 +11,11 @@ import AmplifyTestCommon class ModelCodableTests: XCTestCase { let postJSONWithFractionalSeconds = """ - {"id":"post-1","title":"title","content":"content","comments":[],"createdAt":"1970-01-12T13:46:40.123Z"} + {"comments":[],"content":"content","createdAt":"1970-01-12T13:46:40.123Z","id":"post-1","title":"title"} """ let postJSONWithoutFractionalSeconds = """ - {"id":"post-1","title":"title","content":"content","comments":[],"createdAt":"1970-01-12T13:46:40Z"} + {"comments":[],"content":"content","createdAt":"1970-01-12T13:46:40Z","id":"post-1","title":"title"} """ override func setUp() { diff --git a/Podfile.lock b/Podfile.lock index 223e5332b5..189768ff7f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - AWSCore (2.33.0) + - AWSCore (2.33.5) - CwlCatchException (2.1.1): - CwlCatchExceptionSupport (~> 2.1.1) - CwlCatchExceptionSupport (2.1.1) @@ -40,7 +40,7 @@ CHECKOUT OPTIONS: :tag: 2.1.0 SPEC CHECKSUMS: - AWSCore: ff962198e35c750cb1d59cd459c51f1990e3ec68 + AWSCore: 6d8aa46c210e37f15c10509437a9c8570f7290bd CwlCatchException: 86760545af2a490a23e964d76d7c77442dbce79b CwlCatchExceptionSupport: a004322095d7101b945442c86adc7cec0650f676 CwlMachBadInstructionHandler: aa1fe9f2d08b29507c150d099434b2890247e7f8 @@ -51,4 +51,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 5e20e56b8ef40444b018a3736b7b726ff9772f00 -COCOAPODS: 1.12.0 +COCOAPODS: 1.14.3