aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.43k stars 2.13k forks source link

Datastore.save() sending _version of previous query model instead of current model. #13412

Open johnmacabed opened 5 months ago

johnmacabed commented 5 months ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

DataStore

Amplify Version

v6

Amplify Categories

storage

Backend

Amplify CLI

Environment information

``` # Put output below this line System: OS: macOS 14.4.1 CPU: (8) arm64 Apple M1 Pro Memory: 84.30 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.16.1 - ~/.nvm/versions/node/v18.16.1/bin/node Yarn: 1.22.21 - ~/.nvm/versions/node/v18.16.1/bin/yarn npm: 9.5.1 - ~/.nvm/versions/node/v18.16.1/bin/npm Browsers: Chrome: 124.0.6367.208 Safari: 17.4.1 npmPackages: @aws-amplify/ui-react: ^6.1.12 => 6.1.12 @aws-amplify/ui-react-internal: undefined () @aws-amplify/ui-react-storage: ^3.1.3 => 3.1.3 @aws-sdk/client-s3: ^3.577.0 => 3.577.0 @aws-sdk/credential-provider-cognito-identity: ^3.577.0 => 3.577.0 @aws-sdk/s3-request-presigner: ^3.577.0 => 3.577.0 @fortawesome/fontawesome-free: ^6.5.2 => 6.5.2 @fortawesome/fontawesome-svg-core: ^6.5.2 => 6.5.2 @fortawesome/free-brands-svg-icons: ^6.5.2 => 6.5.2 @fortawesome/free-regular-svg-icons: ^6.5.2 => 6.5.2 @fortawesome/free-solid-svg-icons: ^6.5.2 => 6.5.2 @fortawesome/react-fontawesome: ^0.2.1 => 0.2.1 aos: ^2.3.4 => 2.3.4 autoprefixer: ^10.4.19 => 10.4.19 aws-amplify: ^6.3.2 => 6.3.2 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/data: undefined () aws-amplify/data/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () bundle-optimisations: 1.0.0 dev-404-page: 1.0.0 dotenv: ^16.4.5 => 16.4.5 (7.0.0, 8.6.0, 10.0.0) functions: 1.0.0 gatsby: ^5.13.5 => 5.13.5 gatsby-omni-font-loader: ^2.0.2 => 2.0.2 gatsby-plugin-google-analytics: ^5.13.1 => 5.13.1 gatsby-plugin-google-gtag: ^5.13.1 => 5.13.1 gatsby-plugin-image: ^3.13.1 => 3.13.1 gatsby-plugin-manifest: ^5.13.1 => 5.13.1 gatsby-plugin-postcss: ^6.13.1 => 6.13.1 gatsby-plugin-sharp: ^5.13.1 => 5.13.1 gatsby-source-filesystem: ^5.13.1 => 5.13.1 gatsby-transformer-sharp: ^5.13.1 => 5.13.1 google-map-react: ^2.2.1 => 2.2.1 graphql: ^16.8.1 => 16.8.1 (15.8.0) internal-data-bridge: 1.0.0 load-babel-config: 1.0.0 lodash: ^4.17.21 => 4.17.21 partytown: 1.0.0 postcss: ^8.4.38 => 8.4.38 (7.0.39) prettier: ^3.2.5 => 3.2.5 prod-404-500: 1.0.0 react: ^18.3.1 => 18.3.1 react-dom: ^18.3.1 => 18.3.1 react-dropdown: ^1.11.0 => 1.11.0 react-google-recaptcha-v3: ^1.10.1 => 1.10.1 react-headless-accordion: ^1.0.2 => 1.0.2 react-scripts: ^5.0.1 => 5.0.1 react-table: ^7.8.0 => 7.8.0 tailwindcss: ^3.4.3 => 3.4.3 webpack-theme-component-shadowing: 1.0.0 npmGlobalPackages: @typescript-eslint/eslint-plugin: 7.7.0 corepack: 0.17.0 gatsby: 5.11.0 npm-check-updates: 16.14.12 npm-check: 6.0.1 npm: 9.5.1 vite: 5.1.3 yarn: 1.22.21 ```

Describe the bug

I query my UserLevel model and get the latest data, I update via copyOf then save it. I then do the same with my IndividualData model right after, and its sending the _version that the UserLevel call received in its response. I checked the params before the call and its the correct _version, but the actual call in that gets sent out has the version of the previous model. It will send the right _version if i put a timeout between calls, so it seems to be a race condition. I also went digging in the Datastore lib save function and see awaits in a for loop, awaits are not respected in for loops.

Expected behavior

I expect that my second call to Datastore.save() will have the version that i am sending or calculate it correctly for itself.

Reproduction steps

Call two successive DataStore.save() functions.

Code Snippet

// Put your code below this line.
    const variables = {
      approve: "rejected",
    }

const response = await DataStore.query(UserLevel, id)
const record = await DataStore.save(
  UserLevel.copyOf(response, updated => {
    Object.assign(updated, variables)
  }),
)
const response2 = await DataStore.query(IndividualData, id)
const updateIndividualData = await DataStore.save(
  IndividualData.copyOf(response2, updated => {
    Object.assign(updated, variables)
  })
)

