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

Unable to query Many-to-Many Relationship in Datastore. Keep getting Invalid field modelID, model ModelName. #8139

Closed kenchoong closed 3 years ago

kenchoong commented 3 years ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

DataStore

Amplify Categories

api

Environment information

``` # Put output below this line System: OS: Windows 10 10.0.19042 CPU: (4) x64 Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz Memory: 1.71 GB / 7.94 GB Binaries: Node: 12.18.2 - C:\Program Files\nodejs\node.EXE Yarn: 1.22.4 - C:\Program Files (x86)\Yarn\bin\yarn.CMD npm: 6.14.5 - C:\Program Files\nodejs\npm.CMD Browsers: Chrome: 89.0.4389.128 Edge: Spartan (44.19041.906.0), Chromium (90.0.818.42) Internet Explorer: 11.0.19041.1 npmPackages: @chakra-ui/react: ^1.4.1 => 1.4.1 @emotion/react: 11 => 11.1.5 @emotion/styled: 11 => 11.1.5 @reach/router: ^1.3.4 => 1.3.4 @typescript-eslint/eslint-plugin: ^4.21.0 => 4.21.0 (4.19.0) @typescript-eslint/parser: ^4.21.0 => 4.21.0 (4.19.0) aws-amplify: ^3.3.26 => 3.3.26 bundle-optimisations: 1.0.0 dev-404-page: 1.0.0 eslint: ^7.23.0 => 7.23.0 (7.22.0) eslint-plugin-react: ^7.23.1 => 7.23.1 formik: ^2.2.6 => 2.2.6 framer-motion: 4 => 4.0.3 gatsby: ^3.1.2 => 3.1.2 gatsby-env-variables: ^2.0.0 => 2.0.0 gatsby-image: ^3.1.0 => 3.1.0 gatsby-plugin-gatsby-cloud: ^2.1.0 => 2.1.0 gatsby-plugin-image: ^1.1.2 => 1.1.2 gatsby-plugin-manifest: ^3.1.0 => 3.1.0 gatsby-plugin-offline: ^4.1.0 => 4.1.0 gatsby-plugin-react-helmet: ^4.1.0 => 4.1.0 gatsby-plugin-sharp: ^3.1.2 => 3.1.2 gatsby-source-filesystem: ^3.1.0 => 3.1.0 gatsby-transformer-sharp: ^3.1.0 => 3.1.0 internal-data-bridge: 1.0.0 load-babel-config: 1.0.0 mixpanel-browser: ^2.41.0 => 2.41.0 nanoid: ^3.1.22 => 3.1.22 prettier: 2.2.1 => 2.2.1 prod-404: 1.0.0 prop-types: ^15.7.2 => 15.7.2 react: ^17.0.1 => 17.0.1 react-dom: ^17.0.1 => 17.0.1 react-dropzone: ^11.3.2 => 11.3.2 react-dropzone-uploader: ^2.11.0 => 2.11.0 react-helmet: ^6.1.0 => 6.1.0 react-icons: ^4.2.0 => 4.2.0 typescript: ^4.2.4 => 4.2.4 webpack-theme-component-shadowing: 1.0.0 npmGlobalPackages: @aws-amplify/cli: 4.47.1 @react-native-community/cli: 4.10.1 aws-cdk: 1.95.2 cors-anywhere: 0.4.4 cp-cli: 2.0.0 eslint-config-airbnb-base: 14.2.0 eslint-plugin-import: 2.22.0 eslint: 7.4.0 expo-cli: 3.22.3 gatsby-cli: 2.19.1 lerna: 3.22.1 rimraf: 3.0.2 ```

Describe the bug

I have 2 model with Many-to-Many Relationship. The 2 model is Order and Product. An Order will have many Product and Product will in many Order.

So I followed this Amplify guide to group the into OrderProducts , Order and Product (Code I state in Reproduction step)

But when I query the OrderProduct model like below, in order to get a List of Products by OrderID:

import {  Product, OrderProducts } from '../models';

export const GetAllProductIdByOrderId = async (order) => {
    return await DataStore.query(OrderProducts, op => op.orderID("eq", order.id)) // this is actual orderID
}

I get this error:

Error: Invalid field for model. field: orderID, model: OrderProducts
    at Object.get (index.js:61)
    at eval (dataSource.js:103)
    at Function.ModelPredicateCreator.createFromExisting (index.js:94)
    at DataStore.eval (datastore.js:582)
    at step (datastore.js:43)
    at Object.eval [as next] (datastore.js:24)
    at fulfilled (datastore.js:15)

Expected behavior

When I query OrderProducts with orderID,

 DataStore.query(OrderProducts, op => op.orderID("eq", order.id))

I should get an array of ProductID

