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.12k forks source link

Unable to run client.models.*.update with data coming from client.models.*.observeQuery #13470

Closed amuresia closed 4 months ago

amuresia commented 4 months ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

api

Backend

Amplify Gen 2 (Preview)

Environment information

``` System: OS: macOS 14.4.1 CPU: (8) arm64 Apple M1 Pro Memory: 47.09 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.11.0 - ~/.nvm/versions/node/v20.11.0/bin/node Yarn: 1.22.19 - /opt/homebrew/bin/yarn npm: 10.2.4 - ~/.nvm/versions/node/v20.11.0/bin/npm Browsers: Chrome: 125.0.6422.142 Safari: 17.4.1 npmPackages: %name%: 0.1.0 @aws-amplify/backend: ^1.0.2 => 1.0.3 @aws-amplify/backend-cli: ^1.0.1 => 1.0.4 @aws-amplify/cli-extensibility-helper: ^3.0.30 => 3.0.30 @aws-amplify/ui-react: ^6.1.9 => 6.1.12 @aws-amplify/ui-react-internal: undefined () @aws-cdk/dns_validated_certificate_handler: undefined (0.0.0) @aws-sdk/client-cognito-identity-provider: ^3.583.0 => 3.590.0 @lexical/react: ^0.15.0 => 0.15.0 @lexical/utils: ^0.15.0 => 0.15.0 @types/react: ^18.2.66 => 18.3.3 @types/react-dom: ^18.2.22 => 18.3.0 @typescript-eslint/eslint-plugin: ^7.2.0 => 7.12.0 @typescript-eslint/parser: ^7.2.0 => 7.12.0 @vitejs/plugin-react: ^4.2.1 => 4.3.0 aws-amplify: ^6.2.0 => 6.3.5 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 () aws-cdk: ^2.138.0 => 2.144.0 aws-cdk-lib: ^2.138.0 => 2.144.0 (2.80.0) bootstrap: ^5.3.3 => 5.3.3 constructs: ^10.3.0 => 10.3.0 date-fns: ^3.6.0 => 3.6.0 esbuild: ^0.20.2 => 0.20.2 eslint: ^8.57.0 => 8.57.0 eslint-plugin-react-hooks: ^4.6.0 => 4.6.2 eslint-plugin-react-refresh: ^0.4.6 => 0.4.7 lexical: ^0.15.0 => 0.15.0 material-icons: ^1.13.12 => 1.13.12 react: ^18.2.0 => 18.3.1 react-bootstrap: ^2.10.2 => 2.10.2 react-bootstrap/AbstractModalHeader: undefined () react-bootstrap/Accordion: undefined () react-bootstrap/AccordionBody: undefined () react-bootstrap/AccordionButton: undefined () react-bootstrap/AccordionCollapse: undefined () react-bootstrap/AccordionContext: undefined () react-bootstrap/AccordionHeader: undefined () react-bootstrap/AccordionItem: undefined () react-bootstrap/AccordionItemContext: undefined () react-bootstrap/Alert: undefined () react-bootstrap/AlertHeading: undefined () react-bootstrap/AlertLink: undefined () react-bootstrap/Anchor: undefined () react-bootstrap/Badge: undefined () react-bootstrap/BootstrapModalManager: undefined () react-bootstrap/Breadcrumb: undefined () react-bootstrap/BreadcrumbItem: undefined () react-bootstrap/Button: undefined () react-bootstrap/ButtonGroup: undefined () react-bootstrap/ButtonToolbar: undefined () react-bootstrap/Card: undefined () react-bootstrap/CardBody: undefined () react-bootstrap/CardFooter: undefined () react-bootstrap/CardGroup: undefined () react-bootstrap/CardHeader: undefined () react-bootstrap/CardHeaderContext: undefined () react-bootstrap/CardImg: undefined () react-bootstrap/CardImgOverlay: undefined () react-bootstrap/CardLink: undefined () react-bootstrap/CardSubtitle: undefined () react-bootstrap/CardText: undefined () react-bootstrap/CardTitle: undefined () react-bootstrap/Carousel: undefined () react-bootstrap/CarouselCaption: undefined () react-bootstrap/CarouselItem: undefined () react-bootstrap/CloseButton: undefined () react-bootstrap/Col: undefined () react-bootstrap/Collapse: undefined () react-bootstrap/Container: undefined () react-bootstrap/Dropdown: undefined () react-bootstrap/DropdownButton: undefined () react-bootstrap/DropdownContext: undefined () react-bootstrap/DropdownDivider: undefined () react-bootstrap/DropdownHeader: undefined () react-bootstrap/DropdownItem: undefined () react-bootstrap/DropdownItemText: undefined () react-bootstrap/DropdownMenu: undefined () react-bootstrap/DropdownToggle: undefined () react-bootstrap/ElementChildren: undefined () react-bootstrap/Fade: undefined () react-bootstrap/Feedback: undefined () react-bootstrap/Figure: undefined () react-bootstrap/FigureCaption: undefined () react-bootstrap/FigureImage: undefined () react-bootstrap/FloatingLabel: undefined () react-bootstrap/Form: undefined () react-bootstrap/FormCheck: undefined () react-bootstrap/FormCheckInput: undefined () react-bootstrap/FormCheckLabel: undefined () react-bootstrap/FormContext: undefined () react-bootstrap/FormControl: undefined () react-bootstrap/FormFloating: undefined () react-bootstrap/FormGroup: undefined () react-bootstrap/FormLabel: undefined () react-bootstrap/FormRange: undefined () react-bootstrap/FormSelect: undefined () react-bootstrap/FormText: undefined () react-bootstrap/Image: undefined () react-bootstrap/InputGroup: undefined () react-bootstrap/InputGroupContext: undefined () react-bootstrap/InputGroupText: undefined () react-bootstrap/ListGroup: undefined () react-bootstrap/ListGroupItem: undefined () react-bootstrap/Modal: undefined () react-bootstrap/ModalBody: undefined () react-bootstrap/ModalContext: undefined () react-bootstrap/ModalDialog: undefined () react-bootstrap/ModalFooter: undefined () react-bootstrap/ModalHeader: undefined () react-bootstrap/ModalTitle: undefined () react-bootstrap/Nav: undefined () react-bootstrap/NavContext: undefined () react-bootstrap/NavDropdown: undefined () react-bootstrap/NavItem: undefined () react-bootstrap/NavLink: undefined () react-bootstrap/Navbar: undefined () react-bootstrap/NavbarBrand: undefined () react-bootstrap/NavbarCollapse: undefined () react-bootstrap/NavbarContext: undefined () react-bootstrap/NavbarOffcanvas: undefined () react-bootstrap/NavbarText: undefined () react-bootstrap/NavbarToggle: undefined () react-bootstrap/Offcanvas: undefined () react-bootstrap/OffcanvasBody: undefined () react-bootstrap/OffcanvasHeader: undefined () react-bootstrap/OffcanvasTitle: undefined () react-bootstrap/OffcanvasToggling: undefined () react-bootstrap/Overlay: undefined () react-bootstrap/OverlayTrigger: undefined () react-bootstrap/PageItem: undefined () react-bootstrap/Pagination: undefined () react-bootstrap/Placeholder: undefined () react-bootstrap/PlaceholderButton: undefined () react-bootstrap/Popover: undefined () react-bootstrap/PopoverBody: undefined () react-bootstrap/PopoverHeader: undefined () react-bootstrap/ProgressBar: undefined () react-bootstrap/Ratio: undefined () react-bootstrap/Row: undefined () react-bootstrap/SSRProvider: undefined () react-bootstrap/Spinner: undefined () react-bootstrap/SplitButton: undefined () react-bootstrap/Stack: undefined () react-bootstrap/Switch: undefined () react-bootstrap/Tab: undefined () react-bootstrap/TabContainer: undefined () react-bootstrap/TabContent: undefined () react-bootstrap/TabPane: undefined () react-bootstrap/Table: undefined () react-bootstrap/Tabs: undefined () react-bootstrap/ThemeProvider: undefined () react-bootstrap/Toast: undefined () react-bootstrap/ToastBody: undefined () react-bootstrap/ToastContainer: undefined () react-bootstrap/ToastContext: undefined () react-bootstrap/ToastFade: undefined () react-bootstrap/ToastHeader: undefined () react-bootstrap/ToggleButton: undefined () react-bootstrap/ToggleButtonGroup: undefined () react-bootstrap/Tooltip: undefined () react-bootstrap/TransitionWrapper: undefined () react-bootstrap/createChainedFunction: undefined () react-bootstrap/createUtilityClasses: undefined () react-bootstrap/createWithBsPrefix: undefined () react-bootstrap/divWithClassName: undefined () react-bootstrap/getInitialPopperStyles: undefined () react-bootstrap/getTabTransitionComponent: undefined () react-bootstrap/helpers: undefined () react-bootstrap/safeFindDOMNode: undefined () react-bootstrap/transitionEndListener: undefined () react-bootstrap/triggerBrowserReflow: undefined () react-bootstrap/types: undefined () react-bootstrap/useOverlayOffset: undefined () react-bootstrap/usePlaceholder: undefined () react-bootstrap/useWrappedRefWithWarning: undefined () react-dom: ^18.2.0 => 18.3.1 react-feather: ^2.0.10 => 2.0.10 react-infinite-scroll-component: ^6.1.0 => 6.1.0 react-router-dom: ^6.23.1 => 6.23.1 sass: ^1.77.2 => 1.77.4 styled-components: ^6.1.11 => 6.1.11 styled-components/native: undefined () tsx: ^4.7.2 => 4.11.2 typescript: ^5.4.5 => 5.4.5 (4.4.4, 4.9.5) vite: ^5.2.0 => 5.2.12 npmGlobalPackages: @aws-amplify/cli: 12.11.0 corepack: 0.23.0 npm: 10.2.4 ts-node: 10.9.2 typescript: 5.4.5 ```

