Circular refrence AWS appsync
Asked Answered
U

1

0

I want to know if having a circular reference in AWS Appsync possible or not? I have searched a lot but couldn't find it. Something like this:

type Post {
    title: String!
    content: String!
    user: User!
}

type Query {
    allPosts: [Post!]
    singlePost(id: String!): Post!
}

type User {
    name: String!
    posts: [Post!]
}

Edit (Tried lambda with the logic described here https://youtu.be/bgq7FRSPDpI?list=PL55RiY5tL51rG1x02Yyj93iypUuHYXcB_&t=526 )

Here's the lambda resolver for allPosts (handler function will be called):

import * as sdk from "aws-sdk";

declare var process: {
  env: {
    TABLE_NAME: string;
  };
};

interface Event {
  info: {
    fieldName: string;
    parentTypeName: string;
    variables: Record<string, any>;
  };
}

const client = new sdk.DynamoDB.DocumentClient();

const getUser = (user_id: string): Record<string, any> | null => {
  return client
    .query({
      TableName: process.env.TABLE_NAME,
      KeyConditionExpression: "PK = :pk AND SK = :sk",
      ExpressionAttributeValues: {
        ":pk": user_id,
        ":sk": "profile",
      },
    })
    .promise()
    .then(
      (data) =>
        data.Items?.map((item) => ({
          ...item.data,
          posts: getPost.bind(null, item.PK),
        }))[0]
    )
    .catch((err) => {
      console.log(err);
      return null;
    });
};

const getPost = (user_id: string): Record<string, any> | null => {
  return client
    .query({
      TableName: process.env.TABLE_NAME,
      KeyConditionExpression: "SK = :sk AND pk = :pk",
      ExpressionAttributeValues: {
        ":pk": user_id,
        ":sk": "profile",
      },
    })
    .promise()
    .then((data) =>
      data.Items?.map((item) => ({
        ...item.data,
        user: getUser.bind(null, item.PK),
      }))
    )
    .catch((err) => {
      console.log(err);
      return null;
    });
};

export const handler = async (event: Event) => {
  if (event.info.fieldName === "allPosts") {
    const data = await client
      .query({
        TableName: process.env.TABLE_NAME,
        KeyConditionExpression: "#t = :sk",
        IndexName: "GSI",
        ProjectionExpression: "#d, PK",
        ExpressionAttributeNames: {
          "#t": "type",
          "#d": "data",
        },
        ExpressionAttributeValues: {
          ":sk": "post",
        },
      })
      .promise();
    const result = data.Items?.map((item) => ({
      ...item.data,
      user: getUser.bind(null, item.PK),
    }));
    console.log(data, result);
    return result;
  }
  return;
  //   else if (event.fieldName === "singlePost") {

  //   }
};

The user field has a function bounded as in this video: https://youtu.be/bgq7FRSPDpI?list=PL55RiY5tL51rG1x02Yyj93iypUuHYXcB_&t=526

But lambda response is not returning the bounded function.

[
  {
    "title": "post by user_123",
    "content": "\n\nNew to this community. I need some help in designing the Amazon Dynamo DB table for my personal projects.\n\nOverview, this is a simple photo gallery application with following attributes.\n\nUserID\nPostID\nList item\nS3URL\nCaption\nLikes\nReports\nUploadTime\nI wish to perform the following queries:\n\nFor a given user, fetch 'N' most recent posts\nFor a given user, fetch 'N' most liked posts\nGive 'N' most recent posts (Newsfeed)\nGive 'N' most liked posts (Newsfeed)\nMy solution:"
  },
  {
    "title": "another post by user_123",
    "content": "\n\nNew to this community. I need some help in designing the Amazon Dynamo DB table for my personal projects.\n\nOverview, this is a simple photo gallery application with following attributes.\n\nUserID\nPostID\nList item\nS3URL\nCaption\nLikes\nReports\nUploadTime\nI wish to perform the following queries:\n\nFor a given user, fetch 'N' most recent posts\nFor a given user, fetch 'N' most liked posts\nGive 'N' most recent posts (Newsfeed)\nGive 'N' most liked posts (Newsfeed)\nMy solution:"
  }
]

But I can see the bounded function in the logs:

[
  {
    title: 'post by user_123',
    content: '\n' +
      '\n' +
      'New to this community. I need some help in designing the Amazon Dynamo DB table for my personal projects.\n' +
      '\n' +
      'Overview, this is a simple photo gallery application with following attributes.\n' +
      '\n' +
      'UserID\n' +
      'PostID\n' +
      'List item\n' +
      'S3URL\n' +
      'Caption\n' +
      'Likes\n' +
      'Reports\n' +
      'UploadTime\n' +
      'I wish to perform the following queries:\n' +
      '\n' +
      "For a given user, fetch 'N' most recent posts\n" +
      "For a given user, fetch 'N' most liked posts\n" +
      "Give 'N' most recent posts (Newsfeed)\n" +
      "Give 'N' most liked posts (Newsfeed)\n" +
      'My solution:',
    user: [Function: bound getUser]
  },
  {
    title: 'another post by user_123',
    content: '\n' +
      '\n' +
      'New to this community. I need some help in designing the Amazon Dynamo DB table for my personal projects.\n' +
      '\n' +
      'Overview, this is a simple photo gallery application with following attributes.\n' +
      '\n' +
      'UserID\n' +
      'PostID\n' +
      'List item\n' +
      'S3URL\n' +
      'Caption\n' +
      'Likes\n' +
      'Reports\n' +
      'UploadTime\n' +
      'I wish to perform the following queries:\n' +
      '\n' +
      "For a given user, fetch 'N' most recent posts\n" +
      "For a given user, fetch 'N' most liked posts\n" +
      "Give 'N' most recent posts (Newsfeed)\n" +
      "Give 'N' most liked posts (Newsfeed)\n" +
      'My solution:',
    user: [Function: bound getUser]
  }
]
Unstressed answered 14/10, 2021 at 8:33 Comment(6)
Have you tried anything? Try doing it and see what happensSuperfluid
I tried binding functions for user in Post type in lambda (JavaScript). Seems not to be working. Let me add the code hereUnstressed
Take 5 minutes to please take the tour, read How do I ask a good question? as well as How to create a Minimal, Reproducible Example and then update your question to help us help you :)Superfluid
I have added details.Unstressed
Much better, please also read meta.https://mcmap.net/q/12237/-windows-cmd-pipe-not-unicode-even-with-u-switch!Superfluid
I have added text logs in place of images, but don't know why syntax is not highlightedUnstressed
E
1

