AWS appsync query resolver - amazon-web-services

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.

Related

DynamoDB JavaScript PutItemCommand is neither failing nor working

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.

AWS Amplify searchable field by geo distance (location)

I'm developing an application that will allow users to search for other users by ordering them by geo location.
According to the information found on the internet, to do this on amplify I have to perform the following steps:
create an elastic search mapping, indicating the type 'geo_point' on the lastPosition attribute (I would like to define it once in the project file and not at each build in the elastic search console)
create a custom query and a custom vtl resolver and set the sort by lastPosition (of type '_geo_distance') in it.
But I don't understand how to do these 2 steps, so I need some suggestion :(.
// my schema.graphql
type User
#model
#auth(rules: [{allow: owner}, {allow: private, operations: [read]}])
#searchable {
// ...other fields
id: ID!
lastPosition: Position
positionUpdatedAt: AWSDateTime
}
type Position {
lat: Float!
lon: Float!
}
// my custom searchUsers query in graphql/custom_queries.ts
export const searchUsersNearby = /* GraphQL */ `
query SearchUsersNearby(
$filter: SearchableUserFilterInput
$sort: [SearchableUserSortInput]
$location: PositionInput // current user position that i need to use in vtl resolver to sort users by distance,
$limit: Int
$nextToken: String
$from: Int
$aggregates: [SearchableUserAggregationInput]
) {
searchUsersNearby(
filter: $filter
sort: $sort
limit: $limit
nextToken: $nextToken
from: $from
aggregates: $aggregates
) {
items {
id
// ...other fields
lastPosition
positionUpdatedAt
createdAt
updatedAt
owner
}
nextToken
total
aggregateItems {
name
result {
... on SearchableAggregateScalarResult {
value
}
... on SearchableAggregateBucketResult {
buckets {
key
doc_count
}
}
}
}
}
}
`;
I use amplify v8.1.0 with transformer v2.
Thank you guys

Calling AWS AppSync graphql API from Lambda

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.

Passing variables to an external resolve reference

I'm using Apollo Federation for 2 months but I'm actually stuck. I've no idea how to pass a variable between my two graphql services.
I've got a website (website graphql service) which have orders (orders graphql service).
I have a query to find websites and for these websites I want some stats of orders for a date range. Here the typedef (website) :
type Query {
websites(orderFilter: OrderFilterInput): [Website!]
}
type Website #key(fields: "id") {
id: ID!
name: String!
url: String!
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary
}
input OrderSummaryFilterInput {
beginDate: Date
endDate: Date
}
extend type OrderSummary #key(fields: "websiteId") {
websiteId: String! #external
}
The resolver :
orderSummary: (website, { orderSummaryFilter }) => {
console.log("orderSummaryFilter", orderSummaryFilter); // filters are OK
// HOW CAN I PASS orderFilterSummary to my order graphql service here ????
return { __typename: "OrderSummary", websiteId: website.id };
}
And Order graphql service
Typedef part :
type OrderSummary #key(fields: "websiteId") {
websiteId: String!
count: Int
amount: Int
}
Resolver part :
// order gql service
OrderSummary: {
__resolveReference(website, args, info) {
console.log("website id :", website.id); // I ve got my website ID
// HOW TO GET OrderSummaryFilter here ????
},
},
How can I access to order summary filter variable in order graphql resolver ? Thank you.
From what I am aware of, it is not possible to send variables from one service to another other then the ID. But there is a solution to this.
If you want to pass in variables, extend your Website type in your order service instead of extending order type in website service.
Order typedef:
extend type Website #key(fields: "id") {
id: ID! #external
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary #requires(fields:"id")
}
Order resolver:
Website: {
orderSummary: async (website, { orderFilter }) => getOrderSummary(orderFilter) //get orderSummary with orderFilter
},
So I want to expound on the previous (and I believe correct) answer:
In Federation, you almost never should have to expose a field called somethingId (userId, websiteId, etc). That is often either a left-over from Schema Stitching, or you simply got your type origins backward. Instead of using somethingId, you should be able to just use the object. Often, moving the #extend to the other service will get rid of the somethingId field, and get rid of the type of problem you're currently facing:
Website Service:
type Query {
websites(orderFilter: OrderFilterInput): [Website!]
}
type Website #key(fields: "id") {
id: ID!
name: String!
url: String!
}
Order Service:
extend type Website #key(fields: "id") {
id: ID! #external
orderSummary(orderFilter: OrderSummaryFilterInput): OrderSummary
}
input OrderSummaryFilterInput {
beginDate: Date
endDate: Date
}
type OrderSummary {
website: Website!
count: Int
amount: Int
}
Resolvers:
const resolvers = {
Website: {
orderSummary(parent, args, context) {
const websiteId = parent.id;
// args is the data you wanted
}
},
};

AWS AppSync - Directive "aws_subscribe" may not be used on FIELD_DEFINITION

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