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.42k stars 2.12k forks source link

The source object is not a valid model #9651

Closed maxfahl closed 2 years ago

maxfahl commented 2 years ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication, GraphQL API, Storage

Amplify Categories

No response

Environment information

``` # Put output below this line System: OS: macOS 12.2 CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz Memory: 17.09 MB / 32.00 GB Shell: 5.8 - /bin/zsh Binaries: Node: 16.4.2 - /usr/local/bin/node Yarn: 1.22.15 - /usr/local/bin/yarn npm: 8.0.0 - /usr/local/bin/npm Watchman: 2021.10.11.00 - /usr/local/bin/watchman Browsers: Chrome: 98.0.4758.109 Firefox: 76.0.1 Safari: 15.3 npmPackages: @aws-amplify/datastore: ^3.7.6 => 3.7.6 @aws-amplify/ui-react: ^2.4.0 => 2.4.0 @aws-amplify/ui-react-internal: undefined () @aws-amplify/ui-react-legacy: undefined () @headlessui/react: ^1.4.3 => 1.4.3 @heroicons/react: ^1.0.5 => 1.0.5 @testing-library/jest-dom: ^5.14.1 => 5.16.2 @testing-library/react: ^12.0.0 => 12.1.3 @testing-library/user-event: ^13.2.1 => 13.5.0 @types/jest: ^27.0.1 => 27.4.0 @types/node: ^16.7.13 => 16.11.25 (17.0.18) @types/react: ^17.0.20 => 17.0.39 @types/react-dom: ^17.0.9 => 17.0.11 @typescript-eslint/eslint-plugin: ^5.12.0 => 5.12.0 autoprefixer: ^10.4.2 => 10.4.2 aws-amplify: ^4.3.14 => 4.3.14 aws-sdk: ^2.1077.0 => 2.1077.0 dotenv: ^16.0.0 => 16.0.0 (10.0.0) eslint-plugin-react: ^7.28.0 => 7.28.0 eslint-plugin-react-hooks: ^4.3.0 => 4.3.0 formik: ^2.2.9 => 2.2.9 framer-motion: ^6.2.7 => 6.2.7 graphql-tag: ^2.12.6 => 2.12.6 imagekitio-react: ^1.1.0 => 1.1.0 javascript-time-ago: ^2.3.13 => 2.3.13 javascript-time-ago-cache: 1.0.0 javascript-time-ago-gradation: 1.0.0 javascript-time-ago-prop-types: 1.0.0 javascript-time-ago-steps: 1.0.0 postcss: ^8.4.6 => 8.4.6 (7.0.39) postcss-import: ^14.0.2 => 14.0.2 prettier: ^2.5.1 => 2.5.1 prettier-plugin-tailwindcss: ^0.1.7 => 0.1.7 react: ^17.0.2 => 17.0.2 react-dom: ^17.0.2 => 17.0.2 react-image-file-resizer: ^0.4.7 => 0.4.7 react-query: ^3.34.15 => 3.34.15 react-router-dom: ^6.2.1 => 6.2.1 react-scripts: 5.0.0 => 5.0.0 swiper: ^8.0.6 => 8.0.6 swiper_angular: 0.0.1 tailwindcss: ^3.0.22 => 3.0.22 typescript: ^4.4.2 => 4.5.5 uuid: ^8.3.2 => 8.3.2 (3.4.0, 3.3.2) web-vitals: ^2.1.0 => 2.1.4 zustand: ^3.7.0 => 3.7.0 npmGlobalPackages: alfred-fkill: 0.4.1 bower: 1.8.8 cordova: 9.0.0 firebase-tools: 6.7.0 fs-extra: 8.1.0 gulp: 4.0.2 http-server: 0.11.1 ionic: 4.12.0 ios-deploy: 1.9.4 ios-sim: 8.0.1 less: 3.10.3 node-sass: 4.11.0 nodemon: 2.0.2 npm: 8.0.0 parcel-bundler: 1.12.3 tsd: 0.11.0 typescript: 3.7.2 ```

Describe the bug

When trying create copy of a model instance through Model.copyOf(), I get the error The source object is not a valid model.

The original object comes through a query generated by the amplify-cli..

The model:

type Memory @model @auth(rules: [{allow: private}]) {
  id: ID!
  title: String!
  description: String
  owner: String!
  media: [Media] @hasMany(indexName: "byMemory", fields: ["id"])
  familyID: ID! @index(name: "byFamily")
}

The query definition:

