amplify-js icon indicating copy to clipboard operation
amplify-js copied to clipboard

DataStore fails to update new records when using auth rules

Open josephskeller opened this issue 4 years ago • 12 comments

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication, DataStore

Amplify Categories

auth, storage, api

Environment information

  System:
    OS: macOS 11.6.1
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 350.59 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.13.0 - ~/.nvm/versions/node/v16.13.0/bin/node
    npm: 8.1.0 - ~/.nvm/versions/node/v16.13.0/bin/npm
  Browsers:
    Chrome: 96.0.4664.93
    Safari: 15.1
  npmPackages:
    @aws-amplify/datastore: ^3.7.2 => 3.7.2 
    @aws-amplify/ui-react: ^2.1.4 => 2.1.4 
    @aws-amplify/ui-react-internal:  undefined ()
    @aws-amplify/ui-react-legacy:  undefined ()
    @testing-library/jest-dom: ^5.16.1 => 5.16.1 
    @testing-library/react: ^11.2.7 => 11.2.7 
    @testing-library/user-event: ^12.8.3 => 12.8.3 
    aws-amplify: ^4.3.10 => 4.3.10 
    react: ^17.0.2 => 17.0.2 
    react-dom: ^17.0.2 => 17.0.2 
    react-scripts: 4.0.3 => 4.0.3 
    web-vitals: ^1.1.2 => 1.1.2 
  npmGlobalPackages:
    @aws-amplify/cli: 7.6.3
    corepack: 0.10.0
    npm: 8.1.0

Describe the bug

Trying to update a newly created record using DataStore fails when using a @model that has an @auth rule on it.

The _version and _lastChangedAt fields are undefined when an update is invoked immediately after creating a record (See Reproduction steps for an example.) As a result, DataStore emits the following warning when trying to update the Todo record and change it's name to test:

...
errorType: "MappingTemplate"
localModel: Model {id: '42e47af8-54fc-4e78-9801-61c8bcbc2b0b', name: 'Test', _version: undefined, _lastChangedAt: undefined, _deleted: undefined}
message: "Value for field '$[_version]' must be a number."
operation: "Update"
remoteModel: null
...

Upon refreshing the page, it becomes clear that the update was unsuccessful.

This error does not occur when I am NOT using the @auth rule and Amazon Cognito User Pool with the Todo model.

Expected behavior

Update a record using DataStore.save on a newly created record should work when using @auth rules just like it works when not using @auth rules.

