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

Datastore Obj.copyOf function doesn't accept spread operators, but returns no errors and fails to apply changes to copied object

Open geekbleek opened this issue 2 years ago • 1 comments

Before opening, please confirm:

JavaScript Framework

React, Next.js

Amplify APIs

DataStore

Amplify Categories

api

Environment information

# Put output below this line
 System:
    OS: macOS 13.5.1
    CPU: (12) arm64 Apple M2 Pro
    Memory: 45.55 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.17.1 - ~/.local/share/rtx/installs/node/18.17.1/bin/node
    npm: 9.8.1 - ~/.local/share/rtx/installs/node/18.17.1/bin/npm
  Browsers:
    Chrome: 116.0.5845.179
    Safari: 16.6
  npmPackages:
    @ampproject/toolbox-optimizer:  undefined ()
    @auth0/auth0-spa-js: 2.0.8 => 2.0.8
    @aws-amplify/ui-react: ^5.3.0 => 5.3.0
    @aws-amplify/ui-react-internal:  undefined ()
    @aws-amplify/ui-react-storage: ^2.3.0 => 2.3.0
    @babel/core:  undefined ()
    @babel/runtime:  7.15.4
    @edge-runtime/cookies:  3.2.1
    @edge-runtime/ponyfill:  2.3.0
    @edge-runtime/primitives:  3.0.1
    @emotion/cache: 11.11.0 => 11.11.0
    @emotion/react: 11.11.1 => 11.11.1
    @emotion/server: 11.11.0 => 11.11.0
    @emotion/styled: 11.11.0 => 11.11.0
    @fontsource/inter: 5.0.4 => 5.0.4
    @fontsource/plus-jakarta-sans: 5.0.4 => 5.0.4
    @fontsource/roboto-mono: 5.0.4 => 5.0.4
    @fullcalendar/core: 6.1.8 => 6.1.8
    @fullcalendar/daygrid: 6.1.8 => 6.1.8
    @fullcalendar/interaction: 6.1.8 => 6.1.8
    @fullcalendar/list: 6.1.8 => 6.1.8
    @fullcalendar/react: 6.1.8 => 6.1.8
    @fullcalendar/timegrid: 6.1.8 => 6.1.8
    @fullcalendar/timeline: 6.1.8 => 6.1.8
    @hapi/accept:  undefined ()
    @mapbox/mapbox-gl-style-spec:  13.29.0-dev
    @mui/icons-material: 5.13.7 => 5.13.7
    @mui/lab: 5.0.0-alpha.135 => 5.0.0-alpha.135
    @mui/material: 5.13.7 => 5.13.7
    @mui/system: 5.13.7 => 5.13.7
    @mui/x-date-pickers: 6.9.1 => 6.9.1
    @napi-rs/triples:  undefined ()
    @next/font:  undefined ()
    @next/react-dev-overlay:  undefined ()
    @opentelemetry/api:  undefined ()
    @react-pdf/renderer: 3.1.12 => 3.1.12
    @reduxjs/toolkit: 1.9.5 => 1.9.5
    @reduxjs/toolkit-query:  1.0.0
    @reduxjs/toolkit-query-react:  1.0.0
    @segment/ajv-human-errors:  undefined ()
    @svgr/webpack: 8.0.1 => 8.0.1
    @types/lodash.debounce: 4.0.7 => 4.0.7
    @types/lodash.isequal: 4.5.6 => 4.5.6
    @types/lodash.throttle: 4.1.7 => 4.1.7
    @types/node: 20.4.0 => 20.4.0
    @types/nprogress: 0.2.0 => 0.2.0
    @types/numeral: 2.0.2 => 2.0.2
    @types/react: 18.2.14 => 18.2.14
    @types/react-beautiful-dnd: 13.1.4 => 13.1.4
    @types/react-dom: 18.2.6 => 18.2.6
    @types/react-draft-wysiwyg: 1.13.4 => 1.13.4
    @types/react-redux: 7.1.25 => 7.1.25
    @types/react-slick: 0.23.10 => 0.23.10
    @types/react-syntax-highlighter: 15.5.7 => 15.5.7
    @untitled-ui/icons-react: 0.1.1 => 0.1.1
    @vercel/nft:  undefined ()
    @vercel/og:  undefined ()
    acorn:  undefined ()
    amphtml-validator:  undefined ()
    anser:  undefined ()
    apexcharts: 3.41.0 => 3.41.0
    arg:  undefined ()
    assert:  undefined ()
    async-retry:  undefined ()
    async-sema:  undefined ()
    aws-amplify: ^5.3.10 => 5.3.10
    babel-packages:  undefined ()
    browserify-zlib:  undefined ()
    browserslist:  undefined ()
    buffer:  undefined ()
    bytes:  undefined ()
    chalk:  undefined ()
    ci-info:  undefined ()
    cli-select:  undefined ()
    client-only:  0.0.1
    comment-json:  undefined ()
    compression:  undefined ()
    conf:  undefined ()
    constants-browserify:  undefined ()
    content-disposition:  undefined ()
    content-type:  undefined ()
    cookie:  undefined ()
    cross-spawn:  undefined ()
    crypto-browserify:  undefined ()
    css.escape:  undefined ()
    data-uri-to-buffer:  undefined ()
    date-fns: 2.30.0 => 2.30.0
    debug:  undefined ()
    devalue:  undefined ()
    domain-browser:  undefined ()
    draft-js: 0.11.7 => 0.11.7
    edge-runtime:  undefined ()
    eslint: 8.44.0 => 8.44.0
    eslint-config-next: 13.4.8 => 13.4.8
    eslint-config-prettier: 8.8.0 => 8.8.0
    events:  undefined ()
    find-cache-dir:  undefined ()
    find-up:  undefined ()
    firebase: 9.23.0 => 9.23.0
    firebase/analytics:  undefined ()
    firebase/app:  undefined ()
    firebase/app-check:  undefined ()
    firebase/auth:  undefined ()
    firebase/auth/cordova:  undefined ()
    firebase/auth/react-native:  undefined ()
    firebase/compat:  undefined ()
    firebase/compat/analytics:  undefined ()
    firebase/compat/app:  undefined ()
    firebase/compat/app-check:  undefined ()
    firebase/compat/auth:  undefined ()
    firebase/compat/database:  undefined ()
    firebase/compat/firestore:  undefined ()
    firebase/compat/functions:  undefined ()
    firebase/compat/installations:  undefined ()
    firebase/compat/messaging:  undefined ()
    firebase/compat/performance:  undefined ()
    firebase/compat/remote-config:  undefined ()
    firebase/compat/storage:  undefined ()
    firebase/database:  undefined ()
    firebase/firestore:  undefined ()
    firebase/firestore/lite:  undefined ()
    firebase/functions:  undefined ()
    firebase/installations:  undefined ()
    firebase/messaging:  undefined ()
    firebase/messaging/sw:  undefined ()
    firebase/performance:  undefined ()
    firebase/remote-config:  undefined ()
    firebase/storage:  undefined ()
    formik: 2.4.2 => 2.4.2
    fresh:  undefined ()
    get-orientation:  undefined ()
    glob:  undefined ()
    gray-matter: 4.0.3 => 4.0.3
    gzip-size:  undefined ()
    http-proxy:  undefined ()
    http-proxy-agent:  undefined ()
    https-browserify:  undefined ()
    https-proxy-agent:  undefined ()
    i18next: 23.2.7 => 23.2.7
    icss-utils:  undefined ()
    ignore-loader:  undefined ()
    image-size:  undefined ()
    is-animated:  undefined ()
    is-docker:  undefined ()
    is-wsl:  undefined ()
    jest-worker:  undefined ()
    json5:  undefined ()
    jsonwebtoken:  undefined ()
    loader-runner:  undefined ()
    loader-utils:  undefined ()
    lodash.curry:  undefined ()
    lodash.debounce: 4.0.8 => 4.0.8
    lodash.isequal: 4.5.0 => 4.5.0
    lodash.throttle: 4.1.1 => 4.1.1
    lru-cache:  undefined ()
    mapbox-gl: 2.15.0 => 2.15.0
    micromatch:  undefined ()
    mini-css-extract-plugin:  undefined ()
    mui-one-time-password-input: 1.1.2 => 1.1.2
    nanoid:  undefined ()
    native-url:  undefined ()
    neo-async:  undefined ()
    next: 13.4.8 => 13.4.8
    node-fetch:  undefined ()
    node-html-parser:  undefined ()
    nprogress: 0.2.0 => 0.2.0
    numeral: 2.0.6 => 2.0.6
    ora:  undefined ()
    os-browserify:  undefined ()
    p-limit:  undefined ()
    path-browserify:  undefined ()
    platform:  undefined ()
    postcss-flexbugs-fixes:  undefined ()
    postcss-modules-extract-imports:  undefined ()
    postcss-modules-local-by-default:  undefined ()
    postcss-modules-scope:  undefined ()
    postcss-modules-values:  undefined ()
    postcss-preset-env:  undefined ()
    postcss-safe-parser:  undefined ()
    postcss-scss:  undefined ()
    postcss-value-parser:  undefined ()
    prettier: 3.0.0 => 3.0.0
    process:  undefined ()
    prop-types: 15.8.1 => 15.8.1
    punycode:  undefined ()
    querystring-es3:  undefined ()
    raw-body:  undefined ()
    react: 18.2.0 => 18.2.0
    react-apexcharts: 1.4.0 => 1.4.0
    react-beautiful-dnd: 13.1.1 => 13.1.1
    react-builtin:  undefined ()
    react-dom: 18.2.0 => 18.2.0
    react-dom-builtin:  undefined ()
    react-dom-experimental-builtin:  undefined ()
    react-draft-wysiwyg: 1.15.0 => 1.15.0
    react-dropzone: 14.2.3 => 14.2.3
    react-experimental-builtin:  undefined ()
    react-hot-toast: 2.4.1 => 2.4.1
    react-i18next: 13.0.1 => 13.0.1
    react-is:  18.2.0
    react-map-gl: 7.1.1 => 7.1.1
    react-markdown: 8.0.7 => 8.0.7
    react-quill: 2.0.0 => 2.0.0
    react-redux: 8.1.1 => 8.1.1 (7.2.9)
    react-refresh:  0.12.0
    react-server-dom-webpack-builtin:  undefined ()
    react-server-dom-webpack-experimental-builtin:  undefined ()
    react-slick: 0.29.0 => 0.29.0
    react-syntax-highlighter: 15.5.0 => 15.5.0
    redux: 4.2.1 => 4.2.1
    redux-devtools-extension: 2.13.9 => 2.13.9
    redux-thunk: 2.4.2 => 2.4.2
    regenerator-runtime:  0.13.4
    sass-loader:  undefined ()
    scheduler-builtin:  undefined ()
    scheduler-experimental-builtin:  undefined ()
    schema-utils:  undefined ()
    semver:  undefined ()
    send:  undefined ()
    server-only:  0.0.1
    setimmediate:  undefined ()
    shell-quote:  undefined ()
    simplebar-react: 3.2.4 => 3.2.4
    slick-carousel: 1.8.1 => 1.8.1
    source-map:  undefined ()
    stacktrace-parser:  undefined ()
    stream-browserify:  undefined ()
    stream-http:  undefined ()
    string-hash:  undefined ()
    string_decoder:  undefined ()
    strip-ansi:  undefined ()
    stylis: 4.3.0 => 4.3.0 (4.2.0)
    stylis-plugin-rtl: 2.1.1 => 2.1.1
    tar:  undefined ()
    terser:  undefined ()
    text-table:  undefined ()
    timers-browserify:  undefined ()
    tty-browserify:  undefined ()
    typescript: 5.1.6 => 5.1.6
    ua-parser-js:  undefined ()
    undici:  undefined ()
    unistore:  undefined ()
    util:  undefined ()
    vm-browserify:  undefined ()
    watchpack:  undefined ()
    web-vitals:  undefined ()
    webpack:  undefined ()
    webpack-sources:  undefined ()
    ws:  undefined ()
    yup: 1.2.0 => 1.2.0
    zod:  undefined ()
  npmGlobalPackages:
    @aws-amplify/cli: 12.3.0
    alfred-uuid: 3.0.0
    apollo: 2.34.0
    corepack: 0.18.0
    graphql-introspection-json-to-sdl: 1.0.3
    graphql: 16.8.0
    npm: 9.8.1
    openapi-to-graphql-cli: 3.0.5
    pm2: 5.3.0
    tsup: 7.2.0