Log output

``` // Put your logs below this line {"query":"mutation operation($input: UpdateUserLevelInput!, $condition: ModelUserLevelConditionInput) {\n updateUserLevel(input: $input, condition: $condition) {\n id\n level\n status\n owner\n account\n reason\n createdAt\n updatedAt\n _version\n _lastChangedAt\n _deleted\n }\n}\n","variables":{"input":{"id":"41cbf570-7011-7058-8347-960a5cda3f7d","level":"0","reason":"First Name Invalid","_version":94},"condition":null}} {"data":{"updateUserLevel":{"id":"41cbf570-7011-7058-8347-960a5cda3f7d","level":"0","status":false,"owner":"41cbf570-7011-7058-8347-960a5cda3f7d","account":"individual","reason":"First Name Invalid","createdAt":"2024-04-24T16:19:34.312Z","updatedAt":"2024-05-22T17:12:00.349Z","_version":95,"_lastChangedAt":1716397920363,"_deleted":null}}} {"query":"mutation operation($input: UpdateIndividualDataInput!, $condition: ModelIndividualDataConditionInput) {\n updateIndividualData(input: $input, condition: $condition) {\n id\n approve\n email\n firstname\n lastname\n nationality\n country\n residence\n poiType\n type\n createdAt\n owner\n updatedAt\n _version\n _lastChangedAt\n _deleted\n }\n}\n","variables":{"input":{"id":"41cbf570-7011-7058-8347-960a5cda3f7d","approve":"rejected","_version":95},"condition":null}} {"data":{"updateIndividualData":null},"errors":[{"path":["updateIndividualData"],"data":null,"errorType":"InvalidVersion","errorInfo":null,"locations":[{"line":2,"column":3,"sourceName":null}],"message":"Client version is greater than the corresponding server version."}]} Hub Logs from another call, the first gets processed, second fails due to the problem, it enqueues the correct _version, but sends the 102 to server: Object { event: "outboxMutationEnqueued", data: {…} } data: Object { model: class Model , element: {…} } element: Object { id: "41cbf570-7011-7058-8347-960a5cda3f7d", _version: 101, _lastChangedAt: 1716408811045, … } _deleted: null _lastChangedAt: 1716408811045 _version: 101 id: "41cbf570-7011-7058-8347-960a5cda3f7d reason: "" name: "UserLevel" Object { event: "outboxMutationEnqueued", data: {…} } data: Object { model: class Model , element: {…} } element: Object { approve: "approved", id: "41cbf570-7011-7058-8347-960a5cda3f7d", _version: 49, … } _deleted: null _lastChangedAt: 1716408811377 _version: 49 approve: "approved" id: "41cbf570-7011-7058-8347-960a5cda3f7d name: "IndividualData" Object { event: "outboxMutationProcessed", data: {…} } data: Object { model: class Model , element: {…} } element: Object { id: "41cbf570-7011-7058-8347-960a5cda3f7d", level: "1", status: false, … } _deleted: null _lastChangedAt: 1716408847655 _version: 102 account: "individual" createdAt: "2024-04-24T16:19:34.312Z" id: "41cbf570-7011-7058-8347-960a5cda3f7d" level: "1" owner: "41cbf570-7011-7058-8347-960a5cda3f7d" reason: "" status: false updatedAt: "2024-05-22T20:14:07.637Z" //Actual network call params for individual (_version 49) {"query":"mutation operation($input: UpdateIndividualDataInput!, $condition: ModelIndividualDataConditionInput) {\n updateIndividualData(input: $input, condition: $condition) {\n id\n approve\n email\n firstname\n lastname\n nationality\n country\n residence\n poiType\n type\n createdAt\n owner\n updatedAt\n _version\n _lastChangedAt\n _deleted\n }\n}\n","variables":{"input":{"id":"41cbf570-7011-7058-8347-960a5cda3f7d","approve":"approved","_version":102},"condition":null}} [WARN] 14:07.870 DataStore Object { recoverySuggestion: "Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues", localModel: {…}, message: "Client version is greater than the corresponding server version.", operation: "Update", errorType: "Unknown", errorInfo: null, process: "mutate", cause: {…}, remoteModel: null } ```

if i put a timeout of 500ms between them it works fine.

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

After finding https://github.com/aws-amplify/amplify-js/pull/7354, I am suspecting the _version in the previous requests response overwrites the mutation in the outbox because it has the same id as the previous request, even though its another model and different request. If this is the case, I will have to prevent multiple requests with same id parameter. No response

chrisbonifacio commented 5 months ago

Hi @johnmacabed 👋 thanks for raising this issue.

There have been similar issues in the past regarding consecutive saves. The workaround of adding a timeout in between calls is also consistent with addressing those issues.

I'll mark this as a bug for the team to investigate further. Thank you for the reproduction steps.

david-mcafee commented 5 months ago

@johnmacabed can you share your schema, and explain your use-case in more detail (e.g. are you depending on the _version field for some sort of validation, etc)? Is there an unintended side effect with regards to the _version not being updated before the second call?