Conditionally put Item into DynamoDB - aws-lambda

In my Lambda function, I want to conditionally put items into my DynamoDB only if the value DOESN'T EXIST already. I saw multiple different sources where they use this ConditionExpression and i cant figure out whats wrong with that.
body = await dynamo.put({
TableName: 'polit-stream',
Item: {
urlPath: data.urlPath,
},
ConditionExpression: "attribute_not_exists(urlPath)"
}).promise();
The put will always be successful, even if my secondary index value (urlPath) already exists.
Full Code:
const AWS = require('aws-sdk');
const crypto = require("crypto");
const dynamo = new AWS.DynamoDB.DocumentClient();
/**
* Demonstrates a simple HTTP endpoint using API Gateway. You have full
* access to the request and response payload, including headers and
* status code.
*
* To scan a DynamoDB table, make a GET request with the TableName as a
* query string parameter. To put, update, or delete an item, make a POST,
* PUT, or DELETE request respectively, passing in the payload to the
* DynamoDB API as a JSON body.
*/
exports.handler = async(event, context) => {
let body;
let statusCode = '200';
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '* '
}
const data = JSON.parse(event.body);
const generateUUID = () => crypto.randomBytes(16).toString("hex");
try {
switch (event.httpMethod) {
case 'DELETE':
body = await dynamo.delete(JSON.parse(event.body)).promise();
break;
case 'GET':
body = await dynamo.scan({
TableName: 'db',
IndexName: 'urlPath',
FilterExpression: "urlPath = :urlPath",
ExpressionAttributeValues: {
":urlPath": event.queryStringParameters.urlPath
}
},
function(data) {
}).promise();
break;
case 'POST':
body = await dynamo.put({
TableName: 'db',
Item: {
id: generateUUID(),
name: data.name,
date: data.date,
place: data.place,
goals: data.goals,
type: data.type,
org: data.org,
email: data.email,
urlPath: data.urlPath,
createdAt: new Date().toISOString()
},
ConditionExpression: "attribute_not_exists(urlPath)"
}).promise();
break;
case 'PUT':
body = await dynamo.update(JSON.parse(event.body)).promise();
break;
default:
throw new Error(`Unsupported method "${event.httpMethod}"`);
}
}
catch (err) {
statusCode = '400';
body = err.message;
}
finally {
body = JSON.stringify(body);
}
return {
statusCode,
body,
headers,
};
};