Describe the bug

When using Datastore.save of a copy of an existing object as follows it works as expected

updatedItem = {foo:'bar'}
orig = await DataStore.query(Obj, id)

await DataStore.save(Obj.copyOf(orig, item => {
          Object.assign(item,updatedItem)
}

However, in a React / Next.js environment, spread operators are heavily used, and it would be expected this would be supported as well:

updatedItem = {foo:'bar'}
orig = await DataStore.query(Obj, id)

await DataStore.save(Obj.copyOf(orig, item => {
     item = {...item,...updatedItem}
}

However, this does not work. No error is given by the DataStore function - an un-mutated item is instead attempted to save (with no changes or increments of the object's version). I understand that spread operators vs object assignment have different underlying mechanisms, and spread operators create a new object vs mutating the original, but it would be ideal if this were supported.

Doing assignment from the original object to the new object does not work either.

await DataStore.save(Obj.copyOf(orig, item => {
     item = {...orig,...updatedItem}
}

Expected behavior

Spread operators should be supported in mutating a copy of an object via Datastore functions.

Perhaps the Datastore copyOf function should just return item in whatever form? I imagine mutation tracking is leveraged to only submit the fields via GraphQL that are individually mutated?

At a minimum, it should be documented that spread operators are not supported in this function as they're heavily used in front end UI frameworks.

In an ideal world, we'd be able to just receive the object, mutate/manipulate it outside of the Datastore functions, and then apply via a separate save function.

E.g.

updatedItem = {foo:'bar'}
orig = await DataStore.query(Obj, id)
newItem = {...orig,...updatedItem}
await DataStore.save(newItem)

DataStore.save should be able to validate object schema against the model vs relying on a copy. Given the copy can already be done separately from the save function, the working copy may be stale from local/backend datastore versions at the time of save anyways.

Reproduction steps

Use a spread operator to assign values to a copied item and attempt to save. The save will have no effect and generate no errors.

updatedItem = {foo:'bar'}
orig = await DataStore.query(Obj, id)

await DataStore.save(Obj.copyOf(orig, item => {
     item = {...item,...updatedItem}
}

Code Snippet

// Put your code below this line.

Log output

// Put your logs below this line

NA

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

geekbleek avatar Sep 08 '23 15:09 geekbleek

Hello, @geekbleek 👋. Appreciate you opening this issue and will be marking it as a feature request for now regarding the support of spread operators when mutating a copy of an object. To your point, if it's not supported, we should have this mentioned within our documentation. I'll review this along with the lack of errors being thrown when this happens with the team and report back!

cwomack avatar Sep 08 '23 17:09 cwomack