export type UpdateMemoryMutation = {
  updateMemory?:  {
    __typename: "Memory",
    id: string,
    title: string,
    description?: string | null,
    owner: string,
    media?:  {
      __typename: "ModelMediaConnection",
      items:  Array< {
        __typename: "Media",
        id: string,
        key: string,
        path: string,
        mime: string,
        title?: string | null,
        description?: string | null,
        owner: string,
        memoryID: string,
        createdAt: string,
        updatedAt: string,
      } | null >,
      nextToken?: string | null,
    } | null,
    familyID: string,
    createdAt: string,
    updatedAt: string,
  } | null,
};

And lastly the typescript function I pass to await API.graphql():

export const updateMemory = /* GraphQL */ `
  mutation UpdateMemory(
    $input: UpdateMemoryInput!
    $condition: ModelMemoryConditionInput
  ) {
    updateMemory(input: $input, condition: $condition) {
      id
      title
      description
      owner
      media {
        items {
          id
          key
          path
          mime
          title
          description
          owner
          memoryID
          createdAt
          updatedAt
        }
        nextToken
      }
      familyID
      createdAt
      updatedAt
    }
  }
`;

These are all generated through amplify api gql-compile and amplify codegen.

The root of the problem seems to be the check in isValidModelConstructor() at datastore.ts:119. where modelNamespaceMap.has(obj) returns false. I havent mutated the object in any way before this. Shouldnt my model be in this map?

I try to make a copy like this:

Memory.copyOf(editedMemory, updated => {
  updated.title = title
  updated.description = description
})

I originally get the Memory via ListMemoriesQuery also generated by amplify.

Expected behavior

Memory.copyOf(editedMemory, updated => {
  updated.title = title
  updated.description = description
})

to return a clone of the instance with new values.

Reproduction steps

I will try and put together a minimal sandbox example, but my time is extremely short at the moment. I wanted to post an issue with as much information as possible so that you possible could see what the issue might be. I'm sorry for this, you can delete this report if it is not enough.

Code Snippet

No response

Log output

``` react_devtools_backend.js:4061 [ERROR] 31:59.436 DataStore - The source object is not a valid model source: createdAt: "2022-02-24T21:32:20.866Z" description: "" familyID: "e609971c-c13d-456a-a4dc-a6b879aeb204" id: "5e5f9eb6-0d00-4449-9768-5b08b642daf1" media: {items: Array(4), nextToken: null} owner: "8c52b137-0797-4196-b8e9-1907c9807a46" title: "Rajtan tajtan" updatedAt: "2022-02-24T21:32:20.866Z" ```

aws-exports.js

const awsmobile = { "aws_project_region": "eu-north-1", "aws_cognito_identity_pool_id": "eu-north-1:17206967-ad7c-483a-88f2-6844a73f57a8", "aws_cognito_region": "eu-north-1", "aws_user_pools_id": "eu-north-1_amEMPFTZw", "aws_user_pools_web_client_id": "44bn51v6ts67f9mbnpn78nqs85", "oauth": {}, "aws_cognito_username_attributes": [ "EMAIL" ], "aws_cognito_social_providers": [], "aws_cognito_signup_attributes": [ "EMAIL" ], "aws_cognito_mfa_configuration": "OFF", "aws_cognito_mfa_types": [ "SMS" ], "aws_cognito_password_protection_settings": { "passwordPolicyMinLength": 8, "passwordPolicyCharacters": [] }, "aws_cognito_verification_mechanisms": [ "EMAIL" ], "aws_user_files_s3_bucket": "family-media102846-dev", "aws_user_files_s3_bucket_region": "eu-north-1", "aws_appsync_graphqlEndpoint": "https://pxc2ujiqsfc7boe7uo2usy6zvu.appsync-api.eu-north-1.amazonaws.com/graphql", "aws_appsync_region": "eu-north-1", "aws_appsync_authenticationType": "API_KEY", "aws_appsync_apiKey": "X" };

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

2022-02-25_08-34-31

chrisbonifacio commented 2 years ago

Hi @maxfahl 👋 thank you for raising this issue. Are you importing and using models generated by the CLI command amplify codegen models? You shared the graphql mutation type definition, which means you have a graphql folder, but do you have a models folder?

