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

Referencing Deeper Object Data for Parent Objects in Amplify Gen 2 #13363

Closed ChristopherGabba closed 4 months ago

ChristopherGabba commented 4 months ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

GraphQL API, DataStore

Amplify Version

v6

Amplify Categories

api

Backend

Amplify Gen 2 (Preview)

Environment information

``` System: OS: macOS 14.4.1 CPU: (10) arm64 Apple M2 Pro Memory: 881.88 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.7.0 - /opt/homebrew/bin/node Yarn: 1.22.22 - /opt/homebrew/bin/yarn npm: 10.1.0 - /opt/homebrew/bin/npm Watchman: 2023.09.04.00 - /opt/homebrew/bin/watchman Browsers: Safari: 17.4.1 npmPackages: %name%: 0.1.0 @aws-amplify/backend: ^1.0.1 => 1.0.1 @aws-amplify/backend-cli: ^1.0.2 => 1.0.2 @aws-amplify/react-native: ^1.1.0 => 1.1.0 @aws-amplify/ui-react-native: ^2.2.0 => 2.2.0 @babel/core: ^7.20.0 => 7.24.5 @babel/plugin-proposal-export-namespace-from: ^7.18.9 => 7.18.9 @babel/plugin-proposal-optional-chaining: ^7.0.0 => 7.21.0 @babel/plugin-transform-arrow-functions: ^7.0.0 => 7.24.1 @babel/plugin-transform-nullish-coalescing-operator: ^7.0.0 => 7.24.1 @babel/plugin-transform-shorthand-properties: ^7.0.0 => 7.24.1 @babel/plugin-transform-template-literals: ^7.0.0 => 7.24.1 @babel/preset-env: ^7.20.0 => 7.24.5 @babel/runtime: ^7.20.0 => 7.24.5 @config-plugins/ffmpeg-kit-react-native: ^8.0.0 => 8.0.0 @expo-google-fonts/m-plus-1p: ^0.2.3 => 0.2.3 @expo-google-fonts/montserrat: ^0.2.3 => 0.2.3 @expo/config-plugins: ~8.0.0 => 8.0.4 (7.9.2) @expo/metro-runtime: ~3.1.3 => 3.1.3 @gorhom/bottom-sheet: ^4.6.1 => 4.6.1 @react-native-async-storage/async-storage: ^1.23.1 => 1.23.1 @react-native-community/netinfo: ^11.3.1 => 11.3.1 @react-navigation/bottom-tabs: ^6.5.20 => 6.5.20 @react-navigation/native: ^6.0.2 => 6.1.17 @react-navigation/native-stack: ^6.0.2 => 6.9.26 @sentry/react-native: ~5.22.0 => 5.22.2 @shopify/flash-list: 1.6.4 => 1.6.4 @types/i18n-js: 3.8.2 => 3.8.2 @types/jest: ^29.2.1 => 29.5.12 @types/lodash.filter: ^4.6.9 => 4.6.9 @types/react: ~18.2.14 => 18.2.79 @types/react-test-renderer: ^18.0.0 => 18.3.0 @typescript-eslint/eslint-plugin: ^5.59.0 => 5.62.0 @typescript-eslint/parser: ^5.59.0 => 5.62.0 ContextAPIMixpanel: 0.0.1 HelloWorld: 0.0.1 MixpanelDemo: 0.0.1 SimpleMixpanel: 0.0.1 apisauce: 3.0.1 => 3.0.1 aws-amplify: ^6.3.0 => 6.3.0 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.141.0 => 2.141.0 aws-cdk-lib: ^2.141.0 => 2.141.0 babel-jest: ^29.2.1 => 29.7.0 cheerio: ^1.0.0-rc.12 => 1.0.0-rc.12 constructs: ^10.3.0 => 10.3.0 date-fns: ^2.30.0 => 2.30.0 esbuild: ^0.21.1 => 0.21.1 (0.20.2) eslint: 8.17.0 => 8.17.0 eslint-config-prettier: 8.5.0 => 8.5.0 eslint-config-standard: 17.0.0 => 17.0.0 eslint-plugin-import: 2.26.0 => 2.26.0 eslint-plugin-n: ^15.0.0 => 15.7.0 eslint-plugin-promise: 6.0.0 => 6.0.0 eslint-plugin-react: 7.30.0 => 7.30.0 eslint-plugin-react-native: 4.0.0 => 4.0.0 eslint-plugin-reactotron: ^0.1.2 => 0.1.4 ex: ^0.1.4 => 0.1.4 expo: ^51.0.2 => 51.0.2 expo-application: ~5.9.1 => 5.9.1 expo-av: ~14.0.4 => 14.0.4 expo-blur: ~13.0.2 => 13.0.2 expo-build-properties: ^0.12.1 => 0.12.1 expo-clipboard: ~6.0.3 => 6.0.3 expo-constants: ^16.0.1 => 16.0.1 expo-contacts: ~13.0.3 => 13.0.3 expo-dev-client: ~4.0.13 => 4.0.13 expo-device: ~6.0.2 => 6.0.2 expo-font: ~12.0.4 => 12.0.4 expo-haptics: ~13.0.1 => 13.0.1 expo-image-picker: ~15.0.4 => 15.0.4 expo-linear-gradient: ~13.0.2 => 13.0.2 expo-linking: ~6.3.1 => 6.3.1 expo-localization: ~15.0.3 => 15.0.3 expo-modules-autolinking: ^1.11.1 => 1.11.1 expo-notifications: ^0.28.1 => 0.28.1 expo-secure-store: ~13.0.1 => 13.0.1 expo-share-intent: ^2.0.0 => 2.0.0 expo-splash-screen: ^0.27.4 => 0.27.4 expo-status-bar: ~1.12.1 => 1.12.1 expo-store-review: ~7.0.2 => 7.0.2 expo-updates: ^0.25.11 => 0.25.11 expo-video-thumbnails: ~8.0.0 => 8.0.0 ffmpeg-kit-react-native: ^6.0.2 => 6.0.2 i18n-js: 3.9.2 => 3.9.2 jest: ^29.2.1 => 29.7.0 jest-expo: ~51.0.1 => 51.0.1 libphonenumber-js: ^1.11.1 => 1.11.1 (1.9.47) libphonenumber-js-core: undefined (1.0.0) libphonenumber-js-max: undefined (1.0.0) libphonenumber-js-min: undefined (1.0.0) libphonenumber-js-mobile: undefined (1.0.0) libphonenumber-js/build: undefined () libphonenumber-js/core: undefined () libphonenumber-js/max: undefined () libphonenumber-js/max/metadata: undefined () libphonenumber-js/min: undefined () libphonenumber-js/min/metadata: undefined () libphonenumber-js/mobile: undefined () libphonenumber-js/mobile/examples: undefined () libphonenumber-js/mobile/metadata: undefined () lodash: ^4.17.21 => 4.17.21 lodash.filter: ^4.6.0 => 4.6.0 lottie-react-native: 6.7.0 => 6.7.0 mixpanel-react-native: ^3.0.2 => 3.0.3 mixpanelexpo: 1.0.0 mobx: 6.10.2 => 6.10.2 mobx-react-lite: 4.0.5 => 4.0.5 mobx-state-tree: 5.3.0 => 5.3.0 patch-package: 6.4.7 => 6.4.7 postinstall-prepare: 1.0.1 => 1.0.1 prettier: 2.8.8 => 2.8.8 (2.3.2, 1.19.1) react: 18.2.0 => 18.2.0 react-dom: 18.2.0 => 18.2.0 react-native: 0.74.1 => 0.74.1 react-native-blurhash: ^2.0.2 => 2.0.2 react-native-compressor: ^1.8.24 => 1.8.24 react-native-context-menu-view: ^1.16.0 => 1.16.0 react-native-device-info: ^10.13.2 => 10.13.2 react-native-fs: ^2.20.0 => 2.20.0 react-native-gesture-handler: ~2.16.1 => 2.16.2 react-native-get-random-values: ^1.11.0 => 1.11.0 react-native-mime-types: ^2.5.0 => 2.5.0 react-native-mmkv: ^2.12.2 => 2.12.2 react-native-reanimated: ~3.10.1 => 3.10.1 react-native-safe-area-context: ^4.10.1 => 4.10.1 react-native-screens: 3.31.1 => 3.31.1 react-native-static-safe-area-insets: ^2.2.0 => 2.2.0 react-native-touchable-scale: ^2.2.0 => 2.2.0 react-native-url-polyfill: ^2.0.0 => 2.0.0 react-native-vision-camera: ^4.0.3 => 4.0.3 react-native-volume-manager: ^1.10.0 => 1.10.0 react-native-web: ~0.19.6 => 0.19.11 react-native-webview: 13.8.6 => 13.8.6 react-native-youtube-iframe: ^2.3.0 => 2.3.0 react-test-renderer: 18.2.0 => 18.2.0 reactotron-core-client: ^2.8.13 => 2.9.3 reactotron-mst: ^3.1.7 => 3.1.9 reactotron-react-js: ^3.3.11 => 3.3.14 reactotron-react-native: ^5.0.5 => 5.1.6 ts-jest: ^29.1.1 => 29.1.2 ts-node: ^10.9.2 => 10.9.2 tsx: ^4.9.4 => 4.10.0 typescript: ^5.4.5 => 5.4.5 (4.4.4, 4.9.5) uuid: ^9.0.1 => 9.0.1 (8.3.2, 3.3.2, 7.0.3) npmGlobalPackages: @aws-amplify/cli-internal: 12.12.0 @aws-amplify/cli: 12.11.0 @react-native-community/netinfo: 9.4.1 eas-cli: 9.0.3 expo-cli: 6.3.10 firebase-tools: 11.24.1 n: 9.1.0 node-gyp: 10.0.1 node: 20.6.0 npm: 10.7.0 pod-install: 0.2.0 react-native-spinkit: 1.5.1 typescript: 5.4.5 yarn: 1.22.22 ```

