I am trying to implement a custom directive with apollo server. I took the example from the official site.
My query is like below:
directive #upper on FIELD_DEFINITION
type Query {
hello: String #upper
}
My resolver is like below:
Query:{
async hello(){
return "hello world";
}
}
Here is my apollo server config for custom directive:
const { ApolloServer, SchemaDirectiveVisitor } = require('apollo-server-express');
const { defaultFieldResolver } = require("graphql");
class UpperCaseDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const result = await resolve.apply(this, args);
if (typeof result === "string") {
return result.toUpperCase();
}
return result;
};
}
}
const server = new ApolloServer({
schema,
schemaDirectives: {
upper: UpperCaseDirective
},
introspection: true,
playground: true,
cors: cors()
});
The output I always get :
{
"data": {
"hello": "hello world"
}
}
Why the custom directive is not activated? Why the output is not in uppercase?
You would pass schemaDirectives to ApolloServer's constructor if ApolloServer was building your schema for you -- that is, if you were also passing in resolvers and typeDefs. If you're passing in an existing schema, it's already built and ApolloServer won't apply the directives. If you're using makeExecutableSchema, you can pass your schemaDirectives to that. It's also possible to manually visit all directives like this:
SchemaDirectiveVisitor.visitSchemaDirectives(schema, schemaDirectives)
This is the only way to get directives to work with certain libraries, like graphql-modules.
Related
I'm writing an Apollo server plugin for node.js, and my goal is to improve my teams debugging experience. My plugin currently looks something like this:
export function eddyApolloPlugin(): ApolloServerPlugin {
return {
requestDidStart(requestContext) {
// Set requestId on the header
const requestId = (requestContext?.context as EddyContext)?.requestId;
if (requestId) {
requestContext.response?.http?.headers.set('requestId', requestId);
}
return {
willSendResponse(context) { // <== Where do I find the "path" in the schema here?
// Inspired by this: https://blog.sentry.io/2020/07/22/handling-graphql-errors-using-sentry
// and the official documentation here: https://docs.sentry.io/platforms/node/
// handle all errors
for (const error of requestContext?.errors || []) {
handleError(error, context);
}
},
};
},
};
}
I would like to know if I can access the path in the schema here? It's pretty easy to find the name of mutaiton/query with operation.operationName, but where can I get the name of the query/mutation as defined in the schema?
Solution
export function eddyApolloPlugin(): ApolloServerPlugin {
return {
requestDidStart(requestContext) {
// Set requestId on the header
const requestId = (requestContext?.context as EddyContext)?.requestId;
if (requestId) {
requestContext.response?.http?.headers.set('requestId', requestId);
}
return {
didResolveOperation(context) {
const operationDefinition = context.document
.definitions[0] as OperationDefinitionNode;
const fieldNode = operationDefinition?.selectionSet
.selections[0] as FieldNode;
const queryName = fieldNode?.name?.value;
// queryName is what I was looking for!
},
};
},
};
}
Your requirement is not very clear. If you want to get the name of the query/mutation to distinguish which query or mutation the client sends.
You could get the name from context.response.data in willSendResponse event handler.
E.g.
server.ts:
import { ApolloServer, gql } from 'apollo-server';
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import { parse, OperationDefinitionNode, FieldNode } from 'graphql';
function eddyApolloPlugin(): ApolloServerPlugin {
return {
requestDidStart(requestContext) {
return {
didResolveOperation(context) {
console.log('didResolveOperation');
const obj = parse(context.request.query!);
const operationDefinition = obj.definitions[0] as OperationDefinitionNode;
const selection = operationDefinition.selectionSet.selections[0] as FieldNode;
console.log('operationName: ', context.request.operationName);
console.log(`${context.operation!.operation} name:`, selection.name.value);
},
willSendResponse(context) {
console.log('willSendResponse');
console.log('operationName: ', context.request.operationName);
console.log(`${context.operation!.operation} name:`, Object.keys(context.response.data!)[0]);
},
};
},
};
}
const typeDefs = gql`
type Query {
hello: String
}
type Mutation {
update: String
}
`;
const resolvers = {
Query: {
hello() {
return 'Hello, World!';
},
},
Mutation: {
update() {
return 'success';
},
},
};
const server = new ApolloServer({ typeDefs, resolvers, plugins: [eddyApolloPlugin()] });
const port = 3000;
server.listen(port).then(({ url }) => console.log(`Server is ready at ${url}`));
GraphQL Query:
query test {
hello
}
the logs of the server:
didResolveOperation
operationName: test
query name: hello
willSendResponse
operationName: test
query name: hello
GraphQL Mutation:
mutation test {
update
}
the logs of the server:
didResolveOperation
operationName: test
mutation name: update
willSendResponse
operationName: test
mutation name: update
I was wondering if there is anyway to configure a directive.
I have some options I load during the startup I would like to pass to the directive.
Edit: I known that I can pass options from the schema, but that's not what I'm looking for.
new ApolloServer({
typeDefs: [schema, constraintDirectiveTypeDefs],
schemaDirectives: { myDirect: Mydirective}
});
or
SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
constraint: Mydirective
});
Something like :
export class ConstraintDirective extends SchemaDirectiveVisitor {
constuctor(options) {
}
visitInputFieldDefinition(field: GraphQLInputField): void {
}
Regards
What you pass to ApolloServer is a class, not its instance. At best, you could dynamically create the class:
const server = new ApolloServer({
...
schemaDirectives: {
foo: createFooDirective(SOME_VALUE)
},
})
const createFooDirective = (bar) => {
return class FooDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
// SOME_VALUE is now available as bar
}
}
}
I need help building a GraphQL Api that wraps the ChuckNorris.io API
The API sholud have aQuery type that resolves all Categories
(https://api.chuckmorris.io/jokes/categories)
The Api should have Query type that resolves a random joke given as an argument (https://api.chucknorris.io/jokes/random?category={category})
const express=require('express');
const {ApolloServer,gql}=require('apollo-server-express');
const fetch=require('node-fetch');
const typeDefs=gql`
type Joke{
icon_url:String,
id:String,
url:String
value: String
}
type Category{
animal:String
career:String
celebrity:String
dev:String
explicit:String
fashion:String
food:String
history:String
money:String
movie:String
music:String
political:Strig
religion:String
science:String
sport:String
travel:String
}
type Query{
getCategory(category:String!):Joke
category:Category
}
`
const resolvers={
Query:{
getCategory: async(_,{category})=>{
const response=await fetch(`https://api.chucknorris.io/jokes/random?category=${category}`)
return response.json();
},
category: async(_,{})=>{
const response=await fetch('https://api.chucknorris.io/jokes/categories')
return response.json();
}
}
}
const server= new ApolloServer({typeDefs,resolvers});
const app=express();
server.applyMiddleware({app});
app.listen({port:4000},()=>
console.log('Now browse to http://localhost:4000' + server.graphqlPath)
)
your query for type category should return a list of strings (array)
so
export const typeDefs = gql`
type Joke {
value: String!
id:ID!
icon_url:String!
}
type Query {
getAllCategories:[String!]!
randomJoke(category: String!):Joke
}
`;
for your resolver, you don't need fetch. apollo provides datasources to connect to external REST APIs like the one you have.
so install the npm package "apollo-datasource-rest" and add it to your instance of apollo server like so
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: ()=>({
jokeinstance : new Jokenorris
})
})
then create the datasource class for Jokenorris and import appropriately or do everything in one src file as you did.
import pkg from "apollo-datasource-rest";
const { RESTDataSource } = pkg;
export class Jokenorris extends RESTDataSource {
constructor() {
super();
this.baseURL = "https://api.chucknorris.io/jokes";
}
async getAllCategories() {
const res = await this.get("categories");
return res;
}
async getRandomJoke({ category }) {
const response = await this.get("random", { category: category });
return response;
}
}
then your resolveer can look like so, you can ignore the exports and imports if you chunked everything in one file
export const resolvers = {
Query: {
allJokeCategories: (_, __, { dataSources }) =>
dataSources.jokeinstance.getAllCategories(),
randomJoke: (_, {category}, {dataSources})=>
dataSources.jokeinstance.getRandomJoke({category:category})
},
};
So, when I have a schema:
type Query { me: Me }
type Me { hello: String }
I expect a satisfying resolver to be:
const resolvers = {
Me: {
hello() { return "Hi, me!"; }
}
}
Alas, this isn't the case, I have to add a dummy me-resolver (see below).
Why is this the case?
I'd say it should traverse the query, and if it can't find a satisfying field-resolver, it should look for a corresponding type-resolver.
const { graphql } = require("graphql");
const { makeExecutableSchema } = require("graphql-tools");
const compose = require("compose-function");
const typeDefs = `
type Query {
me: Me
}
type Me {
hello: String!
}
`;
const resolvers = {
Query: {
// ===========================>
me() {
return {};
}
// <===========================
},
Me: {
hello() {
return "Hi, me!";
}
}
};
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
graphql(schema, "{ me { hello } }").then(
compose(
console.log,
JSON.stringify
)
); // {"data":{"me":{"hello":"Hi, me!"}}}
Alright, I've figured it out. It actually makes a lot of sense.
null is a valid response. Only if there is an object with properties that the query wants, it should look for the next resolver to satisfy the query.
It's also written in the spec under Errors and Non‐Null Fields https://graphql.github.io/graphql-spec/June2018/#sec-Executing-Selection-Sets
I currently have a Node.js back-end running Express with Passport.js for authentication and am attempting to switch to GraphQL with Apollo Server. My goal is to implement the same authentication I am using currently, but cannot figure out how to leave certain resolvers public while enabling authorization for others. (I have tried researching this question extensively yet have not been able to find a suitable solution thus far.)
Here is my code as it currently stands:
My JWT Strategy:
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = JWT_SECRET;
module.exports = passport => {
passport.use(
new JwtStrategy(opts, async (payload, done) => {
try {
const user = await UserModel.findById(payload.sub);
if (!user) {
return done(null, false, { message: "User does not exist!" });
}
done(null, user);
} catch (error) {
done(err, false);
}
})
);
}
My server.js and Apollo configuration:
(I am currently extracting the bearer token from the HTTP headers and passing it along to my resolvers using the context object):
const apollo = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
let authToken = "";
try {
if (req.headers.authorization) {
authToken = req.headers.authorization.split(" ")[1];
}
} catch (e) {
console.error("Could not fetch user info", e);
}
return {
authToken
};
}
});
apollo.applyMiddleware({ app });
And finally, my resolvers:
exports.resolvers = {
Query: {
hello() {
return "Hello world!";
},
async getUserInfo(root, args, context) {
try {
const { id } = args;
let user = await UserModel.findById(id);
return user;
} catch (error) {
return "null";
}
},
async events() {
try {
const eventsList = await EventModel.find({});
return eventsList;
} catch (e) {
return [];
}
}
}
};
My goal is to leave certain queries such as the first one ("hello") public while restricting the others to requests with valid bearer tokens only. However, I am not sure how to implement this authorization in the resolvers using Passport.js and Passport-JWT specifically (it is generally done by adding middleware to certain endpoints, however since I would only have one endpoint (/graphql) in this example, that option would restrict all queries to authenticated users only which is not what I am looking for. I have to perform the authorization in the resolvers somehow, yet not sure how to do this with the tools available in Passport.js.)
Any advice is greatly appreciated!
I would create a schema directive to authorized query on field definition and then use that directive wherever I want to apply authorization. Sample code :
class authDirective extends SchemaDirectiveVisitor {
visitObject(type) {
this.ensureFieldsWrapped(type);
type._requiredAuthRole = this.args.requires;
}
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
field._requiredAuthRole = this.args.requires;
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._authFieldsWrapped) return;
objectType._authFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const {
resolve = defaultFieldResolver
} = field;
field.resolve = async function (...args) {
// your authorization code
return resolve.apply(this, args);
};
});
}
}
And declare this in type definition
directive #authorization(requires: String) on OBJECT | FIELD_DEFINITION
map schema directive in your schema
....
resolvers,
schemaDirectives: {
authorization: authDirective
}
Then use it on your api end point or any object
Query: {
hello { ... }
getuserInfo():Result #authorization(requires:authToken) {...}
events():EventResult #authorization(requires:authToken) {...}
};