Reputation: 480
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!]
}
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]
}
]
Upvotes: 0
Views: 397
Reputation: 25669
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.
Upvotes: 1