Reputation: 903
I have a node.js project powered by apollo-server. I use custom @admin
directive that does permission checking on queries, mutations and object fields. For queries and mutation this directive throws errors, for fields it returns null instead of real value.
Now, I want to add graphiql ui to my project, so fellow developers can explore my graphql schema. However, I want them to see the schema as anonymous user sees it, i.e. they should not know about existence of @admin
fields and @admin
queries and all mutations (even non-admin ones). Even those who have credentials to execute these operations (i.e. logged in as admin) should not see those parts of schema.
As far as I understand, graphiql sends special introspection query, which contains __schema
and __type
fields to display schema and its documentation.
Is it possible to somehow modify my schema, which is constructed using makeExecutableSchema
from graphql-tools
to achieve my goal?
Upvotes: 5
Views: 3263
Reputation: 271
With graphql-introspection-filtering
it is possible to achieve it with single schema. There is an example with authentication directive in docs. I tested it with simple use cases only, but it looks promising.
I tried it with graphql@^14.0.0
and graphql-tools@^4.0.0
.
Upvotes: 1
Reputation: 903
Ended up having two schemas, as Tal Z suggested. Only I create two identical schemas and then modify one:
import { makeExecutableSchema, visitSchema } from 'graphql-tools';
import { LimitIntrospection } from './IntrospectionVisitor';
export async function getGraphiqlSchema(): Promise<GraphQLSchema> {
if (!graphiqlSchema) {
graphiqlSchema = await loadSchema(); // same as for "private" schema
const visitor = new LimitIntrospection();
visitSchema(graphiqlSchema, () => [visitor]);
}
return graphiqlSchema;
}
Here is the visitor. Tried to visit fields, but was unable to delete field from its parent from inside the field visitor. So resorted to visiting objects. Also have to set _mutationType to null, because you cannot delete all fields form mutation.
import { DirectiveNode, GraphQLField, GraphQLObjectType, GraphQLSchema } from 'graphql';
import { SchemaVisitor } from 'graphql-tools';
const isAdmin = (field: GraphQLField<any, any>): boolean =>
!!field.astNode &&
!!field.astNode.directives &&
field.astNode.directives.some((d: DirectiveNode) => d.name.value === 'admin');
export class LimitIntrospection extends SchemaVisitor {
visitSchema(schema: GraphQLSchema) {
(schema as any)._mutationType = null;
}
visitObject(object: GraphQLObjectType) {
const fields = object.getFields();
const adminKeys = Object.values(fields).reduce(
(ak, field) => isAdmin(field) ? [...ak, field.name] : ak,
[],
);
adminKeys.forEach(key => delete fields[key]);
}
}
I'm still looking for a solution with one schema.
Upvotes: 0
Reputation: 3210
You can use an extended schema for the main endpoint and use the same schema without the extension for the graphiql endpoint.
Let's take this schema definition as an example:
// schemaDef
type Query {
anonQuery: QueryResult
adminQuery: AdminQueryResult @admin
}
And the executable schema:
const schema = makeExecutableSchema({
typeDefs: [schemaDef /* ... additional schema files */],
resolvers: merge(schemaResolvers/* ... additional resolvers */)
})
Now, let's split the schema definitions with the help of the extend
keyword.
Read here about Extending Types and the extend
keyword.
// anonymous part of the original schema definition:
type Query {
anonQuery: QueryResult
}
// admin extensions definitions:
extend type Query {
adminQuery: AdminQueryResult @admin
}
To avoid having some warnings about resolvers not defined in the schema, you'll probably want to split the admin related resolvers to another file or another resolver map.
Now you'll have 2 executable schemas:
const mainSchema = makeExecutableSchema({
typeDefs: [schemaDef /* ... additional schema files */],
resolvers: merge(schemaResolvers/* ... additional resolvers */)
})
const extendedSchema = makeExecutableSchema({
typeDefs: [schemaDef, adminSchemaExtensions /* ... additional schema files */],
resolvers: merge(schemaResolvers, adminSchemaResolvers /* ... additional resolvers */)
})
Your main endpoint should use the extended schema.
router.use('/graphql', /* some middlewares */, graphqlExpress({schema: extendedSchema}))
Since the GraphiQL endpoint expects a GraphQL endpoint, you'll have to create another one specifically for the second schema. Perhaps something like this:
router.use('/graphql-anon', /* some middlewares */, graphqlExpress({schema: mainSchema}))
router.use('/graphiql', /* some middlewares */, graphiqlExpress({endpointURL: '/graphql-anon'}))
That's it!
Now most of your code is shared and only part of the schema is accessible using the GraphiQL interface.
Putting the admin definitions in separate files could be seen as more convenient or less convenient, depending on your project, code and preferences.
Upvotes: 1