Reproduction steps

  1. Create new react application using npx create-react-app datastore-example.
  2. Setup local development environment by running amplify init and use the defaults.
  3. Add authentication with Default configuration and configure it to use a Username by running amplify add auth
  4. Add the DataStore
    1. Run amplify add api
    2. Select GraphQL
    3. Change the Authorization mode to Amazon Cognito User Pool
    4. Change Conflict detection to Optimistic Concurrency
    5. Choose Single object with fields (e.g., “Todo” with ID, name, description) template.
    6. Update the schema.graphql file as follows:
      type Todo @model @auth(rules: [{allow: owner }]) {
      id: ID!
      name: String!
      description: String
      }
      
  5. Generate models by running amplify codegen models
  6. Push the changes by running amplify push
    1. When asked Do you want to generate code for your newly created GraphQL API select N since we already generated models in the previous step.
  7. Next install Amplify libraries by running npm i aws-amplify @aws-amplify/ui-react @aws-amplify/datastore
  8. Set up frontend by replacing the contents of src/index.js with:
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import Amplify from "aws-amplify";
    import awsExports from "./aws-exports";
    import '@aws-amplify/ui-react/styles.css';
    
    Amplify.configure(awsExports);
    
    ReactDOM.render(
         <App/>,
         document.getElementById('root')
    );
    
  9. Also, replace the contents of src/App.js with:
    import React, {useEffect, useState} from 'react';
    import {Todo} from "./models";
    import {DataStore} from "@aws-amplify/datastore";
    import {withAuthenticator} from '@aws-amplify/ui-react'
    
    function App() {
        const [name, setName] = useState('');
        const [todos, setTodos] = useState([]);
    
        useEffect(() => {
            function fetchData() {
                DataStore.query(Todo).then((persistedTodos) => {
                    setTodos(persistedTodos);
                })
            }
            fetchData();
    
            const subscription = DataStore.observe(Todo).subscribe((value) => {
                fetchData();
            })
            return () => subscription.unsubscribe();
        }, []);
    
        async function addTodo() {
            const initialTodo = await DataStore.save(new Todo({name: ''}));
            console.log("initialTodo", initialTodo);
            let savedTodo = await DataStore.query(Todo, initialTodo.id);
            console.log("savedTodo", savedTodo);
            if (savedTodo != null) {
                let todoWithChanges = Todo.copyOf(savedTodo, draft => {
                    draft.name = name
                });
                const updatedTodo = await DataStore.save(todoWithChanges);
                console.log("updatedTodo", updatedTodo);
            }
        }
    
        return (
            <>
                <ul>
                    {todos.map(todo => (<li key={todo.id}>{todo.name} (id: {todo.id})</li>))}
                </ul>
                <input onChange={(e) => setName(e.target.value)} value={name}/>
                <button onClick={addTodo}>Add</button>
            </>
        );
    }
    
    export default withAuthenticator(App);
    
  10. Use npm run start to run the application.
  11. Create an account and complete the signin process.
  12. Open the browser development tools and view the console output
  13. Attempt to create a new todo
  14. Notice the warning message in the console stating that "Value for field '$[_version]' must be a number.". If you refresh the page, you'll notice that the update to the name of the todo was not saved.

Code Snippet

See Reproduction steps for a complete sample.

The following should work when using an @auth rule but it doesn't. If an @auth rule is not used, it works fine.

const name = 'Test';
const initialTodo = await DataStore.save(new Todo({name: ''}));
let savedTodo = await DataStore.query(Todo, initialTodo.id);
if (savedTodo != null) {
    let todoWithChanges = Todo.copyOf(savedTodo, draft => {
        draft.name = name
    });
    const updatedTodo = await DataStore.save(todoWithChanges);
}

josephskeller avatar Dec 13 '21 19:12 josephskeller

Hi @josephskeller 👋 Thanks for raising this issue and providing great steps to reproduce. I was able to reproduce this issue and will label it as a bug for the team to investigate further.

chrisbonifacio avatar Dec 13 '21 22:12 chrisbonifacio

UPDATE: I'm not totally sure if this is a bug now, because I noticed that editing/updating a record separately doesn't seem to cause this behavior.

Is there a reason you're trying to update a record in the same function you're creating it?

chrisbonifacio avatar Dec 13 '21 22:12 chrisbonifacio

@chrisbonifacio, thank you for looking into this issue. To answer your question, part of the reason for updating the record in the same function that created it was to illustrate the same type of issue I'm having in a larger application. The issue also occurs when updating a record before refreshing the browser window.

For example, here is an update to the existing code that allows users to edit a todo. Simply replace the App.js with the following:

import React, { useEffect, useState } from 'react';
import { Todo } from './models';
import { DataStore } from '@aws-amplify/datastore';
import { withAuthenticator } from '@aws-amplify/ui-react';

function App() {
  const [newName, setNewName] = useState('');
  const [name, setName] = useState('');
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    function fetchData() {
      DataStore.query(Todo).then((persistedTodos) => {
        setTodos(persistedTodos);
      });
    }

    fetchData();

    const subscription = DataStore.observe(Todo).subscribe((value) => {
      fetchData();
    });
    return () => subscription.unsubscribe();
  }, []);

  async function addTodo() {
    const initialTodo = await DataStore.save(new Todo({ name: '' }));
    let savedTodo = await DataStore.query(Todo, initialTodo.id);
    if (savedTodo != null) {
      let todoWithChanges = Todo.copyOf(savedTodo, draft => {
        draft.name = name;
      });
      const updatedTodo = await DataStore.save(todoWithChanges);
    }
    setName("");
  }

  async function updateTodo(id, name) {
    let savedTodo = await DataStore.query(Todo, id);
    if (savedTodo != null) {
      let todoWithChanges = Todo.copyOf(savedTodo, draft => {
        draft.name = name;
      });
      const updatedTodo = await DataStore.save(todoWithChanges);
    }
  }

  return (
    <>
      <ul>
        {todos.map(todo => (<li key={todo.id}>
          <input type='text' value={todo.name}
                 onInput={(e) => updateTodo(todo.id, e.target.value)} /> (id: {todo.id})
        </li>))}
      </ul>
      <input onChange={(e) => setName(e.target.value)} value={name} />
      <button onClick={addTodo}>Add</button>
    </>
  );
}