Reproduction steps

  1. In Schema.graphql put this 3 models
type Order @model @key(name: "byStore", fields: ["storeID"]) @auth(rules: [{allow: private, operations: [read, update, create, delete]}]) {
  id: ID!
  buyer_name: String
  order_total_amount: String
  products: [OrderProducts] @connection(keyName: "byOrder", fields: ["id"])
  created_at: AWSTimestamp
}

type OrderProducts @model @key(name: "byOrder", fields:["orderID", "productID"]) @key(name: "byProduct", fields:["productID", "orderID"]) @auth(rules: [{allow: private, operations: [read, update, create, delete]}]){
  id: ID!
  orderID: ID!
  productID: ID!
  order: Order! @connection(fields: ["orderID"])
  product: Product! @connection(fields: ["productID"])
}

type Product @model @key(name: "byStore", fields: ["storeID"]) @auth(rules: [{allow: owner, operations: [create, update, delete]}, {allow: public, provider: iam, operations: [read]}]){
  id: ID!
  product_name: String!
  product_price: String!
  created_at: AWSTimestamp
  orders: [OrderProducts] @connection(keyName: "byProduct", fields:["id"])
}
  1. amplify push in CLI
  2. Save the Order and OrderProducts relationship like this:
export const CreateOrder = async (buyerPhoneNumber, buyerName, totalAmount, ) => {
    return await DataStore.save(
        new Order({
            "buyer_name": buyerName,
            "order_total_amount": totalAmount,
            "created_at": getCurrentTimestamp(),
            "products": []
        })
    );
}

export const CreateOrderProductsRelationship = async (orderId, productId, order, product) => {
    return await DataStore.save(
        new OrderProducts({
            "orderID": orderId,
            "productID": productId,
            "order": order,
            "product": product
        })
    );
}

// save a new Order by using this
/*1st function above */
 CreateOrder(buyerPhoneNumber, buyerName, totalAmount)
                    .then(orderModel => {
                        console.log(orderModel )

                       // here save the OrderProducts relationship when successfully make an Order
                        createProductOrderRelationShip(orderModel , productModel)
                    }).catch(error => { console.log(error); })

// then save a Order and Product relationship 
/*second function above*/
const createProductOrderRelationShip = (product, order) => {

        CreateOrderProductsRelationship(order.id, product.id, order, product)
            .then(res => {
                console.log(res)

            }).catch(error => {
                console.log(error)
                setOrderResult(false)
            })
    }
  1. Then I want to get all the Product with an OrderId, so I query like this:
export const GetAllProductIdByOrderId = async (order) => {
    return await DataStore.query(OrderProducts, op => op.orderID("eq", order.id))
}
  1. But I get this error
    Error: Invalid field for model. field: orderID, model: OrderProducts

OrderProducts model have a field or orderID, I triple checked this in DynamoDB. But I can query the by orderID.

Code Snippet

// Put your code below this line.

Log output

``` // Put your logs below this line Error: Invalid field for model. field: orderID, model: OrderProducts at Object.get (index.js:61) at eval (dataSource.js:103) at Function.ModelPredicateCreator.createFromExisting (index.js:94) at DataStore.eval (datastore.js:582) at step (datastore.js:43) at Object.eval [as next] (datastore.js:24) at fulfilled (datastore.js:15) ```

aws-exports.js

const awsmobile = {
    "aws_project_region": "ap-southeast-1",
    "aws_cognito_identity_pool_id": "ap-southeast-1:f729257a-e9c1-42dd-81e2-8e48ad2de450",
    "aws_cognito_region": "ap-southeast-1",
    "aws_user_pools_id": "ap-southeast-1_2dHHzRmON",
    "aws_user_pools_web_client_id": "c9j7hv6huv6vm7debfbtulfhq",
    "oauth": {},
    "aws_user_files_s3_bucket": "coollinksbucket164006-dev",
    "aws_user_files_s3_bucket_region": "ap-southeast-1",
    "aws_appsync_graphqlEndpoint": "https://ohobeyi2ffc77im3xegzj5w3ny.appsync-api.ap-southeast-1.amazonaws.com/graphql",
    "aws_appsync_region": "ap-southeast-1",
    "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS"
};

export default awsmobile;

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

kenchoong commented 3 years ago

I followed the suggestion from this issue which suggest in order to get all ProductId in an Order, I should:

  1. Query OrderProducts by orderID, so I get a list of productID.
  2. Then I handle each productID to get all product info

Which I do exactly that. The problem now is I cant get the List of ProductID using orderID, cause I get this error Error: Invalid field for model. field: orderID, model: OrderProducts

I also followed this amplify docs, which almost identical with my model, but I still get that error

Somebody please advise, what else I should do.

