I am using graphql-tools#v6 and I have implemented two directives #map and #filter. My goal is to use them like a map and filter pipeline. In some cases, I want to map before filtering and in other cases, vice-versa. The directives are implemented using the Schema Directives API and they work as expected when only one directive is applied.
However, if I use them together, then they always execute in one specific order which doesn't match how they are declared in the schema.
For example
directive #map on FIELD_DEFINITION
directive #filter on FIELD_DEFINITION
# usage
type MyType {
list1: [String!]! #map #filter
list2: [String!]! #filter #map
}
In this case, either both fields are mapped and then filtered or vice-versa. The order is controlled by how I pass them in schemaTransforms property.
const schema = makeExecutableSchema({
schemaTransforms: [mapDirective, filterDirective] # vs [filterDirective, mapDirective]
});
I believe since these transforms are passed as an array, so their order of execution depends on the ordering of array. I can replace them with directiveResolvers but they are limited in what they can do.
But what throws me off is the following statement from the documentation
Existing code that uses directiveResolvers could consider migrating to direct usage of mapSchema
Because they have different behavior when it comes to order of execution, I don't see how they are interchangeable.
Can someone explain if there is a way to guarantee that the Schema Directives execute in the order they are used in the schema for a particular field?
Please see this github issue for in depth discussion.
The new API doesn't work the same way as directiveResolvers or schemaDirectives. A schemaTransform is applied to the entire schema before the next one contrary to the other two in which the all the transforms are applied to a particular field before visiting the next field node. There are two approaches to this in my opinion:
Create a new #pipeline directive which takes a list of names of other directives and then applies them in the order like directiveResolvers.
I took a bit different route where I created a new function attachSchemaTransforms just like attachDirectiveResolvers which visits each node and applies all the directives in order.
export function attachSchemaTransforms(
schema: GraphQLSchema,
schemaTransforms: Record<string, FieldDirectiveConfig>, // a custom config object which contains the transform and the directive name
): GraphQLSchema {
if (typeof schemaTransforms !== 'object') {
throw new Error(`Expected schemaTransforms to be of type object, got ${typeof schemaTransforms}`);
}
if (Array.isArray(schemaTransforms)) {
throw new Error('Expected schemaTransforms to be of type object, got Array');
}
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: oldFieldConfig => {
const fieldConfig = { ...oldFieldConfig };
const directives = getDirectives(schema, fieldConfig);
Object.keys(directives).forEach(directiveName => {
const config = schemaTransforms[directiveName];
if (config) {
const { apply, name } = config;
const directives = getDirectives(schema, fieldConfig);
if (directives[name]) {
const directiveArgs: unknown = directives[name]
apply(fieldConfig, directiveArgs);
return fieldConfig;
}
}
});
return fieldConfig;
},
});
}
Related
I'm building a custom directive in which I'm hoping to validate entire input objects. I'm using the INPUT_OBJECT type with the visitInputObject method on SchemaDirectiveVisitor extended class.
Every time I run a mutation using the input type then visitInputObject does not run.
I've used the other types/methods like visitObject and visitFieldDefinition and they work perfectly. But when trying to use input types and methods they will not trigger.
I've read all the available documentation I can find. Is this just not supported yet?
Some context code(Not actual):
directive #validateThis on INPUT_OBJECT
input MyInputType #validateThis {
id: ID
someField: String
}
type Mutation {
someMutation(myInput: MyInputType!): SomeType
}
class ValidateThisDirective extends SchemaDirectiveVisitor {
visitInputObject(type) {
console.log('Not triggering');
}
}
All the visit methods of a SchemaDirectiveVisitor are ran at the same time -- when the schema is built. That includes visitFieldDefinition and visitFieldDefinition. The difference is that when we use visitFieldDefinition, we often do it to modify the resolve function for the visited field. It's this function that's called during execution.
You use each visit methods to modify the respective schema element. You can use visitInputObject to modify an input object, for example to add or remove fields from it. You cannot use it to modify the resolution logic of an output object's field. You should use visitFieldDefinition for that.
visitFieldDefinition(field, details) {
const { resolve = defaultFieldResolver } = field
field.resolve = async function (parent, args, context, info) {
Object.keys(args).forEach(argName => {
const argDefinition = field.args.find(a => a.name === argName)
// Note: you may have to "unwrap" the type if it's a list or non-null
const argType = argDefinition.type
if (argType.name === 'InputTypeToValidate') {
const argValue = args[argName]
// validate here
}
})
return resolve.apply(this, [parent, args, context, info]);
}
}
How would you scan schema for missing resolver for queries and non-scalar fields ?
I'm trying to work with a dynamic schema so I need to be able to test this programmatically. I've been browsing graphql tools for few hours to find a way to do this, but I'm getting nowhere...
checkForResolveTypeResolver - this only apply to interface and union resolveType resolver
I can't find a way to know when a defaultFieldResolver is applied
I tried working with custom directives to add #requiredResolver, to help identify those fields, but custom resolver are far from being fully supported:
introspection & directives
no graphql-js directives handler (can workaround this with graphql-tools tho)
any help is appreciated !
Given an instance of GraphQLSchema (i.e. what's returned by makeExecutableSchema) and your resolvers object, you can just check it yourself. Something like this should work:
const { isObjectType, isWrappingType, isLeafType } = require('graphql')
assertAllResolversDefined (schema, resolvers) {
// Loop through all the types in the schema
const typeMap = schema.getTypeMap()
for (const typeName in typeMap) {
const type = schema.getType(typeName)
// We only care about ObjectTypes
// Note: this will include Query, Mutation and Subscription
if (isObjectType(type) && !typeName.startsWith('__')) {
// Now loop through all the fields in the object
const fieldMap = type.getFields()
for (const fieldName in fieldMap) {
const field = fieldMap[fieldName]
let fieldType = field.type
// "Unwrap" the type in case it's a list or non-null
while (isWrappingType(fieldType)) {
fieldType = fieldType.ofType
}
// Only check fields that don't return scalars or enums
// If you want to check *only* non-scalars, use isScalarType
if (!isLeafType(fieldType)) {
if (!resolvers[typeName]) {
throw new Error(
`Type ${typeName} in schema but not in resolvers map.`
)
}
if (!resolvers[typeName][fieldName]) {
throw new Error(
`Field ${fieldName} of type ${typeName} in schema but not in resolvers map.`
)
}
}
}
}
}
}
I have a gatsby site with the contentful plugin and graphql queries (setup is working).
[EDIT]
My gatsby setup pulls data dynamically using the pageCreate feature. And populates my template component, the root graphql query of which I've shared below. I can create multiple pages using the setup if the pages on contentful follow the structure given in the below query.
[/EDIT]
My question is about a limitation I seemed to have come across or just don't know enough grpahql to understand this yet.
My high level content model 'BasicPageLayout' consists of references to other content types through the field 'Section'. So, it's flexible in terms of which content types are contained in the 'BasicPageLayout' and the order in which they are added.
Root page query
export const pageQuery = graphql`
query basicPageQuery {
contentfulBasicPageLayout(pageName: {eq: "Home"}) {
heroSection {
parent {
id
}
...HeroFields
}
section1 {
parent {
id
}
...ContentText
}
section2 {
parent {
id
}
...ContentTextOverMedia
}
section3 {
parent {
id
}
...ContentTextAndImage
}
section4 {
parent {
id
}
...ContentText
}
}
}
The content type fragments all live in the respecitve UI components.
The above query and setup are working.
Now, I have "Home" Hard coded because I'm having trouble creating a flexible reusable query. I'm taking advantage of contentful's flexible nature when creating the models, but haven't found a way to create that flexibility in the graphql query for it.
What I do know:
Graphql query is resolved at run time, so everything that needs to be fetched should be in that query. It can't be 'dynamic'.
Issue: The 'Section' fields in the basicPageLayout can link to any content type. So we can mix and match the granular level content types. How do I add the content type fragment (like ContentTextAndImage vs ContentText) so it is appropriate for that section instance ('Section' field in the query)?
In other words
I'd like the root query to get 'Home' data which might have 4 sections, all of type - ContentTextOverMedia
as well as 'About ' data that might have also have 4 sections but with alternating types - ContentText and ContentTextAndImage
This is the goal because I want to create content (Pages) by mix-matching content types on contentful, without needing to update the code each time a new Page is created. Which is why Contentful is useful and was picked in the first place.
My ideas so far:
A. Run two queries, in series. One fetches the parent.id on each section and that holds the content type info. Second fetches the data using the appropriate fragment.
B. Fetch the JSON file of the basicPageLayouts content instance (such as 'Home') separately through Contentful API, and using that JSON file create the graphql string to be used in each instance (So, different layout for Home, About, and so on)
This needs more experimentation, not sure if it's viable, could also be more complex then it needs to be.
So, please share thoughts on the above paths that I'm exploring or another solution that I haven't considered using graphql or gatsby's features.
This is my first question on SO btw, I've spent some time on refining it and trying to follow the guidelines but please do give me feedback in comments so I can improve even if you don't have an answer to my question.
Thanks in advance.
If I understood correctly you want to create pages dynamically from the data coming from Contentful.
You can achieve this using the Gatsbyjs Node API specifically createPage.
In your gatsby-node.js file you can have something like this
const fs = require('fs-extra')
const path = require('path')
exports.createPages = ({graphql, boundActionCreators}) => {
const {createPage} = boundActionCreators
return new Promise((resolve, reject) => {
const landingPageTemplate = path.resolve('src/templates/landing-page.js')
resolve(
graphql(`
{
allContentfulBesicPageLayout {
edges {
node {
pageName
}
}
}
}
`).then((result) => {
if (result.errors) {
reject(result.errors)
}
result.data.allContentfulBesicPageLayout.edges.forEach((edge) => {
createPage ({
path: `${edge.node.pageName}`,
component: landingPageTemplate,
context: {
slug: edge.node.pageName // this will passed to each page gatsby create
}
})
})
return
})
)
})
}
Now in your src/templates/landing-page.js
import React, { Component } from 'react'
const LandingPage = ({data}) => {
return (<div>Add you html here</div>)
}
export const pageQuery = graphql`
query basicPageQuery($pageName: String!) {
contentfulBasicPageLayout(pageName: {eq: $pageName}) {
heroSection {
parent {
id
}
...HeroFields
}
section1 {
parent {
id
}
...ContentText
}
section2 {
parent {
id
}
...ContentTextOverMedia
}
section3 {
parent {
id
}
...ContentTextAndImage
}
section4 {
parent {
id
}
...ContentText
}
}
}
note the $pageName param that's what was passed to the component context when creating a page.
This way you will end up creating as many pages as you want.
Please note: the react part of the code was not tested but I hope you get the idea.
Update:
To have a flexible query you instead of having your content Types as single ref field, you can have one field called sections and you can add the section you want there in the order you desire.
Your query will look like this
export const pageQuery = graphql`
query basicPageQuery($pageName: String!) {
contentfulBasicPageLayout(pageName: {eq: $pageName}) {
sections {
... on ContentfulHeroFields {
internal {
type
}
}
}
}
Khaled
This works fine
query QryTopics {
topics {
nodes {
name
topicId
count
}
}
}
But I want a filtered result. I'm new to graphql but I see a param on this collection called 'where', after 'first', 'last', 'after' etc... How can I use that? Its type is 'RootTopicsTermArgs' which is likely something autogenerated from my schema. It has fields, one of which is 'childless' of Boolean. What I'm trying to do, is return only topics (a custom taxonomy in Wordpress) which have posts tagged with them. Basically it prevents me from doing this on the client.
data.data.topics.nodes.filter(n => n.count !== null)
Can anyone direct me to a good example of using where args with a collection? I have tried every permutation of syntax I could think of. Inlcuding
topics(where:childless:true)
topics(where: childless: 'true')
topics(where: new RootTopicsTermArgs())
etc...
Obviously those are all wrong.
If a custom taxonomy, such as Topics, is registered to "show_in_graphql" and is part of your Schema you can query using arguments like so:
query Topics {
topics(where: {childless: true}) {
edges {
node {
id
name
}
}
}
}
Additionally, you could use a static query combined with variables, like so:
query Topics($where:RootTopicsTermArgs!) {
topics(where:$where) {
edges {
node {
id
name
}
}
}
}
$variables = {
"where": {
"childless": true
}
};
One thing I would recommend is using a GraphiQL IDE, such as https://github.com/skevy/graphiql-app, which will help with validating your queries by providing hints as you type, and visual indicators of invalid queries.
You can see an example of using arguments to query terms here: https://playground.wpgraphql.com/#/connections-and-arguments
we define a type in GraphQL like this:
const GraphQLTodo = new GraphQLObjectType({
name: 'Todo',
fields: {
id: globalIdField('Todo'),
text: {
type: GraphQLString,
resolve: (obj) => obj.text,
},
complete: {
type: GraphQLBoolean,
resolve: (obj) => obj.complete,
},
},
interfaces: [nodeInterface], // what is this?
});
and I've read there is GraphQLInterfaceType - is more suitable when the types are basically the same but some of the fields are different(is this something like a foreign key?)
and in Relay we get the nodefield and nodeInterface with nodeDefinitions:
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'Todo') {
return getTodo(id);
} else if (type === 'User') {
return getUser(id);
}
return null;
},
(obj) => {
if (obj instanceof Todo) {
return GraphQLTodo;
} else if (obj instanceof User) {
return GraphQLUser;
}
return null;
}
);
The docs and samples only used one on interfaces: [] //it's an array. but when do I need to use many interfaces? I am just confused on what it is, I've read a lot about it(don't know if my understanding is correct), just can't seem to wrap it in my head
A GraphQLInterfaceType is one way GraphQL achieves polymorphism, i.e. types that consist of multiple object types. For example, suppose you have two base object types, Post and Comment. Suppose you want a field that could get a list of both comments and posts. Conveniently, both these types have an id, text, and author field. This is the perfect use case for an interface type. An interface type is a group of shared fields, and it can be implemented by any object type which possesses those fields. So we create an Authored interface and say the Comment and Post implement this interface. By placing this Authored type on a GraphQL field, that field can resolve either posts or comments (or a heterogeneous list of both types).
But wait, Post and Comment accept an array of interfaces. I could pass multiple interfaces here. Why? Since the requirement for implementing an interface is possession of all the fields in that interface, there is no reason why any object type can't implement multiple interfaces. To draw from your example, the Node interface in Relay only needs id. Since our Post and Comment have id, they could implement both Node and Authored. But many other types will likely implement Node, ones that aren't part of Authored.
This makes your object types much more re-usable. If you assign interfaces to your field instead of object types, you can easily add new possible types to the fields in your schema as long as you stick to these agreed-upon interfaces.