TL;DR Yes, appsync can easily handle nested or "circular" queries. The key insight is that it's not the allPosts handler's job to resolve the User type behind the user field. Instead, appsync will invoke the lambda resolver a second time to get the user field's User. We need to add branching logic in our lambda to handle the second invocation, where event.info.fieldName === "user".

// a switch statement inside your lambda function handler "routes" the request

switch (event.parentTypeName) {
  case "Query":
    switch (event.fieldName) {
      case "allPosts":
        // depends on your schema
        const userID = event.arguments?.["id"]
        // handle query, return [Post!], as per your schema
      case "singlePost"
        const postID = event.arguments?["id"]
        // ditto, return Post!, as per your schema
    }
  case "Post":
    switch (event.fieldName) {
      case "user":
       // event.source is a Post, details depend on your actual schema
       const userID = event.source?.["userID"]
       // make dynamo call to get the User and return a User, type that your graphql schema is expecting
    }
  case "User":
    switch (event.fieldName) {
      case "posts":
         // event.source is a User, details depend on your actual schema
        const userID = event.source?.["id"]
        // fetch posts from dynamo, return [Post!], the type your graphql schema is expecting
    }
  default:
    throw new Error("unhandled parent type")
}

Context: this answer, like the question, assumes our datasource is a direct lambda resolver, meaning our function receives as an arg the full context object and that we don't use VTL templates. It also assumes the general default choice of having a single lambda resolver with a giant switch statement to handle the various incoming requests.

Resolvable Fields are resolved separately by appsync. A resolvable field has a resolver datasource. In our case, it is a lambda function. For each resolvable field appsync encounters, appsync will make a separate call to the resolver. (BTW, my guess is that you have already configured User with a datasource. That would solve the mystery of why allPosts is not returning user results. Resolving users is not its job, and your lambda is not currently handling a event.info.fieldName of user).

Setting Resolvers aws gives us several ways to assign a resolver to a field, including in the console (schema tab, attach button) and the cdk (add a LambdaDataSource to a ObjectType).

How do we get the user id? Appsync gives us the event arg for user PK/SK info for our dynamodb call. The event gives us lots of info, including fieldName, parentTypeName, and crucially, a source key with the parent field's values. console.log the event to discover what you've got to work with.

Extra Credit

Q: How many times will appsync invoke our lambda for the following query?

query allPosts(limit: 10) {
    title
    content
    user {
        name
    }
}

A: 11 times. 1x for allPosts, 10x for user.

Prior Art See this and this SO post for similar answers.

Don't try this at home It is theoretically possible, if quite silly, to remove the User type resolver, thereby handing to allPosts (and other queries) the responsibility to return users. Due to the potentially deep nesting of query return values, rather complicated for zero gain.

Esoterica answered 27/10, 2021 at 22:17 Comment(1)
Thank you so much! Your answer is clearly explained and very detailed. I didn't know the concept of a field resolver and this answer made this concept a piece of cake!Unstressed

© 2022 - 2024 — McMap. All rights reserved.