kenchoong commented 3 years ago

We have a discussion in amplify discord here

Then I tried to add a queryField named getOrderByOrderIDByProductID in OrderProducts like this:

type OrderProducts @model @key(name: "byOrder", fields:["orderID", "productID"], queryField: "getOrderByOrderIDByProductID") @key(name: "byProduct", fields:["productID", "orderID"]) @auth(rules: [{allow: private, operations: [read, update, create, delete]}]){
  id: ID!
  orderID: ID!
  productID: ID!
  order: Order! @connection(fields: ["orderID"])
  product: Product! @connection(fields: ["productID"])
}

Then I have tried to query in appsync console:

query MyQuery {
  getOrderByOrderIDByProductID(filter: {orderID: {eq: "8649a9da-9ea6-4a30-afe7-6b336a8f853d"}}) {
    items {
      order {
        buyer_name
        createdAt
        id
      }
    }
  }
}

Then I get the output like this:

{
  "data": {
    "getOrderByOrderIDByProductID": null
  },
  "errors": [
    {
      "path": [
        "getOrderByOrderIDByProductID"
      ],
      "data": null,
      "errorType": "MappingTemplate",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Expression block '$[query]' requires an expression"
    }
  ]
}

You can followed the conversation discord here

kenchoong commented 3 years ago

After adding queryField: "getOrderByOrderIDByProductID") into OrderProducts @key:

Following step taken:

  1. amplify push
  2. amplify codegen

then I cant import getOrderByOrderIDByProductID into my files, cause in my model/index.js files, dont have getOrderByOrderIDByProductID field exported, therefore I get this error:

warn Attempted import error: 'getOrderByOrderIDByProductID' is not exported from '../models' (imported as 'getOrderByOrderIDByProductID').

Therefore I try to add in myself (not sure right or not, I just try, not sure the correct way)

This is how my model/index.js file look like after try to add in getOrderByOrderIDByProductID

import { initSchema } from '@aws-amplify/datastore';
import { schema } from './schema';

// of course have other stuff, I remove the non-relevant 
// I tried to add getOrderByOrderIDByProductID myself 
const { Order, OrderProducts, Product, getOrderByOrderIDByProductID  } = initSchema(schema); 

export {
  Order,
  OrderProducts,
  Product,
  getOrderByOrderIDByProductID
};

Then when I query in my file like this:

import {  Product, OrderProducts, getOrderByOrderIDByProductID } from '../models';

export const GetAllProductIdByOrderId = async (order) => {
    return await DataStore.query(getOrderByOrderIDByProductID, { filter: { orderID: { eq: order.id } } })
}

I get this error:

error Error: Constructor is not for a valid model

The whole process in start here amplify discord message

The next attempt is check with the appsync console which I mention at previous comment

kenchoong commented 3 years ago

To summarize, my goal is ONLY ONE.

With the model I shown here, I want to:

Get all productID with a given orderID

I just followed this doc and the suggestion in this issue

Not sure what I doing wrong, please give some help, I just tried a lot of thing but no luck.

Thanks

kenchoong commented 3 years ago

Any advice on this?? Cause I still didnt see any possible way

kenchoong commented 3 years ago

I also created an github discussion for this issue https://github.com/aws-amplify/amplify-js/discussions/8175#discussion-3336765

raybotha commented 3 years ago

I've also been following the docs on this and it's not clear to me why it's not working. Even though DataStore has been out for a relatively long time, a lot of simple things like these seem to not work.

kenchoong commented 3 years ago

@raybotha end up you need to move the enum to the last card of your schema, like this

https://github.com/aws-amplify/amplify-adminui/issues/188#issuecomment-832110950 Not sure in local schema.graphql, but I tried in Admin UI, it worked

And I definitely agreed what you say, a lot of simple thing is still not working and no way to see why it didnt work, end up when it happen, it consume a lot of time for the simple thing

svidgen commented 3 years ago

@kenchoong, sorry for the delayed response. I have a PR out to add an example to the docs showing how to query M:M relationships. But, the long and short of it is that queries against M:M "joiner" models like this should eagerly load related models, so you can you filter() on those models directly.

In your specific case:

Get all productID with a given orderID

You would return the products by filtering on order.id, like so:

    const productsFromOrder = (await DataStore.query(OrderProducts))
      .filter(op => op.order.id === order.id)
      .map(op => op.product);

If you're only interested in getting product ID's from an order ID (or any other javascript-expressible condition), it's a trivial change at this point.

I'm not actually sure if this interferes, but I believe you should also be skipping the orderID and productID's in your save method, as they are redundant. Just do this:

    return await DataStore.save(
        new OrderProducts({
            order: order,
            product: product
        })
    );