Your error is that you misunderstood what ConditionExpression can do. Your full PutItem code is:
body = await dynamo.put({
TableName: 'db',
Item: {
id: generateUUID(),
name: data.name,
date: data.date,
place: data.place,
goals: data.goals,
type: data.type,
org: data.org,
email: data.email,
urlPath: data.urlPath,
createdAt: new Date().toISOString()
},
ConditionExpression: "attribute_not_exists(urlPath)"
}
What did you expect ConditionExpression: "attribute_not_exists(urlPath)" to do?
Apparently you thought that it will check whether any item exists with this value of urlPath. But this is not, unfortunately, what this expression does. What it does is to look at one specific item - the item with the same key (I don't know what is your key, id?) and check whether this specific item has a urlPath attribute (with any value).
If urlPath was the item's key, this work like you hoped it would. If the urlPath is unique (which it seems it is, according to what you wanted to do) then it can indeed serve as the item key.

In order to use the ConditionExpression you need to provide name and value for the attributes. Try this:
await dynamo.put({
TableName: 'db',
Item: {
id: generateUUID(),
name: data.name,
date: data.date,
place: data.place,
goals: data.goals,
type: data.type,
org: data.org,
email: data.email,
urlPath: data.urlPath,
createdAt: new Date().toISOString(),
},
ConditionExpression: "attribute_not_exists(#u) or (#u=:urlPath)",
ExpressionAttributeNames: { "#u": "urlPath" },
ExpressionAttributeValues: { ":urlPath": data.urlPath },
});

Related

Get headers with executeOperation on Apollo Server (apollo-server) for integrations tests

Since the deprecation of apollo-server-testing I am using the new way of doing integration tests with apollo-server (included in apollo-server 2.25.0). From the mutation signin I set my refresh token in the OutgoingMessage header's (in 'Set-Cookie').
Simplified resolver
#Mutation(() => RefreshTokenOutput)
async refreshToken(#Ctx() { response, contextRefreshToken }: Context): Promise<RefreshTokenOutput> {
if (contextRefreshToken) {
const { accessToken, refreshToken } = await this.authService.refreshToken(contextRefreshToken);
response.setHeader(
'Set-Cookie',
cookie.serialize('refreshToken', refreshToken, {
httpOnly: true,
maxAge: maxAge,
secure: true,
})
);
return { accessToken: accessToken };
} else {
throw new AuthenticationError();
}
}
Test case
// given:
const { user, clearPassword } = await userLoader.createUser16c();
const input = new UserSigninInput();
input.email = user.email;
input.password = clearPassword;
const MUTATE_signin = gql`
mutation signin($userInput: UserSigninInput!) {
signin(input: $userInput) {
accessToken
}
}
`;
// when:
const res = await server.executeOperation(
{ query: MUTATE_signin, variables: { userInput: input }, operationName: 'signin' },
buildContext(user)
);
I'm trying to test if this token is correctly set and well formed. Did you have any idea on how I can access this header with executeOperation ?
I was able to set headers like this:
const res = await apolloServer.executeOperation({ query: chicken, variables: { id: 1 } }, {req: {headers: 'Authorization sdf'}});
server.executeOperation calls processGraphQLRequest
and processGraphQLRequest return type is GraphQLResponse
export interface GraphQLResponse {
data?: Record<string, any> | null;
errors?: ReadonlyArray<GraphQLFormattedError>;
extensions?: Record<string, any>;
http?: Pick<Response, 'headers'> & Partial<Pick<Mutable<Response>, 'status'>>;
}
I'm not sure, but i think headers in GraphQLResponse.http
you can find call structure in github repo.
https://github.com/apollographql/apollo-server/blob/6b9c2a0f1932e6d8fb94a8662cc1da24980aec6f/packages/apollo-server-core/src/requestPipeline.ts#L126
Apollo defines executeOperation as:
public async executeOperation(
request: Omit<GraphQLRequest, 'query'> & {
query?: string | DocumentNode;
},
integrationContextArgument?: ContextFunctionParams,
) {
integrationContextArgument is optional and ContextFunctionParams is just an alias to any.
As mentioned in an answer above, any context JSON passed to the executeOperation function will be sent to Apollo's processGraphQLRequest() function
graphQLServerOptions() function processes that JSON.
For more advanced scenarios, it seems that a context resolver function, not just JSON context data, can be passed in using the context field
Reference: https://github.com/apollographql/apollo-server/blob/e9ae0f28d11d2fdfc5abd5048c85acf70de21592/packages/apollo-server-core/src/ApolloServer.ts#L1014

Optimistic response not working when adding items to list

My data model is a list with items. Very simple:
{
_id: 1,
name: "List 1",
items: [
{ _id: 2, text: "Item text 1" },
{ _id: 3, text: "Item text 2" }
]
}
Adding a new list with optimistic response works perfectly:
const [addListMutation] = useAddListMutation({
update: (cache, { data }) => {
const cachedLists =
(cache.readQuery<GetAllListsQuery>({
query: GetAllListsDocument,
})?.lists as TList[]) ?? [];
if (data) {
cache.writeQuery({
query: GetAllListsDocument,
data: {
lists: [...cachedLists, data?.list as TList],
},
});
}
},
});
const addList = async (name: string) => {
const list = {
_id: ..new id here,
name,
items: [],
};
const variables: AddListMutationVariables = {
data: list,
};
await addListMutation({
variables,
optimisticResponse: {
list,
},
});
};
This gets reflected immediately in my component using const { loading, data } = useGetAllListsQuery();. data is updated twice; first with the optimistic response and then after the mutation is done. Just like expected.
Now I'm trying to add an item to the list this way:
const [updateListMutation] = useUpdateListMutation({
update: (cache, { data }) => {
const cachedLists =
(cache.readQuery<GetAllListsQuery>(
{
query: GetAllListsDocument,
},
)?.lists as TList[]) ?? [];
if (data?.list) {
// Find existing list to update
const updatedList = data?.list as TList;
const updatedListIndex = cachedLists.findIndex(
(list: TList) => list._id === updatedList._id,
);
// Create a copy of cached lists and replace entire list
// with new list from { data }.
const updatedLists = [...cachedLists];
updatedLists[updatedListIndex] = { ...updatedList };
cache.writeQuery({
query: GetAllListsDocument,
data: {
lists: updatedLists,
},
});
}
}
});
const updateList = async (updatedList: TList) => {
const variables: UpdateListMutationVariables = {
query: {
_id: updatedList._id,
},
set: updatedList,
};
await updateListMutation({
variables,
optimisticResponse: {
list: updatedList,
},
});
};
const addListItem = async (list: TList, text: string) => {
const updatedList = R.clone(list);
updatedList.items.push({
_id: ...new item id here,
text: 'My new list item',
});
await updateList(updatedList);
};
The problem is is in my component and the const { loading, data } = useGetAllListsQuery(); not returning what I expect. When data first changes with the optimistic response it contains an empty list item:
{
_id: 1,
name: "List 1",
items: [{}]
}
And only after the mutation response returns, it populates the items array with the item with text 'My new list item'. So my component first updates when the mutation is finished and not with the optimistic response because it can't figure out to update the array. Don't know why?
(and I have checked that the updatedLists array in writeQuery correctly contains the new item with text 'My new list item' so I'm trying to write the correct data).
Please let me know if you have any hints or solutions.
I've tried playing around with the cache (right now it's just initialized default like new InMemoryCache({})). I can see the cache is normalized with a bunch of List:1, List:2, ... and ListItem:3, ListItem:4, ...
Tried to disable normalization so I only have List:{id} entries. Didn't help. Also tried to add __typename: 'ListItem' to item added, but that only caused the { data } in the update: ... for the optimistic response to be undefined. I have used hours on this now. It should be a fairly simple and common use case what I'm trying to do :).
package.json
"#apollo/client": "^3.3.4",
"graphql": "^15.4.0",
"#graphql-codegen/typescript": "^1.19.0",

How to access the context in GraphQL from your resolvers

I just want to send the request to all my resolvers through the context field, but when I access it from one of my resolvers, it returns null.
app.use('/graphql', graphqlHTTP(async (request, response, graphQLParams) => ({
schema: schema,
context:{token_1:null,test:request},
graphiql:true
})));
These are part of my schema. Firstly I Login to set the token ,but when I want to access the context.token_1 from the other resolver (BuyItems), it returns null.
BuyItems :{
type: UserType,
args: {
name: {type: GraphQLString},
points: {type: GraphQLInt}
},
resolve(parent,args,context){
console.log(context.token_1)
return UserModel.findOneAndUpdate({name:args.name},{points:args.points})
}
},
Login: {
type: AuthType,
args: {
email: {type:GraphQLString},
password: {type:GraphQLString}
},
async resolve(parent,args,context){
const user = await UserModel.findOne({ email: args.email });
if (!user) {
throw new Error('User does not exist on login!');
}
const isEqual = await bcrypt.compare(args.password, user.password);
if (!isEqual) {
throw new Error('Password is incorrect!');
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
'somesupersecretkey',
{ expiresIn: '1h' }
);
context.token_1 = token;
return {tokenExpiration: 1, userId: user.id, token:token}
}
}

Graphql multiple arguments in field

I'm using GraphQL.
I'm able to pass one argument in a field. But I would like to know how to pass multiple arguments to a field.
This is my code:
GraphlQL Object type: Price availability
const priceAvailability = new GraphQLObjectType({
name: "priceAvailability",
description: "Check price and availability of article",
fields: () => ({
articleID: {
type: GraphQLString
},
priceType:{
type:GraphQLString
},
stockAvailability: {
type: StockAvailabilityType,
resolve(parentValue, args) {
// stuff to get the price and availability
return (data = getStockAvailability.getStockAvailability(
parentValue.isbn, parentValue.omgeving
));
}
}
})
});
The root query
const RootQuery = new GraphQLObjectType({
name: "RootQuery",
fields: () => ({
price: {
type: new GraphQLList(priceAvailability),
args: [{
articleID: {
type: new GraphQLList(GraphQLString),
description:
'List with articles. Example: ["artid1","artid2"]'
},
priceType: {
type: new GraphQLList(GraphQLString) ,
description:
'PriceType. Example: "SalePrice","CurrentPrice"'
}]
},
resolve: function(_, { articleID , priceType}) {
var data = [];
// code to return data here
return data;
}
}
})
});
Schema
module.exports = new GraphQLSchema({
query: RootQuery
});
This is the query I use in GraphiQL to test:
{
query: price(articleID:"ART03903", priceType:"SalePrice" ){
stockAvailability {
QuantityAvailable24hrs
QuantityAvailable48hrs
}
}
}
I can get the articleID via parentValue.articleID, but I have issues with getting parentValue.priceType.
Also GraphiQL tells me that priceType does not exists:
Unknown argument “priceType”. On field “price” of type “RootQuery”
args for a field takes an object instead of an array. Try:
args: {
articleID: {
type: new GraphQLList(GraphQLString),
description: 'List with articles. Example: ["artid1","artid2"]'
},
priceType: {
type: new GraphQLList(GraphQLString) ,
description: 'PriceType. Example: "SalePrice","CurrentPrice"'
},
}

GraphQL how to mutate data

I have a basic schema for mutating some data which looks like
const schema = new graphql.GraphQLSchema({
mutation: new graphql.GraphQLObjectType({
name: 'Remove',
fields: {
removeUser: {
type: userType,
args: {
id: { type: graphql.GraphQLString }
},
resolve(_, args) {
const removedData = data[args.id];
delete data[args.id];
return removedData;
},
},
},
})
});
Looking around google I cant find a clear example of the example query which needs to be sent to mutate.
I have tried
POST -
localhost:3000/graphql?query={removeUser(id:"1"){id, name}}
This fails with error:
{
"errors": [
{
"message": "Cannot query field \"removeUser\" on type \"Query\".",
"locations": [
{
"line": 1,
"column": 2
}
]
}
]
}
In order to post requests from the front-end application it is recommended to use apollo-client package. Say i wanted to validate a user login information:
import gql from 'graphql-tag';
import ApolloClient, {createNetworkInterface} from 'apollo-client';
client = new ApolloClient({
networkInterface: createNetworkInterface('http://localhost:3000/graphql')
});
remove(){
client.mutate({
mutation: gql`
mutation remove(
$id: String!
) {
removeUser(
id: $id
){
id,
name
}
}
`,
variables: {
id: "1"
}
}).then((graphQLResult)=> {
const { errors, data } = graphQLResult;
if(!errors && data){
console.log('removed successfully ' + data.id + ' ' + data.name);
}else{
console.log('failed to remove');
}
})
}
More information about apollo-client can be found here
Have you tried using graphiql to query and mutate your schema?
If you'd like to create a POST request manually you might wanna try to struct it in the right form:
?query=mutation{removeUser(id:"1"){id, name}}
(Haven't tried POSTing myself, let me know if you succeeded, i structured this out of the url when using graphiql)
You have to explicitly label your mutation as such, i.e.
mutation {
removeUser(id: "1"){
id,
name
}
}
In GraphQL, if you leave out the mutation keyword, it's just a shorthand for sending a query, i.e. the execution engine will interpret it as
query {
removeUser(id: "1"){
id,
name
}
}
cf. Section 2.3 of the GraphQL Specification
const client = require("../common/gqlClient")();
const {
createContestParticipants,
} = require("../common/queriesAndMutations");
const gql = require("graphql-tag");
const createPartpantGql = async (predictObj) => {
try {
let resp = await client.mutate({
mutation: gql(createContestParticipants),
variables: {
input: {
...predictObj,
},
},
});
let contestParticipantResp = resp.data.createContestParticipants;
return {
success: true,
data: contestParticipantResp,
};
} catch (err) {
console.log(err.message)
console.error(`Error creating the contest`);
return {
success: false,
message: JSON.stringify(err.message),
};
}
};

Resources