Describe the bug

I have set up a Gen 2 backend deployment following the steps in the docs and here is my data/resource.ts setup:

  User: a
    .model({
      id: a.id().required(),
      birthdate: a.string().required(),
      firstName: a.string().required(),
      lastName: a.string().required(),
      username: a.string().required(),
      phoneNumber: a.phone().required(),
      pushToken: a.string(),
      profileImage: a.url(),
      profileImageBlurhash: a.string(),
      searchTerm: a.string().required(),
      sentFriendships: a.hasMany("Friendship", "senderId"),
      receivedFriendships: a.hasMany("Friendship", "receiverId"),
    })
    .secondaryIndexes((index) => [
      index("phoneNumber").queryField("listUsersByPhoneNumber"),
      index("searchTerm").queryField("listUsersBySearchTerm").sortKeys(["id"]),
    ])
    .authorization((allow) => [allow.owner()]),

  Friendship: a
    .model({
      id: a.id().required(),
      receiverId: a.id().required(),
      receiver: a.belongsTo("User", "receiverId"),
      senderId: a.id().required(),
      sender: a.belongsTo("User", "senderId"),
      status: a.ref("FriendStatus").required(),
    })
    .authorization((allow) => [allow.publicApiKey()])
    .secondaryIndexes((index) => [
      index("senderId")
        .name("bySender")
        .sortKeys(["receiverId"])
        .queryField("listFriendshipsBySenderId"),
      index("receiverId")
        .name("byReceiver")
        .sortKeys(["senderId"])
        .queryField("listFriendshipsByReceiverId"),
    ]),

