I get the exception ValidationException: One of the required keys was not given a value
when I click on the Play button as instructed in an AWS workshop page. Any advice would be really helpful!
AppSync Mock endpoint is running at http://192.168.20.3:20002
Error while executing Local DynamoDB
{
"version": "2018-05-29",
"operation": "PutItem",
"attributeValues": {
"type": {
"S": "post"
},
"content": {
"S": "test"
},
"timestamp": {
"N": "1668425585"
},
"owner": {
"S": "7d8ca528-4931-4254-9273-ea5ee853f271::user1"
},
"__typename": {
"S": "Post"
}
},
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id"
}
},
"key": {}
}
ValidationException: One of the required keys was not given a value
Schema file
input AMPLIFY {
globalAuthRule: AuthRule = { allow: public }
} # FOR TESTING ONLY!
type Post
#model(
mutations: { create: "createPost", delete: "deletePost", update: null }
timestamps: null
subscriptions: { level: public }
)
#auth(
rules: [
{
allow: owner
ownerField: "owner"
provider: userPools
operations: [read, create, delete]
}
{ allow: private, provider: userPools, operations: [read] }
]
) {
type: String! # always set to 'post'. used in the SortByTimestamp GSI
id: ID!
content: String!
owner: String
timestamp: Int!
}
(I tried adding and removing the exclamation to the ID here)
Screenshot
Related
I'm building a geospatial search on properties using AWS Amplify and ElasticSearch.
I'm currently following this guide: https://gerard-sans.medium.com/finding-the-nearest-locations-around-you-using-aws-amplify-part-2-ce4603605be6
I set up my model as follows
type Property #model #searchable #auth(rules: [{allow: public}]) {
id: ID!
...
Loc: Coord!
}
type Coord {
lon: Float!
lat: Float!
}
I also added a custom Query:
type Query {
nearbyProperties(
location: LocationInput!,
m: Int,
limit: Int,
nextToken: String
): ModelPropertyConnection
}
input LocationInput {
lat: Float!
lon: Float!
}
type ModelPropertyConnection {
items: [Property]
total: Int
nextToken: String
}
I added resolvers for request and response:
## Query.nearbyProperties.req.vtl
## Objects of type Property will be stored in the /property index
#set( $indexPath = "/property/doc/_search" )
#set( $distance = $util.defaultIfNull($ctx.args.m, 500) )
#set( $limit = $util.defaultIfNull($ctx.args.limit, 10) )
{
"version": "2017-02-28",
"operation": "GET",
"path": "$indexPath.toLowerCase()",
"params": {
"body": {
"from" : 0,
"size" : ${limit},
"query": {
"bool" : {
"must" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "${distance}m",
"Loc" : $util.toJson($ctx.args.location)
}
}
}
},
"sort": [{
"_geo_distance": {
"Loc": $util.toJson($ctx.args.location),
"order": "asc",
"unit": "m",
"distance_type": "arc"
}
}]
}
}
}
and response:
## Query.nearbyProperties.res.vtl
#set( $items = [] )
#foreach( $entry in $context.result.hits.hits )
#if( !$foreach.hasNext )
#set( $nextToken = "$entry.sort.get(0)" )
#end
$util.qr($items.add($entry.get("_source")))
#end
$util.toJson({
"items": $items,
"total": $ctx.result.hits.total,
"nextToken": $nextToken
})
And now the CustomStacks.json:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "An auto-generated nested stack.",
"Metadata": {},
"Parameters": {
"AppSyncApiId": {
"Type": "String",
"Description": "The id of the AppSync API associated with this project."
},
"AppSyncApiName": {
"Type": "String",
"Description": "The name of the AppSync API",
"Default": "AppSyncSimpleTransform"
},
"env": {
"Type": "String",
"Description": "The environment name. e.g. Dev, Test, or Production",
"Default": "NONE"
},
"S3DeploymentBucket": {
"Type": "String",
"Description": "The S3 bucket containing all deployment assets for the project."
},
"S3DeploymentRootKey": {
"Type": "String",
"Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory."
}
},
"Resources": {
"QueryNearbyProperties": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": { "Ref": "AppSyncApiId" },
"DataSourceName": "ElasticSearchDomain",
"TypeName": "Query",
"FieldName": "nearbyProperties",
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyProperties.req.vtl", {
"S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
"S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
}]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [ "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyProperties.res.vtl", {
"S3DeploymentBucket": { "Ref": "S3DeploymentBucket" },
"S3DeploymentRootKey": { "Ref": "S3DeploymentRootKey" }
}]
}
}
}
},
"Conditions": {
"HasEnvironmentParameter": {
"Fn::Not": [
{
"Fn::Equals": [
{
"Ref": "env"
},
"NONE"
]
}
]
},
"AlwaysFalse": {
"Fn::Equals": ["true", "false"]
}
},
"Outputs": {
"EmptyOutput": {
"Description": "An empty output. You may delete this if you have at least one resource above.",
"Value": ""
}
}
}
But when i try to amplify push, it does not work. Something about: Resource is not in the state stackUpdateComplete
Any help?
You could take a look in cloudformation at the resource. You're instance is probably stuck in update. Go to : Cloudformation, select your instance (or uncheck view nested first) and go to the events tab. There you will probably find a reason why the instance can't update.
If it's stuck, cancel within the stack actions.
EventBridge bus receives events from S3 bucket.
I need to create a rule that only handles S3 events from objects with Key prefix dir/sub-dir/ and suffix .pdf.
Tried the rule below, but it is handled as OR statement:
new events.Rule(this, 'MyRule', {
eventPattern: {
source: ['aws.s3'],
detailType: ['Object Created'],
detail: {
object: {
key: [
{ prefix: 'dir/sub-dir/' },
{
suffix: '.pdf',
},
],
},
},
},
})
Found a way to make it work, but I hope there is a better answer:
new events.Rule(this, 'MyRule', {
eventPattern: {
source: ['aws.s3'],
detailType: ['Object Created'],
detail: {
object: {
key: [
{ prefix: 'dir/sub-dir/' },
$or: [{ key: [{ suffix: '.pdf' }] }, { fieldThatDoesntExist: ['its a hack'] }],
],
},
},
},
})
I'm building a React app using AWS Amplify. I use Cognito User Pools for by authentication and a GraphQL AppSync backend for my backend.
I'm trying to write a custom resolver to batch mutations. Here is the schema that I use:
type Todo #model #auth(rules: [{ allow: owner }]) {
id: ID!
title: String!
description: String
completed: Boolean
}
input CreateTodoInput {
id: ID
title: String!
description: String
completed: Boolean
}
type Mutation {
batchAddTodos(todos: [CreateTodoInput]): [Todo]
}
This schema enables authentication for the GraphQL API using Cognito User Pools.
In order for this custom mutation to work, one needs to add custom resolvers. I changed amplify/api/<your-api-name>/stacks/CustomResources.json to contain the following resources:
// Left everything as it was
"Resources": {
"EmptyResource": {
"Type": "Custom::EmptyResource",
"Condition": "AlwaysFalse"
},
"BatchAddTodosResolver": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Ref": "AppSyncApiId"
},
"DataSourceName": "TodoTable",
"TypeName": "Mutation",
"FieldName": "batchAddTodos",
"RequestMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.batchAddTodos.req.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
},
"ResponseMappingTemplateS3Location": {
"Fn::Sub": [
"s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.batchAddTodos.res.vtl",
{
"S3DeploymentBucket": {
"Ref": "S3DeploymentBucket"
},
"S3DeploymentRootKey": {
"Ref": "S3DeploymentRootKey"
}
}
]
}
}
}
},
// ... more code that I didn't touch
For the custom request resolver, I wrote the following template:
#foreach($item in ${ctx.args.todos})
## [Start] Owner Authorization Checks **
#set( $isOwnerAuthorized = false )
## Authorization rule: { allow: "owner", ownerField: "owner", identityField: "cognito:username" } **
#set( $allowedOwners0 = $util.defaultIfNull($item.owner, null) )
#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"),
$util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#if( $util.isList($allowedOwners0) )
#foreach( $allowedOwner in $allowedOwners0 )
#if( $allowedOwner == $identityValue )
#set( $isOwnerAuthorized = true )
#end
#end
#end
#if( $util.isString($allowedOwners0) )
#if( $allowedOwners0 == $identityValue )
#set( $isOwnerAuthorized = true )
#end
#end
#if( $util.isNull($allowedOwners0) && (! $item.containsKey("owner")) )
$util.qr($item.put("owner", $identityValue))
#set( $isOwnerAuthorized = true )
#end
## [End] Owner Authorization Checks **
## [Start] Throw if unauthorized **
#if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized
== true) )
$util.unauthorized()
#end
## [End] Throw if unauthorized **
#end
#set($todosdata = [])
#foreach($item in ${ctx.args.todos})
$util.qr($item.put("createdAt", $util.time.nowISO8601()))
$util.qr($item.put("updatedAt", $util.time.nowISO8601()))
$util.qr($item.put("__typename", "Todo"))
$util.qr($item.put("id", $util.defaultIfNullOrBlank($item.id, $util.autoId())))
$util.qr($todosdata.add($util.dynamodb.toMapValues($item)))
#end
{
"version": "2018-05-29",
"operation": "BatchPutItem",
"tables": {
"TodoTable": $utils.toJson($todosdata)
}
}
With the first loop, I was trying to verify that the user has access to the todos he creates. With the second loop I added the data that gets added by the resolvers generated by the Amplify CLI. This includes a __typename, timestamps and an id.
Afterwards I make the request to create the resources. I followed this tutorial for the code. Note that I had to update the version to "2018-05-29". The code generated by the Amplify CLI usually has a version of "2017-02-28" (I don't know if this matters).
I also wrote the following mapping for the response resolver:
#if ($ctx.error)
$util.appendError($ctx.error.message, $ctx.error.type, null, $ctx.result.data.unprocessedKeys)
#end
$util.toJson($ctx.result.data)
This basically tells AppSync to return data and the error for all unprocessed items.
I first tried to make the request using React:
import API, { graphqlOperation } from '#aws-amplify/api';
// ... later
async function handleClick() {
const todoFixtures = [
{ id: 1, title: 'Get groceries', description: '', completed: false },
{ id: 2, title: 'Go to the gym', description: 'Leg Day', completed: true }
];
try {
const input = { todos: prepareTodos(todoFixtures) };
const res = await API.graphql(graphqlOperation(batchAddTodos, input));
console.log(res);
} catch (err) {
console.log('error ', err);
}
}
prepareTodos just gets rid of id fields and sets empty fields to null (to avoid DynamoDB to yell at me). Code is at the bottom, since it's irrelevant.
Since this failed, I tried doing the mutation via the AppSync console:
mutation add {
batchAddTodos(todos: [
{title: "Hello", description: "Test", completed: false}
]) {
id title
}
}
But both attempts throw the following error:
{
"data": {
"batchAddTodos": null
},
"errors": [
{
"path": [
"batchAddTodos"
],
"data": null,
"errorType": "DynamoDB:AmazonDynamoDBException",
"errorInfo": null,
"locations": [
{
"line": 32,
"column": 3,
"sourceName": null
}
],
"message": "User: arn:aws:sts::655817346595:assumed-role/Todo-role-naona7ytt5drxazwmtp7a2uccy-batch/APPSYNC_ASSUME_ROLE is not authorized to perform: dynamodb:BatchWriteItem on resource: arn:aws:dynamodb:eu-central-1:655817346595:table/TodoTable (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: AccessDeniedException; Request ID: EP48SJPVMB9G9M69HPR0BO8SKJVV4KQNSO5AEMVJF66Q9ASUAAJG)"
},
{
"path": [
"batchAddTodos"
],
"locations": null,
"message": "Can't resolve value (/batchAddTodos) : type mismatch error, expected type LIST"
}
]
}
This leads me to believe that the React code is "correct" or at least equally wrong as the AppSync code. But I suspect the error is somewhere in the resolver template mapping. I just can't find it. What is going wrong here?
Maybe the generated assume-role doesn't support the "2018-05-19" version? Here is the code for the role default generated role (I didn't write this):
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "DynamoDBAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": [
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
{
"tablename": {
"Fn::If": [
"HasEnvironmentParameter",
{
"Fn::Join": [
"-",
[
"Todo",
{
"Ref": "GetAttGraphQLAPIApiId"
},
{
"Ref": "env"
}
]
]
},
{
"Fn::Join": [
"-",
[
"Todo",
{
"Ref": "GetAttGraphQLAPIApiId"
}
]
]
}
]
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
{
"tablename": {
"Fn::If": [
"HasEnvironmentParameter",
{
"Fn::Join": [
"-",
[
"Todo",
{
"Ref": "GetAttGraphQLAPIApiId"
},
{
"Ref": "env"
}
]
]
},
{
"Fn::Join": [
"-",
[
"Todo",
{
"Ref": "GetAttGraphQLAPIApiId"
}
]
]
}
]
}
}
]
}
]
}
]
}
}
]
prepareTodos:
const map = f => arr => arr.map(f);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const dissoc = prop => ({ [prop]: _, ...obj }) => obj;
const mapObj = f => obj =>
Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: f(obj[key]) }), {});
const replaceEmptyStringWithNull = x => (x === '' ? null : x);
const prepareTodos = map(
pipe(
dissoc('id'),
mapObj(replaceEmptyStringWithNull)
)
);
Edit: I managed to solve the typemismatch. In the response I have to return: $util.toJson($ctx.result.data.TodoTable).
$util.toJson($ctx.result.data.TodoTable) in the response template, as well as changing TodoTable to the actual table name (you can look it up under DynamoDB in your console, it looks like this Todo-dqeronnsgvd2pf3facjmlgtsjk-master) solved the errors.
Also here is a step by step tutorial that I wrote while I stumbled upon this problem.
I've got a simple AppSync API that has a type for City and Venue, the example is to map small retail shops. I'm trying to create a new City via the "Queries" pane within AppSync, I'm also wanting to provide a shortened ID - as opposed to the long autoId so that I can run queries for the shortened ID and the frontend app can use it from the params in the URL.
I'm trying to create a new item in DynamoDB with the following code:
Schema
type City {
id: String!
name: String!
}
type Mutation {
...
createCity(input: CreateCityInput!): City
...
}
input CreateCityInput {
id: String!
name: String!
}
Mutation.createCity
// Request mapping template
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id)
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
},
"attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args)
}
// Response mapping template
$util.toJson($context.result)
Here's the mutation that I ran within the Queries page
mutation CreateCity {
createCity(input: {
id: "11538062"
name: "Manchester"
}) {
id
name
}
}
And this is the error that is returned from AppSync and or Dynamo
{
"data": {
"createCity": null
},
"errors": [
{
"path": [
"createCity"
],
"data": null,
"errorType": "DynamoDB:AmazonDynamoDBException",
"errorInfo": null,
"locations": [
{
"line": 32,
"column": 3,
"sourceName": null
}
],
"message": "One or more parameter values were invalid: Type mismatch for key id expected: S actual: NULL (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: 7PU4D2...)"
}
]
}
Any help would be really appreciated, I can't see why I'm unable to perform a simple Create using a provided String as the ID and it identifies it as NULL...
## Request Mapping
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.input.id)
},
"attributeValues" : {
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.input.name)
}
}
How do I generate ID automatically when I use AppSync and DynamoDB as the database source?
I have a type looking like below
type Post {
id: ID!
creator: String!
createdAt: String!
like: Int!
dislike: Int!
frozen: Boolean!
}
and input looking like below
input CreatePostInput {
id: ID!
creator: String!
createdAt: String!
like: Int!
dislike: Int!
frozen: Boolean!
}
and my mutation is obviously the combination of the two
createPost(input: CreatePostInput!): Post
However, when I make inserts I have to do something like below
mutation createPost{
createPost(input:{
id:2
creator:"some creator"
createdAt:"some date"
like:0
dislike:0
frozen:false
}){
id
creator
createdAt
like
dislike
frozen
}
}
There is no way I can insert a post without having to know what the id is up to right now. Is there a way I can provide null or any random id and DynamoDB or AppSync automatically create the next index before inserting into the table?
Error generated when I update the input CreatePostInput to not
accept any ID
{
"data": {
"createPost": null
},
"errors": [
{
"path": [
"createPost"
],
"data": null,
"errorType": "DynamoDB:AmazonDynamoDBException",
"errorInfo": null,
"locations": [
{
"line": 2,
"column": 3,
"sourceName": null
}
],
"message": "One or more parameter values were invalid: Type mismatch for key id expected: S actual: NULL (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: 629QEM7MH9BRAJ9MHU3FM1S0U3VV4KQNSO5AEMVJF66Q9ASUAAJG)"
}
]
}
My Resolver Template
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id)
"id": $util.dynamodb.toDynamoDBJson($util.autoId()),
},
"attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args)
}
First, update the createPost resolver as:
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($util.autoId()),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id",
},
},
}
Then, remove the id field from your CreatePostInput input type:
input CreatePostInput {
creator: String!
createdAt: String!
like: Int!
dislike: Int!
frozen: Boolean!
}
Save both the changes and you should be done.
If you're the one of seeing-is-believing guys like me, take a look at this egghead.io video.
You can auto generate a UUIDv4 using the $util.autoId() velocity helper function. For example, you can write a createPost resolver that uses a server-side generated id like this.
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"id": $util.dynamodb.toDynamoDBJson($util.autoId()),
},
"attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args)
}