I'm trying to get to grips with AWS AppSync. I'm quite new to GraphQL. I've got the following GraphQL:
type Mutation {
deleteParcel(geoHash: String!, type_id: String!): Parcel
addParcel(input: ParcelInput!): Parcel
batchAddParcels(parcels: [ParcelInput]): [Parcel]
}
type Parcel {
geoHash: String!
type_id: String!
}
type ParcelConnection {
items: [Parcel]
}
input ParcelInput {
geoHash: String!
type_id: String!
}
input ParcelsInput {
parcels: [ParcelInput]
}
type Query {
getNearbyParcels(geoHash: String!): ParcelConnection
}
type Subscription {
onAddParcel(geoHash: String, type_id: String): Parcel
#aws_subscribe(mutations: ["addParcel"])
onBatchAddParcels(geoHash: String): Parcel
#aws_subscribe(mutations: ["batchAddParcels"])
onDeleteParcel(geoHash: String, type_id: String): Parcel
#aws_subscribe(mutations: ["deleteParcel"])
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
All seems to be setup fine on AWS console. I get the schema.json and then run command:
aws-appsync-codegen generate AWSGraphQL.graphql --schema schema.json --output AppsyncAPI.swift
and get the response:
../SnatchHQ/snatch_appsync/AppSync/AWSGraphQL.graphql: Directive "aws_subscribe" may not be used on FIELD_DEFINITION.
.../SnatchHQ/snatch_appsync/AppSync/AWSGraphQL.graphql: Directive "aws_subscribe" may not be used on FIELD_DEFINITION.
.../SnatchHQ/snatch_appsync/AppSync/AWSGraphQL.graphql: Directive "aws_subscribe" may not be used on FIELD_DEFINITION.
error: Validation of GraphQL query document failed
Can anyone help?
If the file AWSGraphQL.graphql is your API GraphQL schema, then that explains the problem. What you need to do is define a *.graphql file that defines your query, mutation, and subscription operations based on your GraphQL API. For example, the following query definitions would match your schema
mutation AddParcel($geoHash: String!, $type_id: String!) {
addParcel(input: {
geoHash: $geoHash
type_id: $typeId
}) {
...Parcel
}
}
query GetNearbyParcels($geoHash: String!) {
getNearbyParcels(
geoHash: $geoHash
) {
...ParcelConnection
}
}
subscription OnAddParcel {
onAddParcel {
...Parcel
}
}
fragment Parcel on Parcel {
geoHash
type_id
}
fragment ParcelConnection on Parcel Connection {
items {
...Parcel
}
}
Assuming you named it something like parcels.graphql, you can then call the following to generate a Swift implementation of the AddParcel mutation, the GetNearbyParcels query, and the OnAddParcel subscription
aws-appsync-codegen generate parcels.graphql \
--schema schema.json \
--output AppSyncParcelsAPI.swift
Related
Please note: although this question mentions AWS SAM, it is 100% a DynamoDB JavaScript SDK question at heart and can be answered by anyone with experience writing JavaScript Lambdas (or any client-side apps) against DynamoDB using the AWS DynamoDB client/SDK.
So I used AWS SAM to provision a new DynamoDB table with the following attributes:
FeedbackDynamoDB:
Type: AWS::DynamoDB::Table
Properties:
TableName: commentary
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
StreamSpecification:
StreamViewType: NEW_IMAGE
This configuration successfully creates a DynamoDB table called commentary. However, when I view this table in the DynamoDB web console, I noticed a few things:
it has a partition key of id (type S)
it has no sort key
it has no (0) indexes
it has a read/write capacity mode of "5"
I'm not sure if this raises any red flags with anyone but I figured I would include those details, in case I've configured anything incorrectly.
Now then, I have a JavaScript (TypeScript) Lambda that instantiates a DynamoDB client (using the JavaScript SDK) and attempts to add a record/item to this table:
// this code is in a file named app.ts:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { User, allUsers } from './users';
import { Commentary } from './commentary';
import { PutItemCommand } from "#aws-sdk/client-dynamodb";
import { DynamoDBClient } from "#aws-sdk/client-dynamodb";
export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const ddbClient = new DynamoDBClient({ region: "us-east-1" });
let status: number = 200;
let responseBody: string = "\"message\": \"hello world\"";
const { id, content, createdAt, providerId, receiverId } = JSON.parse(event.body);
const commentary = new Commentary(id, content, createdAt, providerId, receiverId);
console.log("deserialized this into commentary");
console.log("and the deserialized commentary has content of: " + commentary.getContent());
await provideCommentary(ddbClient, commentary);
responseBody = "\"message\": \"received commentary -- check dynamoDb!\"";
return {
statusCode: status,
body: responseBody
};
} catch (err) {
console.log(err);
return {
statusCode: 500,
body: JSON.stringify({
message: err.stack,
}),
};
}
};
const provideCommentary = async (ddbClient: DynamoDBClient, commentary: Commentary) => {
const params = {
TableName: "commentary",
Item: {
id: {
S: commentary.getId()
},
content: {
S: commentary.getContent()
},
createdAt: {
S: commentary.getCreatedAt()
},
providerId: {
N: commentary.getProviderId()
},
receiverId: {
N: commentary.getReceiverId()
}
}
};
console.log("about to try to insert commentary into dynamo...");
try {
console.log("wait for it...")
const rc = await ddbClient.send(new PutItemCommand(params));
console.log("DDB response:", rc);
} catch (err) {
console.log("hmmm something awry. something....in the mist");
console.log("Error", err.stack);
throw err;
}
};
Where commentary.ts is:
class Commentary {
private id: string;
private content: string;
private createdAt: Date;
private providerId: number;
private receiverId: number;
constructor(id: string, content: string, createdAt: Date, providerId: number, receiverId: number) {
this.id = id;
this.content = content;
this.createdAt = createdAt;
this.providerId = providerId;
this.receiverId = receiverId;
}
public getId(): string {
return this.id;
}
public getContent(): string {
return this.content;
}
public getCreatedAt(): Date {
return this.createdAt;
}
public getProviderId(): number {
return this.providerId;
}
public getReceiverId(): number {
return this.receiverId;
}
}
export { Commentary };
When I update the Lambda with this handler code, and hit the Lambda with the following curl (the Lambda is invoked by an API Gateway URL that I can hit via curl/http):
curl -i --request POST 'https://<my-api-gateway>.execute-api.us-east-1.amazonaws.com/Stage/feedback' \
--header 'Content-Type: application/json' -d '{"id":"123","content":"test feedback","createdAt":"2022-12-02T08:45:26.261-05:00","providerId":457,"receiverId":789}'
I get the following HTTP 500 response:
{"message":"SerializationException: NUMBER_VALUE cannot be converted to String\n
Am I passing it a bad request body (in the curl) or do I need to tweak something in app.ts and/or commentary.ts?
Interestingly the DynamoDB API expects numerical fields of items as strings. For example:
"N": "123.45"
The doc says;
Numbers are sent across the network to DynamoDB as strings, to maximize compatibility across languages and libraries. However, DynamoDB treats them as number type attributes for mathematical operations.
Have you tried sending your input with the numerical parameters as strings as shown below? (See providerId and receiverId)
{
"id":"123",
"content":"test feedback",
"createdAt":"2022-12-02T08:45:26.261-05:00",
"providerId":"457",
"receiverId":"789"
}
You can convert these IDs into string when you're populating your input Item:
providerId: {
N: String(commentary.getProviderId())
},
receiverId: {
N: String(commentary.getReceiverId())
}
You could also use .toString() but then you'd get errors if the field is not set (null or undefined).
Try using a promise to see the outcome:
client.send(command).then(
(data) => {
// process data.
},
(error) => {
// error handling.
}
);
Everything seems alright with your table setup, I believe it's Lambda async issue with the JS sdk. I'm guessing Lambda is not waiting on your code and exiting early. Can you include your full lambda code.
I am trying to update the value of a table using the AWS-app sync graphql API,
I am able to create data and add it in a table using graphql mutation in lambda
but when I am trying to update the data its not working.
I am calling this lambda service from an API Gateway.
I am referring this article to code
https://cloudonaut.io/calling-appsync-graphql-from-lambda/
I would like to mentioned git no error in cloud watch log
Here is the schema for my graphql
type Mutation {
createLib_content(input: CreateLib_contentInput!): lib_content
#aws_iam
updateLib_content(input: UpdateLib_contentInput!): lib_content
#aws_iam
deleteLib_content(input: DeleteLib_contentInput!): lib_content
}
input CreateLib_contentInput {
content: String
userId: String
}
input UpdateLib_contentInput {
content: String
id: ID!
}
Create Mutation
graphqlData = await clientDetails.mutate({
mutation: gql(`
mutation CreateLibContent($input: CreateLib_contentInput!) {
createLib_content(input: $input) {
id
content
}
}`),
variables: {
input: {
content : {},
userId : identitiesDetails.userId
}
},
});
Update Mutation
const mutation = gql(`
mutation UpdateLibContent($input: UpdateLib_contentInput!) {
updateLib_content(input: $input) {
userId
content
}
}`);
await clientDetails.mutate({
mutation,
variables: {
input: {
id : "2947c37e-6f76-40d8-8c10-4cd6190d3597",
content : JSON.stringify(event)
}
}
}).promise;
Thanks to #cppgnlearner your guess were right.
I just removed the .promise from my update code
And it started working.
can't believe such a small thing took my whole day.
Currently I have my resolver as a lambda function :
import boto3
from boto3.dynamodb.conditions import Key
def lambda_handler(event, context):
list = []
for device in event['source']['devices'] :
dynamodb = boto3.resource('dynamodb')
readings = dynamodb.Table('readings')
response = readings.query(
KeyConditionExpression=Key('device').eq(device['device'])
)
items = response['Items']
list.extend(items)
return list
I would like to be able to have this as a VTL resolver on the dynamodb. My problem is that my table has a sort key
This means I can't use a batch resolver to query on a bunch of id's because I would also need to provide the sort key, and I just want all the results by primary partition key.
How do you query with a bunch of ids using VTL, basically replicating my lambda function in VTL. Is this even possible ?
Schema added, please excuse the mess it is a work in progress and am attempting many things. Still very new to graphQL
type Device {
id: String
device: String!
}
input DeviceInput {
id: String
device: String!
}
type DeviceReadings {
devices: [Device]
}
type Mutation {
createDevice(input: DeviceInput): Device
}
type PaginatedDevices {
devices: [Device]
readings: [Reading]
cows: [cow]
nextToken: String
}
type Query {
getAllUserDevices(nextToken: String, count: Int): PaginatedDevices
getAllDeviceReadings: DeviceReadings
getAllUserReadings: DeviceReadings
getAllReadings(deviceId: String!): Readings
getCowReadings(cowId: String!): UserCowReadings
}
type Reading {
device: String
time: Int
cow: Int
battery: String
}
type Readings {
items: [Reading]
}
type UserCowReadings {
devices: [Device]
readings: [Reading]
}
type cow {
id: Int
device: String
nait: String
}
schema {
query: Query
mutation: Mutation
}
Yes you can do this but you will need to tweak your schema a bit. In that lambda you are essentially saying "for each device do a DynamoDB query to get the most recent readings for that device". Conceptually I would say that devices have many readings. With this in mind, lets make a schema:
type Device {
id: ID!
name: String
# Get the most recent readings for this device.
# Do a Query where "device = $ctx.source.id"
readings(limit: Int, nextToken: String): ReadingConnection
}
type Reading {
# Use the source's device id in the table to fetch the real device
# GetItem where device = $ctx.source.device (and any sort key condition)
device: Device
time: Int
cow: Int
battery: String
}
type ReadingConnection {
items: [Reading]
nextToken: String
}
type DeviceConnection {
items: [Device]
nextToken: String
}
type Query {
getAllDevices(limit: Int, nextToken: String): DeviceConnection
}
You may then paginate through your devices and paginate through each devices readings separately:
query GetAllDevicesAndReadings {
getAllDevices(first: 10) {
items {
id
name
readings(limit: 10) {
time
cow
battery
}
}
}
}
I recommend using the drop down in the AppSync console's resolver page to get more ideas for what you can do with the resolver VTL to implement these GetItems and Queries. This is a good starting point. Let me know if you have trouble implementing the VTL.
We are having huge troubles with subscriptions with arguments
to simplify the problem Here are the steps to reproduce
create a simpleSchema
type Mutation {
testSubMutation(param: String!): String
}
type Query {
testQuery: String
}
type Subscription {
testSubs(param: String): String
#aws_subscribe(mutations: ["testSubMutation"])
}
I attached a local resolver to the mutation which returns the timestamp.
in one window open the app sync query tab and make the subscription
subscription sub{
testSubs
}
in the other window make a mutation
mutation mut{
testSubMutation(param:"123")
}
works like a charm
now change the subscription to listen to a parameter
subscription sub{
testSubs(param:"123")
}
Does not work any more. :(
Any help is appreciated.
Subscriptions require the parameter you're filtering on to be in the response of the mutation. Could you try updating your mutation to this?
mutation mut{
testSubMutation(param:"123") {
param
}
}
I'm doing same as above for subscription but not getting response, It's only working with one argument room
mutation addMessage {
addMessage(input: {
room: "45a87f5b-ef9e-41cd-9cd7-f3e2f4946d31",
receiver: "3cea9c02-1cf5-4248-8ebe-3580a7a47b8b" }) {
id
room
receiver {
id
userName
}
}
}
subscription roomMessage {
roomMessage(room: "45a87f5b-ef9e-41cd-9cd7-f3e2f4946d31",
receiver: "3cea9c02-1cf5-4248-8ebe-3580a7a47b8b") {
id
room
receiver {
id
userName
}
}
}
I created this question in case anyone was curious on how to add union / Polymorphic types in Apollo. Hopefully this will make it easier for them.
In this example I wanted the response to either be a Worksheet or ApiError
// typedefs.js
export default [`
schema {
query: Query
}
type Query {
worksheet(id: String!): Worksheet | Error
}
type Worksheet {
id: String!
name String
}
type ApiError {
code: String!
message: String!
}
`];
// resolvers.js
export default {
Query: {
worksheet(_, args, { loaders }) {
return loaders.worksheet.get(args.id).catch(() => {
// ApiError
return {
code: '1',
message: 'test'
}
});
}
}
};
// Express Server
import { graphqlExpress } from 'apollo-server-express';
import { makeExecutableSchema } from 'graphql-tools';
import typeDefs from './typedefs';
import resolvers from './resolvers';
...
app.post(
'/graphql',
graphqlExpress(req => ({
makeExecutableSchema({ typeDefs, resolvers }),
context: mkRequestContext(req.ctx, req.log),
formatError: formatGraphQLError(req.ctx, req.log)
}))
);
In GraphQL to add a union type in the typedefs you have to define the union
i.e union WorksheetOrError = Worksheet | ApiError
// typedefs.js
export default [
`
schema {
query: Query
}
type Query {
worksheet(id: String!): WorksheetOrError
}
union WorksheetOrError = Worksheet | ApiError
type Worksheet {
id: String!
name String
}
type ApiError {
code: String!
message: String!
}
`];
In the resolvers you have to define a resolver for the union type that has the property __resolveType. This will help tell the GraphQL executor which type the result is.
// resolvers.js
export default {
Query: {
worksheet() {
...
}
},
WorksheetOrError: {
__resolveType(obj) {
if (obj.id) {
return 'Worksheet';
}
if (obj.code) {
return 'ApiError';
}
return null;
}
},
};
To create a GraphQL Query in Apollo Client
// Your application code.
// This is my Worksheet Query in my React code.
const WorksheetQuery = gql`
query GetWorksheet($worksheetId: String!) {
worksheet(id: $worksheetId) {
... on Worksheet {
id
name
}
... on ApiError {
code
message
}
}
}
Now you can check the __typename to check what type is in the response.
Note: For those who are wondering why I'm not using GraphQL errors. It's because Apollo doesn't seem to handle errors well when it encounters a graphQL error. So for a work around I'm trying to return a custom ApiError in my response.
There a few reasons why using a union with an error type is nice.
Currently if you wanted a partial response with GraphQLError. Apollo does not cache the errors so if you wanted to re-use the cached response later you wouldn't have the complete response since the errors are removed. (Now you can't display the proper UI with errors)
Getting GraphQLError back in Apollo would return a flat list of errors with the path to where the error is in the data. So you would need to verify that which part of your schema did the error occur in. However if you follow the instructions above you would have the error within the schema already. That way you already know which part of the schema the error happened.