Within the app, I use my secondary index query like so:

      const response = await client.models.Friendship.listFriendshipsByReceiverId({
          receiverId: currentUser.userId,
      })

      // should return a friendship object

      const firstFriendshipReceiver = response.data[0].receiver
      console.log(firstFriendshipReceiver) // logs: [Function anonymous]

      const firstFriendshipReceiver = response.data[0].receiver() // added function parenthesis
      console.log(firstFriendshipReceiver) // logs {"_A": null, "_x": 0, "_y": 3, "_z": {"_A": null, "_x": 0, "_y": 0, "_z": null}}

Expected behavior

I want to be able to reference response.data[0].receiver.firstName but I can't reference any user properties from my friendship query.

Reproduction steps

  1. Use the data structure and code above to try to reference user data through the friendship query results.

Code Snippet

Refer to issue description above.

Log output

``` // Put your logs below this line ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

iPhone 12

Mobile Operating System

iOS 17.2.1

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

Using Expo 51

chrisbonifacio commented 4 months ago

Hi @ChristopherGabba can you try awaiting the receiver in your code? You may have to lazy load the relationship data.

Like so:

const firstFriendshipReceiver = await response.data[0].receiver();

Here are our docs on lazy loading hasMany relationships:

https://docs.amplify.aws/react/build-a-backend/data/data-modeling/relationships/#lazy-load-a-has-many-relationship

ChristopherGabba commented 4 months ago

@chrisbonifacio Interesting. This worked, but it still creates an issue for me. I basically fetch the friendship (with underlying user (receiver/sender) nested data) and I apply it straight into my local state.

  1. const results = await fetchFriendshipsAndUsers()
  2. setState(results)

Now I will have to:

  1. const response = fetchFriendships()
  2. For each friendship // fetchEachUserData() by lazy loading
  3. setFriendships(response)
  4. setUsers(users)

It would be painful and inefficient to have to create a whole other query when the user data should already be available in the same friendship api call. I noticed in the same link you provided there is an eagerly load, which is what I'm looking for I think. How can I define a selectionSet: { selectionSet: ["id", "receiver.", "sender."] }, so that the deeper nested data just comes through initially in the first api call?

I had this working fine in a Gen 1 without having to lazy load by modifying the query in one of the amplify files locally that was created with the amplify codegen command: aws/graphql/queries.ts

export const friendshipsByReceiverId =
  /* GraphQL */ `query FriendshipsByReceiverId(
  $receiverId: ID!
  $sortDirection: ModelSortDirection
  $filter: ModelFriendshipFilterInput
  $limit: Int
  $nextToken: String
) {
  friendshipsByReceiverId(
    receiverId: $receiverId
    sortDirection: $sortDirection
    filter: $filter
    limit: $limit
    nextToken: $nextToken
  ) {
    items {
      id
      senderId
      sender { // defined deeper nested return data without lazy loading
        id
        profileImage
        profileImageBlurhash
        firstName
        lastName
        username
        birthdate
        phoneNumber
        searchTerm
        pushToken
        createdAt
        updatedAt
        __typename
      }
      receiverId
      receiver { // defined deeper nested return data without lazy loading
        id
        profileImage
        profileImageBlurhash
        firstName
        lastName
        username
        birthdate
        phoneNumber
        searchTerm
        pushToken
        createdAt
        updatedAt
        __typename
      }
      status
      createdAt
      updatedAt
      __typename
    }
    nextToken
    __typename
  }
}
` as GeneratedQuery<
    APITypes.FriendshipsByReceiverIdQueryVariables,
    APITypes.FriendshipsByReceiverIdQuery
  >;

Now this file is not exposed for editting in Gen 2.

I think this is super valuable, and I doubt I am the only user who will need this. Perhaps this can be a secondaryIndex input when defining a query?

chrisbonifacio commented 4 months ago

Hey @ChristopherGabba you can do "receiver.*" in the selection to select and eagerly load all fields on a nested/related record.

ChristopherGabba commented 4 months ago

Thanks @chrisbonifacio thats exactly what I was looking for! I couldn't get the selectionSet field to show up but it's actually a second object parameter input into the list query.

Is there a way to define this globally instead of in every query?

Example:

receiver: a.belongsTo("User", "receiverId").includeInSelectionSet()

The problem with it having to be defined in every query is that now if I try to reference a friendship type:

const friendship: Schema["Friendship"]["type"] = ...

const firstName = friendship.reciever.firstName // throws typescript error on firstname because the main schema won't allow me to reference the nested object data even when I know it is there.

Defining the selection sets seems to override the tyepscript for the results of the query but I want that to be global so throughout my app, I can reference things deeper without typescript errors.

chrisbonifacio commented 4 months ago

@ChristopherGabba the solution here might be to define the selection set separately and combine its typing with Schema using our SelectionSet helper type:

Here's an example:


import type { SelectionSet } from 'aws-amplify/data';
import type { Schema } from '../amplify/data/resource';

const selectionSet = ['content', 'blog.author.*', 'comments.*'] as const;
type PostWithComments = SelectionSet<Schema['Post']['type'], typeof selectionSet>;

// ...
const [posts, setPosts] = useState<PostWithComments[]>([]);

const fetchPosts = async () => {
  const { data: postsWithComments } = await client.models.Post.list({
    selectionSet,
  });
  setPosts(postsWithComments);
}

https://docs.amplify.aws/react/build-a-backend/data/query-data/#typescript-type-helpers-for-amplify-data

ChristopherGabba commented 4 months ago

@chrisbonifacio Awesome! This worked. Closing this issue as complete, thanks again for all the help, I know that Gen 2 is fairly new.

I will propose a feature request in a separate thread like so:

receiver: a.belongsTo("User", "receiverId").includeInSelectionSet()

So that custom types won't have to be defined, and you won't have to set selectionSets separately on each query, should the user not want it.