Describe the bug

Items returned by subscribers are not usable out of the box if they have nested relationships that are optional. If the relationship is not defined then it is set to null and trying to use the object for an update operation requires deconstructing it first.

const { goal, ...todoWithoutGoal } = todo;
const todoToUpdate = goal ? todo : todoWithoutGoal;
await client.models.Todo.update(todoToUpdate);

Expected behavior

client.models.Todo.update(updatedTodo) should return a successful response if the goal is not set.

Reproduction steps

  1. Setup a data schema i.e. a todo and user with a has-many relationship

    Todo: a
      .model({
        title: a.string().required(),
        goalId: a.id(),
        goal: a.belongsTo("Goal", "goalId"),
      })
      .authorization((allow) => [
        allow.owner(),
        allow.group("Users").to(["read"]),
        allow.group("Administrators"),
      ]),
    
    Goal: a
      .model({
        title: a.string().required(),
        todos: a.hasMany("Todo", "goalId"),
      })
      .authorization((allow) => [
        allow.owner(),
        allow.group("Users").to(["read"]),
        allow.group("Administrators"),
      ]),
  2. Setup a component to subscribe to Todo like
  3. Create a new todo without a goal
  4. Implement the update todo functionality, copy the todo returned by the subscription into a new const i.e. updatedTodo and change just the title, leaving the goal as is (empty).
  5. Use updatedTodo with client.models.Todo.update(updatedTodo)
  6. The function returns
    TypeError: null is not an object (evaluating 'inputValue[associatedFieldName]')

