How to filter the data from dynamo db when the key is not a partition or Sort key with Node.js and typescript? - aws-lambda

My table looks like [alias, inheritedLdap, LdapGroup ] here alias is the string and the LdapGroup is the List form eg: [{S:aws}]. So basically my use case is to get the list of aliases whose ldapGroup is aws. Here the alias is the partition key, we don't have the sort key. So I need to write a method which takes the ldapGroup as the parameter and filter the list of the alias when the ldapGroup is aws. But ldapGroup doesn't contain scalar values. I tried to implement the code but its failing when I try to compile,
public async getMemberList(): Promise<any> {
const input: any = {
TableName: UserInfoDao.TABLE_NAME, // use this from the constants
ProjectionExpression: "alias",
FilterExpression: "#l = :ldapGroups",
ExpressionAttributeNames: {
"#l": "ldapGroups"
},
ExpressionAttributeValues: {
":ldapGroups": "PPOA"
}
};
try {
const ddbClient = DynamDBClient.getInstance();
return await ddbClient.scan(input);
} catch (error) {
const message = `ERROR: Failed to retrieve alias for given ldapGroups:
ERROR: ${JSON.stringify(error)}`;
error.message = message;
throw error;
}
}
But when I use the ScanCommandOutput and ScanCommadInput in my code instead of any, its shows the error that the
Type 'Record<string, AttributeValue>[] | undefined' is not assignable to type 'ScanCommandInput'. Type 'undefined' is not assignable to type 'ScanCommandInput'
Property '$metadata' is missing in type 'Request<ScanOutput, AWSError>' but required in type 'ScanCommandOutput'.
Can someone help me with this one.
I am expecting whether my approach is correct or not

This works for me, I made some edits you your example:
import { DynamoDBClient } from "#aws-sdk/client-dynamodb";
import { ScanCommand, ScanCommandInput } from "#aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({
region: 'eu-west-1',
});
class MyClass {
public getMemberList(): Promise<any> {
const input: ScanCommandInput = {
TableName: 'Test1',
// ProjectionExpression: "alias",
FilterExpression: "contains(#l, :ldapGroups)",
ExpressionAttributeNames: {
"#l": "ldapGroups"
},
ExpressionAttributeValues: {
":ldapGroups": "aws"
}
};
try {
return client.send(new ScanCommand(input))
} catch (error) {
const message = `ERROR: Failed to retrieve alias for given ldapGroups: ERROR: ${JSON.stringify(error)}`;
error.message = message;
throw error;
}
}
}
const c = new MyClass();
c.getMemberList().then(res => console.log(res)).catch(err => console.log(err));

Related

MissingRequiredParameter: Missing required key 'TableName' in params : Serverless Framework

I am getting the error stack below, The code looks fine to me, don't know where is the error!
My handler code is below :
import AWS from 'aws-sdk';
import commonMiddleware from '../lib/commonMiddleware';
import createError from 'http-errors';
const dynamodb = new AWS.DynamoDB.DocumentClient();
async function placeBid(event, context) {
const { id } = event.pathParameters;
const { amount } = event.body;
console.log('**********************************');
console.log(process.env.AUCTIONS_TABLE_NAME);
console.log('**********************************');
const params = {
TableName: process.env.AUCTIONS_TABLE_NAME,
Key: {
id: { S: id}
},
UpdateExpression: "set highestBid.amount = :amount",
ExpressionAttributeValues: {
":amount": { S: amount },
},
ReturnValues: "UPDATED_NEW"
};
console.log('**********************************');
console.log(JSON.stringify(params));
console.log('**********************************');
let updatedAuction;
try {
const result = await dynamodb.update({params}).promise();
console.log({result});
updatedAuction = result.Attributes;
} catch(e) {
console.log({'error': e});
throw new createError.InternalServerError(e);
}
return {
statusCode: 200,
body: JSON.stringify(updatedAuction)
};
}
export const handler = commonMiddleware(placeBid);
2021-06-16T15:41:20.179Z 722a3b28-9f67-44eb-8e5a-85368325dae5 INFO {
error: MultipleValidationErrors: There were 2 validation errors:
* MissingRequiredParameter: Missing required key 'TableName' in params
* MissingRequiredParameter: Missing required key 'Key' in params
at ParamValidator.validate (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:40:28)
at Request.VALIDATE_PARAMETERS (/var/runtime/node_modules/aws-sdk/lib/event_listeners.js:132:42)
at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
at callNextListener (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:96:12)
at /var/runtime/node_modules/aws-sdk/lib/event_listeners.js:86:9
at finish (/var/runtime/node_modules/aws-sdk/lib/config.js:386:7)
at /var/runtime/node_modules/aws-sdk/lib/config.js:404:9
at EnvironmentCredentials.get (/var/runtime/node_modules/aws-sdk/lib/credentials.js:127:7)
at getAsyncCredentials (/var/runtime/node_modules/aws-sdk/lib/config.js:398:24)
at Config.getCredentials (/var/runtime/node_modules/aws-sdk/lib/config.js:418:9) {
code: 'MultipleValidationErrors',
errors: [ [Error], [Error] ],
time: 2021-06-16T15:41:20.176Z
}
}
You've nested params in another object. This causes the method call to receive:
{ params: { TableName: 'AUCTIONS_TABLE_NAME', Key: { id: id}}}
Try replacing dynamodb.update({params}) with just dynamodb.update(params).
Also with the document client, you don't need to specify the type.
Key: {id} will be fine.

Graphql directives doesn't work for mutation input arguments when pass the arguments as external variables

I am implementing custom Graphql directives to validate client input. A sample code as below, I referred to the official examples here: https://www.apollographql.com/docs/apollo-server/schema/creating-directives/#enforcing-value-restrictions
const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');
const { GraphQLScalarType, GraphQLNonNull } = require('graphql');
const typeDefs = gql`
directive #validateInput on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
type Mutation {
sampleMutation(
test1: String #validateInput
nestedInput: SampleMutationInput
): String
}
input SampleMutationInput {
test2: String #validateInput
}
`;
The implementation of the directive logic:
class ValidateInputDirective extends SchemaDirectiveVisitor {
visitInputFieldDefinition(field) {
this.wrapType(field);
}
visitFieldDefinition(field) {
this.wrapType(field);
}
visitArgumentDefinition(argument) {
console.log('visitArgumentDefinition', argument);
this.wrapType(argument);
}
wrapType(field) {
console.log('wrapType', field);
if (
field.type instanceof GraphQLNonNull &&
field.type.ofType instanceof GraphQLScalarType
) {
field.type = new GraphQLNonNull(
new ValidateInputType(field.type.ofType)
);
} else if (field.type instanceof GraphQLScalarType) {
field.type = new ValidateInputType(field.type);
} else {
throw new Error(`Not a scalar type: ${field.type}`);
}
}
}
class ValidateInputType extends GraphQLScalarType {
constructor(type) {
super({
name: 'ValidatedInput',
serialize(value) {
return value;
},
parseValue(value) {
const result = type.parseValue(value);
if (/[?!]/.test(result)) {
throw new Error('Invalid characters');
}
return result;
},
parseLiteral(ast) {
const result = type.parseLiteral(ast);
if (/[?!]/.test(result)) {
throw new Error('Invalid characters');
}
return result;
},
});
}
}
export default { validateInput: ValidateInputDirective };
It works as expected for the input field 'test2', but for the argument 'test1', it works when the String value is directly passed to the mutation, then the method "parseLiteral" is called and the validation logic applied to the input value. However, when I pass the 'test1' value as external variables (via JSON format), the directive doesn't work and the method "parserValue" never be called.
What I found so far:
"parserValue" is used when the input comes from variable JSON. "parseLiteral" is used when the input comes directly from the query/mutation.
It seems a bug in Graphql tools according to https://github.com/ardatan/graphql-tools/issues/789
I want to understand:
what's the real difference between an argument passed by variable and directly pass to mutation?
is there an alternate way to apply the directives to an argument to avoid this issue?
If this is really a bug with Graphql, does it fixed now? Which version should I use to resolve the issue?

throw a descriptive error with graphql and apollo

Consider the following class:
// entity/Account.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { Field, Int, ObjectType } from 'type-graphql'
#ObjectType()
#Entity()
export class Account extends BaseEntity {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number
#Field()
#Column({ length: 50, unique: true })
#Index({ unique: true })
accountIdentifier: string
#Field({ nullable: true })
#Column({ length: 100 })
name?: string
}
With it's corresponding resolver:
// AccountResolver.ts
#Resolver()
export class AccountResolver {
#Mutation(() => Account)
async addAccount(#Arg('options', () => AccountInput) options: AccountInput) {
try {
// if (!options.accountIdentifier) {
// throw new Error(`Failed adding account: the accountIdentifier is missing`)
// }
return await Account.create(options).save()
} catch (error) {
if (error.message.includes('Cannot insert duplicate key')) {
throw new Error(
`Failed adding account: the account already exists. ${error}`
)
} else {
throw new Error(`Failed adding account: ${error}`)
}
}
}
}
Jest test file
// AccountResolver.test.ts
describe('the addAccount Mutation', () => {
it('should throw an error when the accountIdentifier is missing', async () => {
await expect(
client.mutate({
mutation: gql`
mutation {
addAccount(
options: {
name: "James Bond"
userName: "James.Bond#contoso.com"
}
) {
accountIdentifier
}
}
`,
})
).rejects.toThrowError('the accountIdentifier is missing')
})
The field accountIdentifier is mandatory and should throw a descriptive error message when it's missing in the request. However, the error thrown is:
"Network error: Response not successful: Received status code 400"
What is the correct way to modify the error message? I looked at type-graphql with the class-validators and made sure that validate: true is set but it doesn't give a descriptive error.
EDIT
After checking the graphql playground, it does show the correct error message by default. The only question remaining is how write the jest test so it can read this message:
{
"error": {
"errors": [
{
"message": "Field AccountInput.accountIdentifier of required type String! was not provided.",
Thank you for any help you could give me.
The ApolloError returned by your client wraps both the errors returned in the response and any network errors encountered while executing the request. The former is accessible under the graphQLErrors property, the latter under the networkError property. Instea dof using toThrowError, you should use toMatchObject instead:
const expectedError = {
graphQLErrors: [{ message: 'the accountIdentifier is missing' }]
}
await expect(client.mutate(...)).rejects.toMatchObject(expectedError)
However, I would suggest avoiding using Apollo Client for testing. Instead, you can execute operations directly against your schema.
import { buildSchema } from 'type-graphql'
import { graphql } from 'graphql'
const schema = await buildSchema({
resolvers: [...],
})
const query = '{ someField }'
const context = {}
const variables = {}
const { data, errors } = await graphql(schema, query, {}, context, variables)

Graphql returning Cannot return null for non-nullable field Query.getDate. As I am new to graphql I want to know is my approach is wrong or my code?

I have created resolver, schema and handler which will fetch some record from dynamoDB. Now when I perform query then I am getting "Cannot return null for non-nullable field Query.getDate" error. I would like to know whether my approach is wrong or there is any change required in code.
My code : https://gist.github.com/vivek-chavan/95e7450ff73c8382a48fb5e6a5b96025
Input to lambda :
{
"query": "query getDate {\r\n getDate(id: \"0f92fa40-8036-11e8-b106-952d7c9eb822#eu-west-1:ba1c96e7-92ff-4d63-879a-93d5e397b18a\") {\r\n id\r\n transaction_date\r\n }\r\n }"
}
Response :
{
"errors": [
{
"message": "Cannot return null for non-nullable field Query.getDate.",
"locations": [
{
"line": 2,
"column": 7
}
],
"path": [
"getDate"
]
}
],
"data": null
}
Logs of lambda function :
[ { Error: Cannot return null for non-nullable field Query.getDate.
at completeValue (/var/task/node_modules/graphql/execution/execute.js:568:13)
at completeValueCatchingError (/var/task/node_modules/graphql/execution/execute.js:503:19)
at resolveField (/var/task/node_modules/graphql/execution/execute.js:447:10)
at executeFields (/var/task/node_modules/graphql/execution/execute.js:293:18)
at executeOperation (/var/task/node_modules/graphql/execution/execute.js:237:122)
at executeImpl (/var/task/node_modules/graphql/execution/execute.js:85:14)
at execute (/var/task/node_modules/graphql/execution/execute.js:62:229)
at graphqlImpl (/var/task/node_modules/graphql/graphql.js:86:31)
at /var/task/node_modules/graphql/graphql.js:32:223
at graphql (/var/task/node_modules/graphql/graphql.js:30:10)
message: 'Cannot return null for non-nullable field Query.getDate.',
locations: [Object],
path: [Object] } ],
data: null }
2019-02-25T10:07:16.340Z 9f75d1ea-2659-490b-ba59-5289a5d18d73 { Item:
{ model: 'g5',
transaction_date: '2018-07-05T09:30:31.391Z',
id: '0f92fa40-8036-11e8-b106-952d7c9eb822#eu-west-1:ba1c96e7-92ff-4d63-879a-93d5e397b18a',
make: 'moto' } }
Thanks in advance!
This is your code:
const data = {
getDate(args) {
var params = {
TableName: 'delete_this',
Key: {
"id": args.id
}
};
client.get(params, function(err,data){
if(err){
console.log('error occured '+err)
}else{
console.log(data)
}
});
},
};
const resolvers = {
Query: {
getDate: (root, args) => data.getDate(args),
},
};
You're seeing that error because getDate is a a Non-Null field in your schema, but it is resolving to null. Your resolver needs to return either a value of the appropriate type, or a Promise that will resolve to that value. If you change data like this
const data = {
getDate(args) {
return {
id: 'someString',
transaction_date: 'someString',
}
}
}
you'll see the error go away. Of course, your goal is to return data from your database, so we need to add that code back in. However, your existing code utilizes a callback. Anything you do inside the callback is irrelevant because it's ran after your resolver function returns. So we need to use a Promise instead.
While you can wrap a callback with Promise, that shouldn't be necessary with aws-sdk since newer versions support Promises. Something like this should be sufficient:
const data = {
getDate(args) {
const params = //...
// must return the resulting Promise here
return client.get(params).promise().then(result => {
return {
// id and transaction_date based on result
}
})
}
}
Or using async/await syntax:
const data = {
async getDate(args) {
const params = //...
const result = await client.get(params).promise()
return {
// id and transaction_date based on result
}
}
}

Building GraphQL Resolver to Return List of Strings -- Receiving [object Object] Instead of Strings

I am developing a web application that queries an OrientDB Graph Database using GraphQL. It uses Apollo Server to resolve incoming GraphQL queries.
I want to build a query that will simply return the 'name' field for each "Topic" Object as a list of Strings. e.g.:
{
"data": {
"allTopicNames": [
"Topic 1",
"Topic 2",
"Topic 3",
"Topic 4"
]
}
}
To do so, I created a Type Definition:
// Imports: GraphQL
import { gql } from 'apollo-server-express';
// GraphQL: TypeDefs
const TYPEDEFS = gql`
type Query {
allTopics: [Topic]
topic(name: String): [Topic]
allTopicNames: [String] //This is the new Type Definition -- we want a list of Strings
}
type Topic {
name: String
}
`;
// Exports
export default TYPEDEFS;
And the associated Resolver:
//Connect to OrientDB
var OrientJs = require('orientjs');
var server = OrientJs({
host: "localhost",
port: "2424",
username: "root",
password: "root"
});
var db = server.use({
name: 'database',
username: 'root',
password: 'root'
});
// GraphQL: Resolvers
const RESOLVERS = {
Query: {
allTopics: () => {
return db.query('SELECT FROM Topic ORDER BY name');
},
allTopicNames: () => {
return db.query('SELECT name FROM Topic ORDER BY name'); //This is the new resolver
},
topic: (obj, args) => {
return db.query('SELECT FROM Topic WHERE name=\'' + args.name + '\' LIMIT 1');
}
}
};
// Exports
export default RESOLVERS;
However, when I try to implement the above Type Definition and Resolver, I receive a list of strings which are all "[object Object]" instead of the actual strings:
{
"data": {
"allTopicNames": [
"[object Object]",
"[object Object]",
"[object Object]",
"[object Object]"
]
}
}
I tried to add some code to the resolver that would iterate through each object and create a proper list of Strings to return:
// GraphQL: Resolvers
const RESOLVERS = {
Query: {
allTopics: () => {
return db.query('SELECT FROM Topic ORDER BY name');
},
allTopicNames: () => {
let the_list_of_records = db.query('SELECT name FROM Topic ORDER BY name').then(res => {
let the_list_of_names = []; //We'll return a List of Strings using this
for(var i = 0; i < res.length; i++){
the_list_of_names.push(res[i]['name']);
}
console.log(the_list_of_names);
return the_list_of_names;
});
},
topic: (obj, args) => {
return db.query('SELECT FROM Topic WHERE name=\'' + args.name + '\' LIMIT 1');
}
}
};
But this didn't work, resulting in a null value being returned instead:
{
"data": {
"allTopicNames": null
}
}
I'm frankly confused as to why I can't get a simple list of Strings to populate via this resolver. Perhaps I'm missing something obvious -- any insight is greatly appreciated!
Your initial approach didn't work as expected because you were returning an array of objects. Your second attempt returns null because you don't return anything inside your resolver. Your resolver should always return a value or a Promise that will resolve to that value, otherwise the resolved value for the field will always be null.
The value of the_list_of_records will be a Promise, so you can just return that and that should be sufficient. But we can make this code a little easier to read using map like this:
allTopicNames: () => {
return db.query('SELECT name FROM Topic ORDER BY name').then(res => {
return res.map(topic => topic.name)
})
}
// using async/await
allTopicNames: async () => {
await topics = await db.query('SELECT name FROM Topic ORDER BY name')
return topics.map(topic => topic.name)
}

Resources