export default withAuthenticator(App);

You'll notice that when the user tries to type in a new name, the update fails because the _version is still undefined, even a few seconds after from the time the record was created. So, as far as I can tell, editing/updating a record separately does indeed result in the record failing to update.

josephskeller avatar Dec 13 '21 23:12 josephskeller

The issue seems to be intermittent for me. At first I get the error but the records do seem to update and even persist to DynamoDB as I am able to retrieve them with the updated values in the console. Doesn't seem that DataStore fails to update the record, but there does seem to be an issue with the subscription when performing consecutive saves.

chrisbonifacio avatar Dec 14 '21 17:12 chrisbonifacio

Did you refresh the page between the time the record was created and updated? If so, yes, it will work. But, from as far as I can tell, if you don't refresh the browser, it won't work. Refreshing the browser causes the _version and related fields to be populated which allows updates to work.

josephskeller avatar Dec 14 '21 18:12 josephskeller

Any update on this?

josephskeller avatar Dec 30 '21 17:12 josephskeller

I have the same issue, but it seems to only happen if I create and delete/update before DataStore syncs. If I create something and quickly delete (before it syncs to cloud), I get this error. If I wait 1-2secs after creating, I do not have this error when deleting/updating.

otaviojacobi avatar Jan 20 '22 15:01 otaviojacobi

I have the same issue with updating records after first creation, but it also seems to extend to updating any existing record more than once after querying it too. If i query a record and the _version is 1 for example, i can do a Datastore.save() and the changes are properly synced to the cloud, but when i look at the object the _version is still 1 (even if i try to re-query the record again), and any further updates don't sync, though they are stored locally. Once i refresh the page, i get _version = 2 and i can make one update before it fails again.

socialsuiteDan avatar Feb 25 '22 04:02 socialsuiteDan

@chrisbonifacio Any updates on this? I am have the exact same issue as described by @socialsuiteDan . I can save a Model multiple times to DataStore locally but the objects version locally never changes until I refresh the browser where the remote versions number has increased by the number of times it has been saved. If I subscribe to Hub I can see the model is syncing in there and returning the new version number but the object returned from DataStore.save() or DataStore.query() is the same until the browser has been refreshed.

I am using aws-amplify version 4.3.16. Below is image of the Chrome console showing return data from the DataStore and the Hub after doing a save: image

bobalucard avatar Mar 12 '22 11:03 bobalucard

any updates on this?

Parajulibkrm avatar May 21 '22 09:05 Parajulibkrm

is this still pending?

rkushkuley avatar Jun 15 '22 15:06 rkushkuley

Does anyone have a simple workaround for this? I am running into the same issue where DataStore Create returns the object with undefined values for createdAt, updatedAt, and _version. If I fetch it from Datastore before refresh those values are undefined, however if I use a subscription from observeQuery of Datastore, those values are populated. Same with using the GraphQL client api.

rlefflerElite avatar Sep 13 '22 17:09 rlefflerElite

I'm not able to reproduce this on the latest version. This bug must have been inadvertently fixed with some other change. Please comment if you are still experiencing the issue.

dpilch avatar Mar 01 '23 16:03 dpilch

I am still facing this issue with amplify 5.0.7

ujjwal-neu avatar Sep 12 '23 14:09 ujjwal-neu