Code Snippet

// Put your code below this line.

Log output

``` TypeError: null is not an object (evaluating 'inputValue[associatedFieldName]') ```

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

No response

chrisbonifacio commented 4 months ago

Hi @amuresia 👋 thanks for raising this issue!

I wasn't able to reproduce the exact error message mentioned in the description but with the following reproduction code:

"use client";

import { Schema } from "@/amplify/data/resource";
import { generateClient } from "aws-amplify/api";
import { useEffect, useState } from "react";
import { Amplify } from "aws-amplify";
import amplify_outputs from "@/amplify_outputs.json";

Amplify.configure(amplify_outputs);

const client = generateClient<Schema>();

type Todo = Schema["Todo"]["type"];

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>([]);

  const updateTodo = async () => {
    const todo = todos[0];
    todo.title = "updated title";

    const { data, errors } = await client.models.Todo.update(todo);

    if (errors) {
      console.error(errors);
    } else {
      console.log(data);
    }
  };

  useEffect(() => {
    const sub = client.models.Todo.observeQuery().subscribe(
      async ({ isSynced, items }) => {
        if (isSynced) {
          console.log(items);
          setTodos(items);
        }
      }
    );

    return () => {
      sub.unsubscribe();
    };
  }, []);

  async function createTodo() {
    const { data: todo, errors } = await client.models.Todo.create({
      title: "my todo",
    });
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <button onClick={createTodo}>create todo</button>
      <button onClick={updateTodo}>update todo</button>
    </main>
  );
}

I get the following error when calling client.models.Todo.update() and pass in a todo returned from observeQuery:

image

It might be worth noting that the update function is only expecting a few fields, so createdAt and updatedAt also shouldn't be present.

image

I don't think this is a bug, the shape of the data returned from queries and subscriptions isn't really meant to be passed as is to mutations. The mutations expect fields that are on the schema and for relational fields, an id but not the relational type as an input.

I would recommend constructing the input in the shape expected by the mutation type.

amuresia commented 4 months ago

Hi @chrisbonifacio, if this is the way it works by design then that's fine. It was a bit of a grey area and I thought I should raise it. I can construct new elements with just the "required" properties whenever the subscription is triggered. Thanks for taking the time to look into it!