We'll also want to be certain you'd enabled DataStore for your API, which you can check by looking for this setting in your amplify/backend/api/starter/cli-inputs.json file

    ...
    "conflictResolution": {
      "defaultResolutionStrategy": {
        "type": "AUTOMERGE"
      }

OR by running amplify update api and if you see an option for Disable conflict detection then that means DataStore is already enabled. Otherwise, make sure to enable it.

maxfahl commented 2 years ago

Sorry for for the delayed response.

Are you importing and using models generated by the CLI command amplify codegen models?

Yes I am.

You shared the graphql mutation type definition, which means you have a graphql folder, but do you have a models folder?

I have run amplify codegen, amplify codegen models (isn't codegen doing models as well?) as well as amplify api gql-compile.

...which you can check by looking for this setting in your amplify/backend/api/starter/cli-inputs.json file

I had disabled conflict resolutions all together when I posted the issue, so datastore was disabled. I enabled it again and am now using the OPTIMISTIC_CONCURRENCY strategy. After that I ran all the codegen commands I listed above, but no difference. Still getting the The source object is not a valid model error. I've recompiled my react code as well, but it doesn't want to agree that it's an actual model. Also, I disabled conflict resolutions because when they're enabled, it won't delete record directly, it just flags them as deleted, and I could figure out how to tell the API to delete them directly. Also, I don't think there's a way to filter items based on the deleted flag, is there? I have lambdas hooked up for when I delete an entry, so that related entries are deleted as well, and I want them to run concurrently, so to speak. The change of deleted: true won't trigger these I presume?

For now, I will disable conflict resolution so that I can continue working, but please let me know if there's something else I can try, and I will gladly give it a shot.

For now I'm using the produce function from the immer library to mutate the object, but I have to do cleanup myself based on the model, such as deleting the createdAt and updatedAt properties.

chow11 commented 2 years ago

I am having the same/similar issue. I have a React Context with a useEffect() to start the query/subscription and providing an update function as well as the query results in the ContextProvider.

import React, { createContext, useEffect } from "react";
import { DataStore, Predicates } from 'aws-amplify';
import { Context } from './models';

export const ContextContext = createContext();

export const ContextContextProvider = ({ children }) => {
  const [context, setContext] = React.useState({});

  useEffect(() => {
    const subscription = DataStore.observeQuery(
      Context
    ).subscribe(snapshot => {
      const { items, isSynced } = snapshot;
      console.log(`[Snapshot] item count: ${items.length}, isSynced: ${isSynced}`);
      setContext(items.length ? { ...items[0] } : {});
    });

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

  const updateContext = async (data) => {
    console.log(context);
    DataStore.save(Context.copyOf(context, updated => {
      updated.field_to_change = data.field_to_change;
      console.log(updated);
  }));

  return (
    <ContextContext.Provider value={{ context, updateContext }}>
      {children}
    </ContextContext.Provider>
  );
};

The console output for both context and updated are identical except for the modified data, but I get the same error. DataStore is configured for automerge.

chrisbonifacio commented 2 years ago

@chow11 where are you running into this issue? When querying or when updating the record? Are you sure that you're passing a valid model instance to Context.copyOf and not the empty object from the initial state? It looks like setContext(items.length ? { ...items[0] } : {}); is spreading the attributes of a model instance to a normal object. I suspect the issue stems from here in your case.

DataStore only works with valid instances of the models initialized with the schema (by using initSchema, which you can see in the models/index.js file.

chrisbonifacio commented 2 years ago

@maxfahl

For now, I will disable conflict resolution so that I can continue working, but please let me know if there's something else I can try, and I will gladly give it a shot.

I would've asked to look at how you might've been getting editedMemory in your code snippet, which is being passed to Memory.copyOf. That might help me see why it's not considered a valid model by DataStore. Could be something similar to what I noticed about @chow11 's code.

https://docs.amplify.aws/lib/datastore/data-access/q/platform/js/#create-and-update

Besides that, unless you have a specific use case and/or requirement for offline capabilities, I think it would be a smoother dev experience sticking to API.graphql without DataStore.

Also, I don't think there's a way to filter items based on the deleted flag, is there?

Sounds like you might be querying with API.graphql here? The "soft delete" behavior is a necessity in DataStore-enabled APIs because other clients have to be able to sync which records should be removed from their local database.

https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html

The _deleted field on Post is used for DELETE operations. When clients are offline and records are removed from the Base table, this attribute notifies clients performing synchronization to evict items from their local cache. In cases where clients are offline for longer periods of time and the item has been removed before the client can retrieve this value with a Delta Sync query, the global catch-up event in the base query (configurable in the client) runs and removes the item from the cache. This field is marked optional because it only returns a value when running a sync query that has deleted items present.

chow11 commented 2 years ago

removing the spread of the query results when copying to the local state resolved this error. Thank you very much for that tip.

maxfahl commented 2 years ago

I would've asked to look at how you might've been getting editedMemory in your code snippet

I'm using react query together with query code generated by amplify generate, like this:

import { deleteMemory, listMemories } from '../api/memory-api'

const {
    isLoading: isMemoriesLoading,
    data: memories,
    error: memoriesFetchError,
  } = useQuery(
    MEMORY_LIST_CACHE_KEY,
    useCallback(() => listMemories(family.id), [family]),
    {
      enabled: !!family,
    }
  )

listMemories looks like this:

import { listMemories as listMemoriesQuery } from '../graphql/queries'

export const listMemories = async (familyId: string | undefined) => {
  return (
    await runQuery<ListMemoriesQuery>(listMemoriesQuery, {
      filter: {
        familyID: {
          eq: familyId,
        },
      } as ModelMemoryFilterInput,
    })
  ).data?.listMemories?.items as Memory[]
}

and finally, the listMemories query, generated by amplify generate:

export const listMemories = /* GraphQL */ `
  query ListMemories($filter: ModelMemoryFilterInput, $limit: Int, $nextToken: String) {
    listMemories(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        title
        description
        owner
        media {
          items {
            id
            key
            path
            mime
            title
            description
            owner
            memoryID
            createdAt
            updatedAt
          }
          nextToken
        }
        familyID
        createdAt
        updatedAt
      }
      nextToken
    }
  }
`

One of the memories returned by the query is then passed as a prop to a form that does the actual update. I was first thinking maybe react query did something to the object returned by the API. I removed react query from the equation, but the error was still present.

As you say, I don't have any specific use case and/or requirement, neither do I need offline capabilities, so maybe I should just skip datastore alltogether and through that disable conflict resolutions for now. Would just be handy to be able to use the Model.copyOf() helper.

Could you please be a little bit more specific on what I miss out on not using datastore?

chrisbonifacio commented 2 years ago

@maxfahl hmm, if you were using the memory record received from GraphQL that would not be a Memory instance, just regular json data. The record you're trying to update has to be queried by DataStore to get a Model instance back.

here's what I mean

Screen Shot 2022-03-01 at 10 00 15 AM

In the DataStore query result, the records are Model instances, which can be passed to Model.copyOf. The GraphQL results are not.

Could you please be a little bit more specific on what I miss out on not using datastore?

The main benefit of DataStore is the offline capability so if you don't need that, you're not missing out on much.

I think the only drawback with API.graphql is the typing suggestions since you mentioned the helper, which I assume you were using for the field suggestions? For that you'd have to be using TypeScript. If you can use TS in your project, you can configure codegen to generate a file with the type declarations for the schema by running amplify configure codegen and selecting typescript for the "Choose the code generation language target" prompt. Then run amplify codegen to generate an API.ts file.

Screen Shot 2022-03-01 at 10 24 06 AM
maxfahl commented 2 years ago

@chrisbonifacio thank you for taking the time to help me, really appreciate it. You can close the issue now.

FlGrown commented 2 years ago

@chrisbonifacio I'm am having this issue but the above don't appear to be of help. Was trying to follow data store documentation

const user = await DataStore.query(User, (u) => u.email('eq', username.VALUE));
await DataStore.save(
          User.copyOf(user, (updated) => {
            (updated.otp = otpNew), (updated.otp_expiration = otpExpirationNew);
          })
        );

I get

[ERROR] 07:45.505 DataStore - The source object is not a valid model, Object {
  "source": Array [
    User {
      "_deleted": null,
      "_lastChangedAt": 1657335450543,
      "_version": 1,
      "createdAt": "2022-07-09T02:57:30.498Z",     
      "date_of_birth": "01-01-1990",
      "email": "john@gmail.com",
      "first_name": "John",
      "id": "ace1cbea-15e6-4c77-8f31-666d06818faa",
      "last_name": "Doe",
      "otp": 123456,
      "otp_expiration": "08-01-2022",
      "updatedAt": "2022-07-09T02:57:30.498Z",     
    },
  ],
}
type User @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  first_name: String
  last_name: String
  email: String
  date_of_birth: String
  otp: Int
  otp_expiration: String
}
bc4253 commented 2 years ago

copyOf needs a single instance of the model, not an array. For example..

DataStore.save( User.copyOf(user[0], (updated) => {

FlGrown commented 2 years ago

copyOf needs a single instance of the model, not an array. For example..

DataStore.save( User.copyOf(user[0], (updated) => {

@bc4253 Thank you!

github-actions[bot] commented 1 year ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server amplify-help forum.