Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataStore.save() with condition #355

Merged
merged 18 commits into from
Apr 3, 2020

Conversation

lawmicha
Copy link
Contributor

@lawmicha lawmicha commented Mar 12, 2020

Description of changes:

This adds the condition argument to the DataStore.save() for local storage.

Scenario 1: the local store does not contain model, and there is a condition pased in, fail with conditional save failed.

Scenario 2: the local store contains the model, but the condition does not match the existing instance, fail with conditional save failed

Scenario 3: the local storage contains the model, and the condition matches the existing instance, update the model with the provided fields with the condition in the where clause, preventing any race condition between checkiing that the condition matches existing instance and actually updating it.

Details

  • Add Codable conformance to QueryPredicate to allow encodinig/decoding to JSON, stored in MutationEvent. This is accomplished with AnyQueryPredicate for encoding array of predicates, and custom Codable for QueryOperator.
  • Storing queryPredicateJson in MutationEvent to allow decoding to QueryPredicate for 1. sending API requests with condition input on create/update mutations, and 2. handle conditional mutations in the AWSMutationDatabaseAdapter: MutationEventIngester.
  • AWSMutationDatabaseAdapter resolves conflicts for incoming candidate mutation events with conditions by saving the candidate. (does not drop/replace local with candidate, etc)
  • Added a ProcessMutationErrorFromCloudOperation to check errors for "conditional failed event", query for latest, and storage it back to local.

TODO:

  • emit failed events to the hub

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@lawmicha lawmicha requested review from drochetti and wooj2 March 12, 2020 15:39
@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from 8d38c08 to ae7e816 Compare March 12, 2020 15:40
@lawmicha lawmicha self-assigned this Mar 23, 2020
@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from ae7e816 to 723ae70 Compare March 23, 2020 14:55
@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from 723ae70 to d631c28 Compare March 23, 2020 16:26
@lawmicha lawmicha changed the title DataStore.save() with condition DataStore.save() with condition for local storage Mar 23, 2020
Copy link
Contributor

@drochetti drochetti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did an first pass and left a couple of comments.

I believe the missing part here is since the condition needs to be sent to the Sync Engine later on, we will need to change the MutationEvent to save the condition as well. This will require a bit of work since now the conditions will need to be Codable and converted to/from JSON when interacting with the MutationEvent. See the existing model in the MutationEvent for reference.

@lawmicha lawmicha changed the title DataStore.save() with condition for local storage DataStore.save() with condition Mar 24, 2020
…uccessful responnse with graphql errors. If the graphQL error is conditional failed, then retrieve the latest remote model, and apply it to local storage.
…after possibly exeuction of ProcessMutationErrorFromCloudOperation.

- Added unit test for ProcessMutationErrorFromCloudOperation
Comment on lines +56 to +59
if data.count == 1, let first = data.first, case .null = first.value {
let responseErrors = try decodeErrors(graphQLErrors: errors)
return GraphQLResponse<R>.failure(.error(responseErrors))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was added due to graphql response that looks like

{
 "data": {
  "updatePost": null
 },
"errors": {
 ...
}
}

not too happy with this class, needs another pass on refactoring and better unit tests

- AnyModelTester class rename
- unit test parallelization fix
- add correct error from StorageEngine for save()
}

public init<M: Model>(model: M,
mutationType: MutationType,
version: Int? = nil) throws {
version: Int? = nil,
queryPredicate: QueryPredicate? = nil) throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the model should stay a pure and simple value holding data structure. Therefore I don't think we need this initializer nor any initializer in models should throw. The encoding/decoding logic should happen outside, where the model is initialized (just like with the json property)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code looks a bit outdated, it should be taking in queryPredicateJson: String? = nil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is now updated to graphQLFilterJSON

Amplify/Categories/DataStore/DataStoreError.swift Outdated Show resolved Hide resolved
extension String {

/// Deserialize the JSON string converted with `QueryPredicate.toGraphQLFilterString()` to `[String: Any]` JSON
public func toGraphQLFilterJson() throws -> [String: Any] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a public extension.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we've been using JSON instead of Json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a public extension.

isn't this public for DataStore to turn it back into GraphQLFilter? that is unless we change the GraphQLRequest extension methods to take in graphQLFilterJSON String and then internally do graphQLFilterJSON.toGraphQLFiilter()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that this is a generic String -> JSON function, added as an extension to String. Also the semantics are not entire correct. For example: "{a:1}". toGraphQLFilterJson() would return a valid JSON as [String: Any] but it's not a valid GraphQL filter.

Therefore, if the extension is toJSON(), that could be valid. Then the discussion is if we should expose that extension to customers or not (it seems outside of the scope of Amplify to add such extension).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking a look at this to see if we can reduce the access level or at least move it under a static function of a different class rather than extension on String

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree, the string extension is incorrect in this case, and is prone to errors, and don't think we want to take on the toJSON() implementation. Updated to GraphQLFilterConverter with static methods for turning into a GraphQLFilter JSON and back to a GraphQLFilter

Comment on lines 156 to 182
func toGraphQLFilterJSON() throws -> String {
func toGraphQLFilterPrettyPrintedJson() throws -> String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: if you have only one toJSON function, I think if it's pretty printed or not can be omitted from the function name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was to differentiate between the code's toGraphQLFilterJSON and test code's toGraphQLFilterJSON which does pretty print for test case assertion, i think what we can do here is in actual code, have optional options and allow test to leverage the parameter by passing in options = [.prettyPrinted]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I understand in actual code, have optional options. If in tests it's always pretty printed, then no options should be needed. Do the extensions clash? Is that why you need another toJSON function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes the reason for the different naming in test code is the clashing of method names. i wanted toGraphQLFilterJSON with pretty print options set because our test relies on asserting against a hand crafted pretty printed version of the graphql filter json. "actual code" i meant the production code

@@ -15,6 +15,7 @@ public protocol DataStoreBaseBehavior {

/// Saves the model to storage. If sync is enabled, also initiates a sync of the mutation to the remote API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method just got a new layer of complexity, not need to address it right now but I believe we need to add proper API documentation to DataStore in general. I'll create an issue to track it.

@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from 0357ad7 to 27a75e8 Compare March 31, 2020 18:40
@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from 27a75e8 to 8977e88 Compare March 31, 2020 18:42
… error naming, removed QueryPredicateInput, updated MutationEvent's field as graphQLFiilterJSON
@lawmicha lawmicha force-pushed the feature/datastore-conditional-update branch from 8977e88 to f09e606 Compare April 1, 2020 16:16
@lawmicha
Copy link
Contributor Author

lawmicha commented Apr 2, 2020

Tracking issue regarding dead code: #370

Copy link
Contributor

@drochetti drochetti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job man, thanks for the thoughtful work and follow up :)

        /\/\

                        /\/\
         O  O  O
                 O
                __|__
                || ||_____
                || ||    |
     --------------------------
      \   O   O   O   O      /
 ~ ~ ~ ~ ~ ~ ~ ~  ~ ~ ~ ~ ~ ~ ~ ~
  ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

@lawmicha lawmicha merged commit 5bd3e65 into master Apr 3, 2020
@lawmicha lawmicha deleted the feature/datastore-conditional-update branch April 6, 2020 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants