throw a descriptive error with graphql and apollo - graphql

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)

Related

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

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));

TypeGraphQL - Not able to match all the interfaces of a union

Summary
The goal is to declare the return type of a mutation using a union in order to express multiple states: Success and user errors
Being able to select concrete types according to the use cases:
mutation($data: CreateUserInput!) {
createUser(data: $data){
... on CreateUserSuccess {
user {
id
}
}
... on EmailTakenError {
emailWasTaken
}
... on UserError {
code
message
}
}
}
Implementation using TypeGraphQ:
#ObjectType()
class CreateUserSuccess {
#Field(() => User)
user: User
}
#ObjectType()
class EmailTakenError {
#Field()
emailWasTaken: boolean
}
const mapMutationValueKeyToObjectType = {
user: CreateUserSuccess,
code: UserError,
emailWasTaken: EmailTakenError
}
const CreateUserPayload = createUnionType({
name: 'CreateUserPayload',
types: () => [CreateUserSuccess, EmailTakenError, UserError] as const,
resolveType: mutationValue => {
const mapperKeys = Object.keys(mapMutationValueKeyToObjectType)
const mutationValueKey = mapperKeys.find((key) => key in mutationValue)
return mapMutationValueKeyToObjectType[mutationValueKey]
}
})
#InputType()
class CreateUserInput implements Partial<User> {
#Field()
name: string
#Field()
email: string
}
#Resolver(User)
export class UserResolver {
#Mutation(() => CreateUserPayload)
createUser (#Arg('data', {
description: 'Represents the input data needed to create a new user'
}) createUserInput: CreateUserInput) {
const { name, email } = createUserInput
return createUser({ name, email })
}
}
Data layer
export const createUser = async ({
name, email
}: { name: string; email: string; }) => {
const existingUser = await dbClient.user.findUnique({
where: {
email
}
})
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
return dbClient.user.create({
data: {
name,
email
}
})
}
Issue
The response doesn't resolve all of the selected fields according to their unions, even by returning fields that are related to different types
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
My doubt is this case is, why emailWasTaken is not being returned within the response if the EmailTakenError type is being selected?
This was an interpretation mistake on my part
The reasoning is that resolvers with a union type as the return definition should indeed just return one of those, in the case above, UserError and EmailTakenError wouldn't be returned on the same response
More info on this GitHub discussion

How to upload file using nestjs-graphql-fastify server and how to test such feature?

I struggle to upload .csv file to nestjs-graphql-fastify server. Tried following code:
#Mutation(() => Boolean)
async createUsers(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<boolean> {
try {
// backend logic . . .
} catch {
return false;
}
return true;
}
but all I get when testing with postman is this response:
{
"statusCode": 415,
"code": "FST_ERR_CTP_INVALID_MEDIA_TYPE",
"error": "Unsupported Media Type",
"message": "Unsupported Media Type: multipart/form-data; boundary=--------------------------511769018912715357993837"
}
Developing with code-first approach.
Update: Tried to use fastify-multipart but issue still remains. What has changed is response in postman:
POST body missing, invalid Content-Type, or JSON object has no keys.
Found some answer's on Nestjs discord channel.
You had to do following changes:
main.ts
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastify = adapter.getInstance();
fastify.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastify.addHook('preValidation', async function (request: any, reply) {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
);
await app.listen(apiServerPort, apiServerHost);
}
bootstrap();
upload.scalar.ts
import { Scalar } from '#nestjs/graphql';
import { GraphQLUpload } from 'graphql-upload';
#Scalar('Upload')
export class UploadGraphQLScalar {
protected parseValue(value) {
return GraphQLUpload.parseValue(value);
}
protected serialize(value) {
return GraphQLUpload.serialize(value);
}
protected parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}
users.resolver.ts
#Mutation(() => CreateUsersOutput, {name: 'createUsers'})
async createUsers(
#Args('input', new ValidationPipe()) input: CreateUsersInput,
#ReqUser() reqUser: RequestUser,
): Promise<CreateUsersOutput> {
return this.usersService.createUsers(input, reqUser);
}
create-shared.input.ts
#InputType()
export class DataObject {
#Field(() => UploadGraphQLScalar)
#Exclude()
public readonly csv?: Promise<FileUpload>;
}
#InputType()
#ArgsType()
export class CreateUsersInput {
#Field(() => DataObject)
public readonly data: DataObject;
}
Also, I want to mention you should not use global validation pipes (in my case they made files unreadable)
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
You could use graphql-python/gql to try to upload a file:
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
transport = AIOHTTPTransport(url='YOUR_URL')
client = Client(transport=transport)
query = gql('''
mutation($file: Upload!) {
createUsers(file: $file)
}
''')
with open("YOUR_FILE_PATH", "rb") as f:
params = {"file": f}
result = client.execute(
query, variable_values=params, upload_files=True
)
print(result)
If you activate logging, you can see some message exchanged between the client and the backend.

#client Apollo GQL tag breaks query

I have a vue-apollo (using nuxt) query that is supposed to have a local client field show. However, when I have the show #client line included in the query the component does not render. For some reason it also seems to fail silently.
query myAccounts {
accounts: myAccounts {
email
calendars {
id
name
hex_color
is_enabled
show #client
}
}
}
I am extending the Calendar type in an extensions.js file (pasted below) with two mutations.
import gql from 'graphql-tag'
export const typeDefs = gql`
extend type Calendar {
show: Boolean
}
type Mutation {
showCalendar(id: ID!): Boolean
hideCalendar(id: ID!): Boolean
}
`
Here is the resolver that sets the value, along with the Apollo config:
import { InMemoryCache } from 'apollo-cache-inmemory'
import { typeDefs } from './extensions'
import MY_ACCOUNTS_QUERY from '~/apollo/queries/MyAccounts'
const cache = new InMemoryCache()
const resolvers = {
Mutation: {
showCalendar: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: MY_ACCOUNTS_QUERY })
const found = data.accounts
.flatMap(({ calendars }) => calendars)
.find(({ id }) => id === '1842')
if (found) {
found.show = true
}
cache.writeQuery({ query: todoItemsQuery, data })
return true
}
}
}
export default context => {
return {
cache,
typeDefs,
resolvers,
httpLinkOptions: {
credentials: 'same-origin'
},
}
}
along with the nuxt config:
apollo: {
defaultOptions: {
$query: {
loadingKey: 'loading',
fetchPolicy: 'cache-and-network',
},
},
errorHandler: '~/plugins/apollo-error-handler.js',
clientConfigs: {
default: '~/apollo/apollo-config.js'
}
}
Querying local state requires the state to exist (i.e. it should be initialized) or for a local resolver to be defined for the field. Apollo will run the resolver first, or check the cache directly for the value if a resolver is not defined. There's not really a good way to initialize that value since it's nested inside a remote query, so you can add a resolver:
const resolvers = {
Calendar: {
show: (parent) => !!parent.show,
},
// the rest of your resolvers
}
See the docs for additional examples and more details.

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