One last thing to note: Your model depends on multiple authentication modes, which should not have been capable of syncing as of the time this issues was raised. I think it will work now, but I'm actually not 100% sure about that, as I didn't get to testing your auth rules. TLDR: you may have some work to do to verify your auth rules to make syncing work.

kenchoong commented 3 years ago

@svidgen yup.. this worked, I can confirm that. But the auth rules I think is still a problem, but that will be another issues

kenchoong commented 3 years ago

Regarding to the auth rule u mention above, @svidgen , I using gatsby and I keep getting this error:

Error in function rejectionHandler in ./node_modules/@pmmmwh/react-refresh-webpack-plugin/client/utils/errorEventHandlers.js:46

a12

Situation is: in dev environment(with gatsby develop), it have this error(liked screenshot above), but still have Product model returned in production environment(with gatsby build), totally NOTHING IS RETURNED, totally NOTHING.

This occured when I query this Product models with the stated @auth rules,

type Product @model @key(name: "byStore", fields: ["storeID"]) 
@auth(rules: [
 # allow owner to CREATE, UPDATE, DELETE, other user List and Get
  {allow: owner, operations: [create, update, delete]},

  # allow all authenticated user to READ Product model 
  { allow: private,provider: iam, operations: [read] }, 

  # allow all unauthenticated user to READ Product model 
  {allow: public, provider: iam, operations: [read]}]) {
  id: ID!
  product_name: String!
  product_price: String!
  product_image_s3_object_key: String!
  created_at: AWSTimestamp
  storeID: ID!
  product_short_id: String!
  orders: [OrderProduct] @connection(keyName: "byProduct", fields: ["id"])
}

Query like this (when user not logged in):

export const GetProductByShortId = async (productShortId) => {
    return await DataStore.query(Product, product => product.product_short_id("eq", productShortId))
}

with the @auth rule define above. Totally no idea what is it. Cant even search any answer about this.

I asked in amplify discord here and here, for more details can refer that.

Read 100 times in this docs, not sure what I doing wrong.

iartemiev commented 3 years ago

Regarding auth, using multiple different authorization types (e.g., Cognito with IAM) was added in aws-amplify@3.4.0, so I'd make sure you're using that or a later version. You'll want to refer to this section of the docs: https://docs.amplify.aws/lib/datastore/setup-auth-rules/q/platform/js#configure-multiple-authorization-types

The "No current user" error could be happening because you're attempting to use DataStore before you've signed in / gotten credentials for Cognito or IAM.

kenchoong commented 3 years ago

@iartemiev IT WORKS!!! Finally it works, thank you very much

type Product @model @key(name: "byStore", fields: ["storeID"]) 
@auth(rules: [{allow: owner, operations: [create, update, delete]},
{ allow: private,provider: iam, operations: [read] },
 {allow: public, provider: apiKey, operations: [read]}]) {

By using this @auth rules, the unauthenticated user is able to Read the Product model. Thank you for that.

But 1 more problem,

  1. when unauthenticated user reach the website like http://my-domain/item/product-id(which will query the product with product-id) for the FIRST TIME, it unable to query the model, it return nothing.
  2. After refresh, then only will the product data get return.
  3. Tested on Google chrome, Mozilla Firefox, Microsoft Edge, all having the same behavior

Is this behavior normal?? And how can I avoid this? I want the data is available when unauthenticated user visit link at the First time.

iartemiev commented 3 years ago

I'd need to see the app code for the components that get rendered in your /item/product-id view to get a better idea.

If this is only happening the first time a user is navigating to this view, most likely DataStore hasn't finished syncing data to the local store at the time you're calling DataStore.query. All DataStore operations (e.g., save, query, observe, etc.) are performed against the local store, so upon initial visit, the store may be empty, and would therefore return [].

You can use the following pattern to eagerly start DataStore and then perform your initial query after the sync has completed:

import Amplify, {DataStore, Hub} from 'aws-amplify';

function App() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    DataStore.start();

    const listener = Hub.listen('datastore', async ({ payload: { event } }) => {
      if (event === 'ready') {
        getAllProducts();
        // initiate subscription with DataStore.observe here, if needed
      }
    });

    return () => {
      listener();
    };
  }, []);

  async function getAllProducts() {
    const records = await DataStore.query(Product);
    setProducts(records);
  }
}
kenchoong commented 3 years ago

@iartemiev Your code WORKS!!!!!!! FINALLY IT WORKS!!! THANK YOU VERY VERY MUCH

iartemiev commented 3 years ago

@kenchoong that's great to hear! Are you ok with us closing this issue?

kenchoong commented 3 years ago

@iartemiev yup.. you ok to closing this..

github-actions[bot] commented 2 years 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 *-help channels or Discussions for those types of questions.