Skip to content

A utility that makes working with GraphQL Schema Definition Language far easier

License

Notifications You must be signed in to change notification settings

nyteshade/ne-schemata

Repository files navigation

ne-schemata Build Status Greenkeeper badge MIT license package version

Logo

GraphQL IDL/Schemata String class to make working with GraphQL easier

NOTE!!

Henceforth, this library will be called @nyteshade/ne-schemata. Please this npm library will not be maintained on this name after this release.

Overview

Working with Schemata (Schema Definition Language), sometimes called IDL (Interface Definition Language) can occasionally be difficult. A schema definition is the most concise way to specify a schema in GraphQL.

Classes in programming languages are data structures that associated properties and functions designed to work with them. The provided Schemata class extends String and should be able to be used in most places in JavaScript where strings are used with no changes. This, however, is where things change.

Some of the most often applied tasks with SDL are building a schema object, building an executable schema object, parsing it into ASTNode objects. Occasionally a given bit of SDL is checked for validity.

The Schemata class does all this and more.

Features

Biggest selling points of working with the Schemata class

  • Creation of bound or non-bound GraphQLSchema and ASTNode object
  • Storage of an associated resolvers map
  • Storage of an associated schema object for iteration and modification
  • Checking of the validity of SDL as a ASTNode as well as a GraphQLSchema
  • Merge SDL together via AST parsing
  • Merge multple GraphQLSchemas via AST parsing
  • Pare down one SDL/Schema using another as a guide

Recent Breaking Changes

4.0.0

The package has been renamed to @nejs/schemata in coordination with several projects. Reliance on flow-type has been removed in favor of using pure JavaScript with types documeted in jsdoc comment blocks.

2.4.0, 2.4.1 -> 3.0.1

2.4.0 and 2.4.1 are both actually breaking changes. Tests have been updated and version updated appropriately for 3.0.0. The breaking changes are as follows:

// Previously
let schemata = gql`type Query { name: String }` // -> Schemata instance

// Currently
let ast = gql`type Query { name: String }` // -> Proxy(ASTNode) with access to Schemata

The gql template string function no returns an AST node as is inline with with Apollo GraphQL's implementation of a template string with the same name. This allows compatibility to be increased.

The returned AST object is wrapped in a Proxy instance allowing access to schemata instance properties, a string and schemata property accessor that provide access to a String representation of the SDL and the instance of Schemata that represents the templated string

The Proxy instance allows direct access to any Schemata instance property that does not directly conflict with a AST node property of the same name. If the AST node property conflicts, access will be limited to ast.schemata.property where ast is the result of calling gql and property is the desired conflicting property.

New Changes

You can now require files with .graphql, .gql, and .sdl extensions. If found, the contents of the file will be used as parameters for a call to new Schemata(). Various obvious aspects of the file can be imported specifically as follows

  astNode   - an ASTNode document object representing the SDL contents
              of the .graphql file contents. Null if the text is invalid
  resovlers - if there is an adjacent file with the same name, ending in
              .js and it exports either a `resolvers` key or an object by
              default, this value will be set on the sdl object as its set
              resolvers/rootObj
  schema    - a GraphQLSchema instance object if the contents of the .graphql
              file represent both valid SDL and contain at least one root
              type such as Query, Mutation or Subscription
  sdl       - the string of SDL wrapped in an instance of Schemata
  typeDefs  - the raw string used that `sdl` wraps
  default   - the sdl string wrapped in an instance of Schemata is the
              default export

Notably, in 3.x and onward, if one of these extensions is require'd, and adjacent to the file is a .js file with the same name, its contents (or .resolvers if present) will be used as the resolvers for the generated Schemata instance.

Installation

Simply npm or yarn install the package

/Volumes/harddrive/path/to/your/project
$ npm install --save ne-schemata

Usage

Using new

Creating instances of Schemata can of course be done with typical JavaScript via the new keyword.

let schemata = new Schemata(
  'type Person { name: String } type Query { peep: Person }'
)

Using Schemata.from

Creating instances of Schemata can also be done with a constant on the class itself called .from().

let schemata = Schemata.from(
  'type Person { name: String } type Query { peep: Person }'
)

From an existing instance of Schemata

Creating instances of Schemata from other instances of Schemata is also supported. In these cases, all the set values of note will be moved over. This includes schema and resolvers set on the original instance.

let sdlA = Schemata.from(
  'type Person { name: String } type Query { peep: Person }',
  {
    Query: {
      peep() {
        return { name: 'Brielle' }
      },
    },
  }
)
let sdlB = Schemata.from(sdlA)

console.log(sdlB.resolvers) // { Query: { peep: [function peep()] } }

From an instance of GraphQLSchema

You can also create a Schemata instance from an existing instance of GraphQLSchema. The typeDefs or SDL string will be generated using printSchema() from the base graphql module. The .schema value will be set explicitly to the value supplied in the case it is executable or has had other modifications performed on it.

import { makeRemoteExecutableSchema, introspectSchema } from 'graphql-tools';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';

let link = new HttpLink({ uri: 'some uri', fetch })
let schema = makeRemoteExecutableSchema(await introspectSchema(link), link)
let schemata = new Schemata(schema) // or Schemata.from(schema)

console.log(schemata) // the SDL from the remote schema

Using the gql template function

Alternatively, creating a Schemata string instance can be done using the gql template function. The function was named as many popular editors have a syntax highlighting theme that highlights GraphQL SDL. It seemed to make a lot of sense to add the resulting string from this as an instance of Schemata

import { gql } from 'ne-schemata'

let schemata = gql`
  type Person {
    name: String
  }

  type Query {
    peep: Person
  }
`

let schema = schemata.schema

SDL or typeDefs with extend type

SDL or typeDefs with extend type in them will honored and types will be extended as directed by the SDL. Calling .sdl or .typeDefs will contain the extend type SDL. .schema will return a GraphQLSchema object with the types properly extended however. To modify and update the internal SDL, call .flattenSDL(). This will regenerate the SDL on the instance. If, instead, you simply wish to see what the flattened SDL string looks like without modifying the instance, this can be found by accessing the .flatSDL property.

// Create instance with extend type in SDL
let schemata = gql`type User { name: String } extend type User { age: Int }`

console.log(schemata)
// Prints
// type User { name: String } extend type User { age: Int }

console.log(schemata.flatSDL)
// Prints
// type User {
//   name: String
//   age: Int
// }

schemata.flattenSDL()
console.log(schemata)
// Prints
// type User {
//   name: String
//   age: Int
// }

Using require('file.graphql') with require extensions

The final way to create new instances of Schemata in a helpful and unobtrusive manner is to use the '.graphql' extension handler. This handler registers the '.graphql' extension with the require() function in your local nodeJS environment.

Once registered, require() and import {} from will now do several interesting things with '.graphql' imported text. First the resulting exported object has 5 different keys, with the default being a Schemata string wrapping the contents of the file itself.

The five exports of imported '.graphql' files are:

  • astNode an ASTNode object representing the parsed contents of the file
  • resolvers if there is an adjacent .js file of the same name, and if that file exports 'resolvers' or if the exported results are an object and not a function, then that object is considered to be a resolvers map for the associated .graphql file.
  • schema generated from the contents of the Schemata string. If the resolvers object was successfully generated and imported, this will be an executableSchema instead.
  • sdl the Schemata string instance, also the default export
  • typeDefs keeping in line and adding compatibility with Apollo Server, the typeDefs export is simply a synonym for sdl.

How do you use the extension? Somewhere, typically early on in the project entry script, make a call to register() and then subsequently import/require your '.graphql' files.

require('ne-schemata/register')

import { sdl, schema, resolvers } from './graphql/Person.graphql'

// or alternatively
require('ne-schemata').register('graphql') // or simply register() as .graphql is the default

import { sdl, schema, resolvers } from './graphql/Person.graphql'

Schemata Merging (both strings and GraphQLSchema objects)

AST merging of multiple SDL files and resolver maps. This will allow you to do something like the following. Merging schemas is just as easy. A new instance of GraphQLSchema will be created and stored on the instance but the pre-existing resolvers will be deeply merged with the supplied resolvers on the schema to be merged and all will be applied and bound to the newly generated schema.

import graphqlHTTP from 'express-graphql'
import Express from 'express'
import { gql } from 'ne-schemata'

const app = Express()
const sdlA = gql`
  type Person {
    name: String
  }

  type Query {
    peep: Person
  }
`
const sdlB = gql`
  type Person {
    age: Int
  }

  type Query {
    peeps: [Person]
  }
`
const sdlC = sdlA.mergeSDL(sdlB)

/*
sdlC.schema would evaluate to something like the following:

type Person {
  name: String
  age: Int
}

type Query {
  peep: Person
  peeps: [Person]
}
*/

app.use(
  '/graphql',
  graphqlHTTP({
    schema: sdlC.schema,
    graphiql: true,
  })
)

app.listen(4000)

Merging Concerns

TLDR; Schemata has your back and you don't really need to read this

Still here? Cool, let's learn what happens during merging. When merging multiple schemas, one of the problems that can occur, especially if one of the schemas being merged is a remote executable schema, is that the resolvers might be bound to an older version of a schema that is only a subset of the new merge.

Schemata helps to alleviate this problem by introducing a few interfaces of note. The immediate solution to this problem is to wrap the resolvers and have those resolvers point to a newly merged GraphQLSchema object rather than the one it was originally bound to.

The problem comes in when you try to merge more than once. Wrapping a previously wrapped version of a resolver will allow you to set it at the top, but the previous attempt to do will execute and override the changes you just made.

Schemata solves this problem by storing the older unbound methods with each bind. A reference to the schema, sdl and resolvers at the time of the merge are kept. When each subsequent merge occurs, these older unwrapped/unbound resolvers are used to apply the new schema rather than layering things like Russian nesting dolls.

Furthermore, when making a call to .mergeSchema() you may add your own resolver injectors which are simply objects containing a property called '.resolverInjectors' which are an array of ResolverArgsTransformer functions. These give you full acceess to the source, args, context and info parameters to modify to your hearts content before the original unwrapped resolver receives those parameters.

import { Schemata } from 'ne-schemata'

let a = Schemata.from('type Query { me: String }', {
  me(s, a, c, i) {
    return 'Brielle'
  },
})

let b = Schemata.from('type Query { her: String }', {
  her(s, a, c, i) {
    return 'Stacy'
  },
})

let ab = a.mergeSchema(b)

console.log(ab.prevMergeResolvers)
// [{schema:..., sdl:..., resolvers:...}, ...] <-- unwrapped
console.log(ab.resolvers)
// { me(), her() }     <-- wrapped to new schema

let c = Schemata.from('type Polygon { sides: Int }', {
  Polygon: {
    sides(s, a, c, i) {
      return 6
    },
  },
})

let abc = ab.mergeSchema(c)
console.log(abc.prevMergeResolvers)
// [{schema:..., sdl:..., resolvers:...}, ...] <-- unwrapped
console.log(abc.resolvers)
// { me(), her(), Polygon: { sides() } } <-- wrapped to new schema

Even this example has difficulty explaining properly what is happening and why it's important, but as each merge happens the previous unwrapped resolvers are passed along and newly introduced schemas' resolvers are added to this list.

Schemata Class Properties and Methods

Constructor

constructor(
  typeDefs: SchemaSource,
  resolvers: ResolverMap = null,
  buildResolvers: boolean | string = false,
  flattenResolvers: boolean = false
): Schemata

Schemata instances are versatile and can be created through a variety of sources. Typically anything that can be converted to a string of SDL can be used to create a new Schemata instance. Everything from a GraphQL Source instance, previously instantiated Schemata instance, a GraphQLSchema object or even an ASTNode. Of course, basic strings of SDL can also be used.

An object with resolver functions can be supplied as the second parameter allowing executable schemas to be generated from the source SDL.

The final two parameters are for the case where the Schemata instance is initialized with a GraphQLSchema instance or with an older instance of Schemata.

  • The first causes a programmatic attempt to generate and set an object containing resolvers from the executable schema represented by either eligible object. If the string 'all' is supplied than a resolver for each field not defined will receive GraphQL's defaultFieldResolver as its resolver.
  • The second causes the generated resolver map to be in the flattened style with root fields exposed at the root of the resolver map. By default the Apollo style resolver map with fields under their respective type names is the default

Instance properties

.ast

getter generates ASTNodes that the internal string would generate when passed to require('graphql').parse()

.schema

getter retrieves any internally stored GraphQLSchema instance generated thus far; if one does not exist, one will be generated setter sets the value of the internal GraphQLSchema storage variable

.executableSchema

getter retrieves the internally stored executable schema, if one exists, or it creates one if the .resolvers value has been set and if there is valid .sdl. This getter is now deprecated. All functionality has been integrated in the single .schema property without any loss of functionality. Please use that instead.

.flatSDL

getter working from the built in .schema GraphQLSchema instance, SDL is regenerated and printed. Any extend type declarations applied to the schema instance will be merged and only the single type will be shown. This will return a string of SDL but the instance will not be modified in the process.

.graphiql

getter retrieves the .graphiql flag which defaults to true. This might be used by express-graphql. Schemata does not use this flag internally, but rather stores it as a convenience property.
setter sets the internal graphiql flag to a value other than true. Remember that this field is not used internally by Schemata other than storage and retrieval.

.hasAnExecutableSchema

getter A getter to determine if an executable schema is attached to this Schemata instance. It does so by walking the schema fields via buildResolvers() and reporting whether there is anything inside the results or not.

.hasFlattenedResolvers

getter Determines if the internal resolver map has at least one defined root type resolver (Query, Mutation, Subscription) or not. If not, including if there are no root resolvers (yet), this property will evaluate to false

.prevResolverMaps

When a Schemata instance is merged with another GraphQLSchema, its resolvers get stored before they are wrapped in a function that updates the schema object it receives. This allows them to be wrapped safely at a later date should this instance be merged with another.

getter retrieves any internally stored resolver maps setter sets the internally stored resolver maps

.schemaDirectives

Schemata is designed to be used with various GraphQL JavaScript engines. In many cases, it is simply used as a variable to store configuration for more specific calls to things like makeExecutableSchema from graphql-tools or GraphQLServer and the like. For more information on Apollo's usage of this property, see this page: https://www.apollographql.com/docs/graphql-tools/schema-directives.html

getter An object containing the name to function mapping for schema directive classes when used with apollo-server or graphql-yoga.

setter Internally sets the instance variable for the schemaDirectives object used with apollo-server or graphql-yoga

.sdl

getter the same value as the string this instance represents

.types

getter calculates a JavaScript object based on the schema where type names are sub-objects, and their keys are field names with values containing objects with two keys. Those keys are type and args. The type field will evaluate to the SDL type of the field in question and the args field will evaluate to an order specific set of objects with an argument name key and SDL argument type value.

For clarity, given a schema such as the following:

type A {
  a: String
  b: [String]
  c: [String]!
}
type Query {
  As(name: String): [A]
}

One can expect a return type like this:

{
  Query: {
    As: { type: '[A]', args: [ { name: 'String' } ] }
  },
  A: {
    a: { type: 'String', args: [] },
    b: { type: '[String]', args: [] },
    c: { type: '[String]!', args: [] }
  }
}

.typeDefs

getter a synonym for .schemata, ideal for use with Apollo and other librariees that expect this nomenclature

.rootValue

getter a synonym for .resolvers, ideal for use with express-graphql or other libraries that expect this nomenclature. Note: that the format of this object is slightly different for Apollo than the Facebook reference implementation

.resolvers

getter retrieves any internally store resolvers map setter sets the internally stored resolvers map

.validSDL

getter if the SDL this string represents can be parsed without error, true will be returned; false otherwise

.validSchema

getter if the SDL this string represents can be used to build a schema without error, true will be returned; false otherwise

.valid

getter returns true if both .validSDL and .validSchema are both true

Instance methods

astFieldByName

astFieldByName(type: string, field: string): FieldNode

For SDL that doesn't properly build into a GraphQLSchema, it can still be searched for a type and field.

astTypeByName

astTypeByName(type: string): ASTNode

For SDL that doesn't properly build into a GraphQLSchema, it can still be parsed and searched for a type by name.

buildResolvers

buildResolvers(
  flattenRootResolversOrFirstParam: boolean | ResolverMap,
  ...extendWith: Array<ResolverMap>
): ResolverMap

In the case where Schemata instance was created with a GraphQLSchema, especially one that is an executableSchema or somehow has resolvers in it already, buildResolvers() will walk the schema fields and build up a resolvers object of those resolve() functions on each field. If true is supplied as the first parameter, the resolver functions from Query, Mutation and Subscription will appear in the root of the returned object rather than under their aforementioned parent types.

buildResolverForEachField

buildResolverForEachField(
  flattenRootResolversOrFirstParam: boolean | ResolverMap,
  ...extendWith: Array<ResolverMap>
): ResolverMap

From time to time it makes more sense to wrap every possible resolver mapping in given schema. Getting a handle to each fields resolver and or substituting missing ones with GraphQL's defaultFieldResolver can be a tiresome affair. This method walks the schema for you and returns any previously defined resolvers alongside defaultFieldResolvers for each possible field of every type in the schema.

If a schema cannot be generated from the SDL represented by the instance of Schemata, then an error is thrown.

clearResolvers

clearResolvers()

Removes any existing .resolvers map that might have been assigned to the object instance. This is identical to calling .resolvers = null, and only exists here as a semantic method that may have future functionality added to it.

clearResolvers

clearSchema()

Removes any existing .schema GraphQLSchema instance that might have been assigned to the object instance. This is identical to calling .schema = null, and only exists here as a semantic method that may have future functionality added to it.

flattenSDL

flattenSDL()

Working from the built in .schema GraphQLSchema instance, SDL is regenerated and printed. Any extend type declarations applied to the schema instance will be merged and only the single type will be shown. The internal typeDefs will be modified after this call and subsequent calls to .ast, .schema, .sdl, or .typeDefs will no longer have the extend type directives listed.

forEachOf

forEachOf(
  fn: ForEachOfResolver,
  context: unknown,
  types: number = Schemata.TYPES,
  suppliedSchema: ?GraphQLSchema = null
): GraphQLSchema

Iterates over the type map of either the internal schema or one created from the SDL string this instance represents and then stored for later use. See above for the definition of the ForEachOfResolver callback signature. types is a bitmask made up of at least one of the constant properties on Schemata such as Schemata.ALL or Schemata.TYPES and optionally augmented with Schemata.HIDDEN

See above for the definition of the ForEachOfResolver callback signature

forEachType

forEachType(
  fn: ForEachOfResolver,
  context: unknown,
  suppliedSchema: ?GraphQLSchema
): GraphQLSchema

Iterates over all types on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachInputObjectType

forEachInputObjectType(
  fn: ForEachOfResolver,
  context: unknown,
  suppliedSchema: ?GraphQLSchema
): GraphQLSchema

Iterates over all input types on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachUnion

forEachUnion(
  (fn: ForEachOfResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema)
)

Iterates over all the unions on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachEnum

forEachEnum(
  (fn: ForEachOfResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema)
)

Iterates over all enums on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachInterface

forEachInterface(
  (fn: ForEachOfResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema)
)

Iterates over all interfaces on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachScalar

forEachScalar(
  (fn: ForEachOfResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema)
)

Iterates over all scalars on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachRootType

forEachRootType(
  (fn: ForEachOfResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema)
)

Iterates over the three root types (Query, Mutation and Subscription) on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachOfResolver callback signature

forEachField

forEachField(
  (fn: ForEachFieldResolver),
  (context: unknown),
  (types: number = ALL),
  (suppliedSchema: ?GraphQLSchema = null)
)

Iterates over all fields on all types on either the internal schema or one created from the Schemata string this instance represents and then stored for later use. See above for the definition of the ForEachFieldResolver callback signature. types is a bitmask made up of at least one of the constant properties on Schemata such as Schemata.ALL or Schemata.TYPES and optionally augmented with Schemata.HIDDEN

See above for the definition of the ForEachFieldResolver callback signature

forEachTypeField

forEachTypeField(
  (fn: ForEachFieldResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema = null)
)

Iterates over all type fields on all types on either the internal schema or one created from the SDL string this instance represents and then stored for later use.

See above for the definition of the ForEachFieldResolver callback signature

forEachInterfaceField

forEachInterfaceField(
  (fn: ForEachFieldResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema = null)
)

Iterates over all interface fields on all types on either the internal schema or one created from the SDL string this instance represents and then stored for later use

See above for the definition of the ForEachFieldResolver callback signature

forEachInputObjectField

forEachInputObjectField(
  (fn: ForEachFieldResolver),
  (context: unknown),
  (suppliedSchema: ?GraphQLSchema = null)
)

Iterates over all input object fields on all types on either the internal schema or one created from the SDL string this instance represents and then stored for later use.

See above for the definition of the ForEachFieldResolver callback signature

merge

merge(
  schema: SchemaSource,
  config?: MergeOptionsConfig = DefaultMergeOptions
): Schemata

A new Schemata object instance with merged schema definitions as its contents as well as merged resolvers and newly bound executable schema are all created in this step and passed back. The object instance itself is not modified

Post merge, the previously stored and merged resolvers map are are applied and a new executable schema is built from the ashes of the old.

mergeSDL

mergeSDL(
  schemaLanguage: string | Schemata | Source | GraphQLSchema,
  conflictResolvers: ?ConflictResolvers = DefaultConflictResolvers
): Schemata

Given a string of Schema Definition Language (or SDL) or a Schemata instance itself or any of the other initialization sources, the function will proceed to blend the derived SDL with the one of the instance. In the case of conflicts, either the default resolver, which simply takes the newer right hand value, or a function of your own devising will be called to decide. See the ConflictResolvers type for more info

pareSDL

pareSDL(
  schemaLanguage: string | Schemata | Source | GraphQLSchema,
  resolverMap: ?ResolverMap = null
): Schemata

Given a string of Schema Definition Language (or SDL) or a Schemata instance itself or any of the other initialization sources, the function will proceed to remove the items using the derived SDL as a guide. If the internal instance has resolvers set or one can be built from the Schema stored on the instance, or if a set is supplied as the second parameter, the resolvers will also be pared down for any removed types and fields. Types stripped of all fields will themselves be removed.

mergeSchema

mergeSchema(
  schema: GraphQLSchema | Schemata,
  config?: MergeOptionsConfig = DefaultMergeOptions
): Schemata

Shortcut for the merge() function; mergeSDL still exists as an entity of itself, but merge() will invoke that function as needed to do its job and if there aren't any resolvers to consider, the functions act identically.

run

run (
  query: string | Source,
  contextValue?: unknown,
  variableValues?: ?ObjMap<mixed>,
  rootValue?: unknown,
  operationName?: ?string,
  fieldResolver?: ?GraphQLFieldResolver<any,any>
)

A convenient pass-thru to graphqlSync() to query the schema in a synchronous manner. The schema used is built from the contents of the Schemata string itself. If there is a .resolvers map set on the instance, it will be passed for the root object/value parameter.

runAsync

async runAsync(
  query: string | Source,
  contextValue?: unknown,
  variableValues?: ?ObjMap<mixed>,
  rootValue?: unknown,
  operationName?: ?string,
  fieldResolver?: ?GraphQLFieldResolver<any,any>
)

A convenient pass-thru to graphql() to query the schema in an asynchronous manner. The schema used is built from the contents of the Schemata string itself. If there is a .resolvers map set on the instance, it will be passed for the root object/value parameter.

schemaFieldByName

schemaFieldByName(type: string, field: string): FieldNode

Builds a schema based on the SDL in the instance and then parses it to fetch a named field in a named type. If either the type or field are missing or if the SDL cannot be built as a schema, null is returned.

schemaResolverFor

schemaResolverFor(type: string, field: string): ?Function

A method to fetch a particular field resolver from the schema represented by this Schemata instance.

Static properties

.gql

getter a reference to the results of require('graphql')

.ALL

getter used with forEachOf/forEachField() methods to denote that all GraphQLTypes should be iterated over. Can be combined with bit-wise OR'ing.

.TYPES

getter used with forEachOf/forEachField() methods to denote that all types should be iterated over. Can be combined with bit-wise OR'ing.

.INTERFACES

getter used with forEachOf/forEachField() methods to denote that all interfaces should be iterated over. Can be combined with bit-wise OR'ing.

.ENUMS

getter used with forEachOf/forEachField() methods to denote that all enums should be iterated over. Can be combined with bit-wise OR'ing.

.UNIONS

getter used with forEachOf/forEachField() methods to denote that all unions should be iterated over. Can be combined with bit-wise OR'ing.

.SCALARS

getter used with forEachOf/forEachField() methods to denote that all scalars should be iterated over. Can be combined with bit-wise OR'ing.

.ROOT_TYPES

getter used with forEachOf/forEachField() methods to denote that all root types (Query, Mutation, Subscription) should be iterated over. Can be combined with bit-wise OR'ing.

.INPUT_TYPES

getter used with forEachOf/forEachField() methods to denote that all input object types should be iterated over. Can be combined with bit-wise OR'ing.

.HIDDEN

getter used with forEachOf/forEachField(). When iterating the internal meta types that start with underscores are not iterated over. By combining this flag via bit-wise OR'ing, the internal meta types will be included

Symbols

.iterator

get [Symbol.iterator](): Function

Redefine the iterator for Schemata instances so that they simply iterate over the contents of the SDL/typeDefs.

.species

get [Symbol.species](): Function

Symbol.species ensures that any String methods used on this instance will result in a Schemata instance rather than a String. NOTE: this does not work as expected in current versions of node. This bit of code here is basically a bit of future proofing for when Symbol.species starts working with String extended classes

.toStringTag

get [Symbol.toStringTag](): Function

Ensures that instances of Schemata report internally as Schemata object. Specifically using things like Object.prototype.toString.

Static methods

buildFromDir

static async buildFromDir(
  path: string
): Promise<Schemata>

Finally a way to recursively look through a directory for any .graphql, .gql, or any .sdl files and use their contents for a merged typeDefs. It also will build up an object of resolvers to be set on the final instance of Schemata.

Loading the schema for the site has now become as easy as

let siteSchemata = await Schemata.buildFromDir('/path/to/src/with/.graphql/files')

buildSchema

static buildSchema(
  sdl: string | Source | Schemata | GraphQLSchema,
  showError: boolean = false,
  schemaOpts: BuildSchemaOptions & ParseOptions = undefined
): ?GraphQLSchema

Using the Facebook reference implementation's call to buildSchema() is made. If an error is thrown and showError is not true, the exception will be swallowed and null will be returned instead. schemaOpts is the optional second parameter taken by require('graphql').buildSchema()

parse

static parse(
  sdl: string | Schemata | Source | GraphQLSchema,
  showError: boolean = false
): ?ASTNode

Using the Facebook reference implementation's call to parse() is made. The resulting AstNode objects will be returned. If the sdl is not valid, an error will be thrown if showError is truthy

print

static print(
  ast: ASTNode,
  showError: boolean = false
): ?Schemata

A little wrapper used to catch any errors thrown when printing an ASTNode to string form using require('graphql').print(). If showError is true any thrown errors will be rethrown, otherwise null is returned instead. Should all go as planned, an instance of Schemata wrapped with the printed SDL will be returned.

Since version 1.7

from

static from(
  typeDefs: string
    | Source
    | Schemata
    | GraphQLSchema
    | ASTNode,
  resolvers: ?ResolverMap = null,
  buildResolvers: boolean | string = false,
  flattenResolvers: boolean = false,
): Schemata

An alterate way of creating a new instance of Schemata. Effectively equivalent to new Schemata(...)

Exported Types

fromContentsOf

static async fromContentsOf(
  path: string
): Promise<Schemata>

Sometimes you just want to grab the contents of the SDL from disk and pass it to Schemata.from()

ConflictResolvers

export type ConflictResolvers = {
  /** A handler for resolving fields in matching types */
  fieldMergeResolver?: FieldMergeResolver,

  /** A handler for resolving directives in matching types */
  directiveMergeResolver?: DirectiveMergeResolver,

  /** A handler for resolving conflicting enum values */
  enumValueMergeResolver?: EnumMergeResolver,

  /** A handler for resolving type values in unions */
  typeValueMergeResolver?: UnionMergeResolver,

  /** A handler for resolving scalar config conflicts in custom scalars */
  scalarMergeResolver?: ScalarMergeResolver,
}

The ConflictResolvers is simply an object that defines one or more of the handler functions mentioned above. FieldMergeResolvers are the most common types, however the others can be useful in schema and Schemata merging as well. If no resolvers are supplied, the default ones are used. These simply overrite the lefthand (instance) value with the righthand (supplied) value

AsyncEntryInspector

type AsyncEntryInspector = (
  key: string,
  value: Function,
  path: Array<string>,
  map: ResolverMap
) => ?Promise<{ [string]: Function }>

An AsyncEntryInspector is a function passed to asyncWalkResolverMap that is invoked for each encountered pair along the way as it traverses the ResolverMap in question. The default behavior is to simply return the supplied entry back.

If undefined is returned instead of an object with a string mapping to a Function, then that property will not be included in the final results of asyncWalkResolverMap.

EntryInspector

type EntryInspector = (
  key: string,
  value: Function,
  path: Array<string>,
  map: ResolverMap
) => { [string]: Function }

An EntryInspector is a function passed to walkResolverMap that is invoked for each encountered pair along the way as it traverses the ResolverMap in question. The default behavior is to simply return the supplied entry back.

If undefined is returned instead of an object with a string mapping to a Function, then that property will not be included in the final results of walkResolverMap.

DirectiveMergeResolver

export type DirectiveMergeResolver = (
  leftType: ASTNode,
  leftDirective: DirectiveNode,
  rightType: ASTNode,
  rightDirective: DirectiveNode
) => DirectiveNode

The DirectiveMergeResolver is a function that takes both left and right types as well as left and right directives. The function decides which directive to return and does so.

EnumMergeResolver

export type EnumMergeResolver = (
  leftType: ASTNode,
  leftValue: EnumValueNode,
  rightType: ASTNode,
  rightValue: EnumValueNode
) => EnumValueNode

The EnumMergeResolver is a function that takes both left and right types as well as left and right enum values. The function decides which enum value to return and does so.

FieldMergeResolver

export type FieldMergeResolver = (
  leftType: ASTNode,
  leftField: FieldNode,
  rightType: ASTNode,
  rightField: FieldNode
) => FieldNode

The FieldMergeResolver is a function that takes both left and right types as well as left and right field values. The function decides which field value to return and does so.

ForEachFieldResolver

export type ForEachFieldResolver = (
  type: unknown,
  typeName: string,
  typeDirectives: Array<GraphQLDirective>,
  field: unknown,
  fieldName: string,
  fieldArgs: Array<GraphQLArgument>,
  fieldDirectives: Array<GraphQLDirective>,
  schema: GraphQLSchema,
  context: unknown
) => void

The ForEachFieldResolver function definition is about a function that takes a callback that receives the type object, the type object name, an array of directives if any, the field object, the field object name, any field arguments, any field directives, the schema and any supplied context to use in the iteration and looping over the types defined in the Schematas schema representation. If an internal store of a schema is not availabe, one is created

ForEachOfResolver

export type ForEachOfResolver = (
  type: unknown,
  typeName: string,
  typeDirectives: Array<GraphQLDirective>,
  schema: GraphQLSchema,
  context: unknown
) => void

The ForEachOfResolver function definition is about a function that takes a callback that receives the type object, the type object name, an array of directives if any, the schema and any supplied context to use in the iteration and looping over the types defined in the Schematas schema representation. If an internal store of a schema is not availabe, one is created

ResolverArgs

export type ResolverArgs = {
  source: unknown,
  args: unknown,
  context: unknown,
  info: GraphQLResolveInfo,
}

Resolver functions receive four arguments when they execute. This type represents and object with the same four argument types within. Used by ResolverArgsTransformer and, indirectly by, MergeOptionsConfig objects.

ResolverArgsTransformer

export type ResolverArgsTransformer = (args: ResolverArgs) => ResolverArgs

A function that takes a ResolverArgs object and returns a ResolverArgs

ResolverMap

export type ResolverMap = { [string]: Function | ResolverMap }

A resolver map is either a flat string/Function mapping or nested version of itself. Each string key should either point to a object map of string/Function pairs or Function.

SchemaSource

type SchemaSource = string | Source | Schemata | GraphQLSchema | ASTNodes

A flow type that represents the various types of inputs that can often be used to construct an instance of Schemata.

MergeOptionsConfig

export type MergeOptionsConfig = {
  resolverInjectors: ResolverArgsTransformer | Array<ResolverArgsTransformer>,
}

A MergeOptionsConfig is a way to configure the arguments that are bound to each resolver during a call to mergeSchema. The functions set as resolverInjectors can be either a single function or an array of functions.

UnionMergeResolver

export type UnionMergeResolver = (
  leftType: ASTNode,
  leftUnion: NamedTypeNode,
  rightType: ASTNode,
  rightUnion: NamedTypeNode
) => NamedTypeNode

The UnionMergeResolver is a function that takes both left and right types as well as left and right union types. The function decides which union type to return and does so.

ScalarMergeResolver

export type ScalarMergeResolver = (
  leftScalar: ScalarTypeDefinitionNode,
  leftConfig: GraphQLScalarTypeConfig,
  rightScalar: ScalarTypeDefinitionNode,
  rightConfig: GraphQLScalarTypeConfig
) => GraphQLScalarTypeConfig

The ScalarMergeResolver is a function that takes both left and right scalars that are colliding and their custom resolver configs if available. Scalars definitions are a not always available, but if they are they will be provided. Therefore the default resolver for custom scalars will take whichever GraphQLScalarTypeConfig object is availabe starting with right, moving to left and then null.

External Functions

isRootType()

const isRootType = t => boolean

Given a AST type node from parsed SDL, return true if the type represents a root type; i.e. Query, Mutation or Subscription

normalizeSource()

export function normalizeSource(
  typeDefs: string | Source | Schemata | GraphQLSchema | ASTNode,
  wrap: boolean = false
): (string | Schemata)

A function that takes the various types of input that Schemata takes as a constructor and converts the results into either a string or Schemata instance if wrap is set to true.

runInjectors()

export function runInjectors(
  config: MergeOptionsConfig,
  resolverArgs: ResolverArgs
): ResolverArgs

Given an initial set of arguments passed to a resolver function and a MergeOptionsConfig setup, it generates a new set of ResolverArgs that, post-modification, can finally be passed to the resolver when wrapped.

SchemaInjectorConfig()

export function SchemaInjectorConfig(
  schema: GraphQLSchema,
  extraConfig?: MergeOptionsConfig
): MergeOptionsConfig

The merge options config takes the arguments passed into a given resolve() function, allowing the implementor to modify the values before passing them back out.

This function takes a schema to inject into the info object, or fourth parameter, passed to any resolver. Any extraConfig object added in will have its resolverInjectors added to the list to be processed.

stripResolversFromSchema()

export function stripResolversFromSchema(
  schema: GraphQLSchema
): ?ResolverMap

Walk the supplied GraphQLSchema instance and retrieve the resolvers stored on it. These values are then returned with a [typeName][fieldName] pathing

Default Functions

DefaultDirectiveMergeResolver()

function DefaultDirectiveMergeResolver(
  leftType: ASTNode,
  leftDirective: DirectiveNode,
  rightType: ASTNode,
  rightDirective: DirectiveNode
): DirectiveNode

The default directive resolver blindly takes returns the right field. This resolver is used when one is not specified.

DefaultAsyncEntryInspector()

const DefaultAsyncEntryInspector: AsyncEntryInspector = (key, value, path, map) => ?Promise<{
  [string]: Function
}>

A default implementation of the EntryInspector type for use as a default to asyncWalkResolverMap. While not immediately useful, a default implementation causes asyncWalkResolverMap to wrap any non-function and non-object values with a function that returns the non-compliant value and therefore has some intrinsic value.

DefaultEntryInspector()

const DefaultEntryInspector: EntryInspector = (key, value, path, map) => ?{
  [string]: Function
}

A default implementation of the EntryInspector type for use as a default to walkResolverMap. While not immediately useful, a default implementation causes walkResolverMap to wrap any non-function and non-object values with a function that returns the non-compliant value and therefore has some intrinsic value.

DefaultEnumMergeResolver()

function DefaultEnumMergeResolver(
  leftType: ASTNode,
  leftValue: EnumValueNode,
  rightType: ASTNode,
  rightValue: EnumValueNode
): EnumValueNode

The default field resolver blindly takes returns the right field. This resolver is used when one is not specified.

DefaultFieldMergeResolver()

function DefaultFieldMergeResolver(
  leftType: ASTNode,
  leftField: FieldNode,
  rightType: ASTNode,
  rightField: FieldNode
): FieldNode

The default field resolver blindly takes returns the right field. This resolver is used when one is not specified.

DefaultScalarMergeResolver()

function DefaultScalarMergeResolver(
  leftScalar: ScalarTypeDefinitionNode,
  leftConfig: GraphQLScalarTypeConfig,
  rightScalar: ScalarTypeDefinitionNode,
  rightConfig: GraphQLScalarTypeConfig
): GraphQLScalarTypeConfig

The default scalar merge resolver returns the right config when there is one, otherwise the left one or null will be the default result. This is slightly different behavior since resolvers for scalars are not always available.

DefaultUnionMergeResolver()

function DefaultUnionMergeResolver(
  leftType: ASTNode,
  leftUnion: NamedTypeNode,
  rightType: ASTNode,
  rightUnion: NamedTypeNode
): NamedTypeNode

The default union resolver blindly takes returns the right type. This resolver is used when one is not specified.

Additional Goodies

at()

function at(
  object: Object,
  path: string | Array<string>,
  setTo?: unknown,
  playNice?: boolean = false
): unknown

This function takes an array of values that are used with eval to dynamically, and programmatically, access the value of an object in a nested fashion. It can take either a string with values separated by periods (including array indices as numbers) or an array equivalent were .split('.') to have been called on said string.

Examples:

  // Calling `at` with either set of arguments below results in the same
  // values.
  let object = { cats: [{ name: 'Sally' }, { name: 'Rose' }] }
  at(object, 'cats.1.name') => Rose
  at(object, ['cats', 1, 'name']) => Rose
  // Values can be altered using the same notation
  at(object, 'cats.1.name', 'Brie') => Brie
  // Values cannot normally be accessed beyond existence. The following
  // will throw an error. A message to console.error will be written showing
  // the attempted path before the error is again rethrown
  at(object, 'I.do.not.exist') => ERROR
  // However, if you want the function to play nice, `undefined` can be
  // returned instead of throwing an error if true is specified as the
  // fourth parameter
  at(object, 'I.do.not.exist', undefined, true) => undefined

atNicely()

function atNicely(
  object: Object,
  path: string | Array<string>,
  setTo?: unknown
): unknown

atNicely() is a shorthand version of calling at() but specifying true for the argument playNice. This can make reads normally performed with calls to at() where you want to prevent errors from being thrown with invalid paths

gql()

function gql(template, ...substitutions)

The gql template tag function takes the contents of a given template string and returns a Schemata instance using the string as a parameter to the constructor

// i.e.
let sdl = gql`
  type Person {
    name: String
  }
`
console.log(sdl instanceof Schemata) // true

register()

function register(extension = '.graphql')

This function takes an optional string denoting the extension string in which you store your SDL typeDefs in your project. Usually these are .graphql files. Regardless, once this function is invoked, you may then on in your project call require('/path/to/my/typeDef.graphql') and the resulting value would be an object with the following keys and values:

Named Exports

  • astNode the AST document for the contents of the typeDefs.graphql file
  • resolvers if there is an adjacent file with a .js extension and the same name, an attempt to load its contents as a module will occur. If successful this object will be the results of that require()
  • schema an instance of GraphQLSchema based on the contents of the .graphql file
  • schemata a Schemata instance built around the SDL, and JS resolvers if any
  • typeDefs an alias for schemata

The default export is the same as the schemata export

walkResolverMap()

function walkResolverMap(
  object: ResolverMap,
  inspector: EntryInspector = DefaultEntryInspector,
  wrap: boolean = true,
  path: Array<string> = []
): ResolverMap

Given a ResolverMap object, walk its properties and allow execution with each key, value pair. If the supplied function for handling a given entry returns null instead of an object with the format {key: value} then that entry will not be included in the final output.

Paths here are somewhat interesting and bear a moments discussion. A path of ['job', 'responsibilities'] would indicate that the entry in question is located at object.job.responsibilities[entry.key].

  • object an object containing string keys mapping to functions or objects nesting the same
  • inspector a function handler that takes a key, a value, a path which is an array of values from the top of the resolver map leading to the current entry and a reference to the resolver map itself.
  • wrap a boolean object, which defaults to true, and denotes that should a non-function, non-object, value be located then it should be wrapped in a function that returns that value instead
  • path an array of path strings; predominantly for re-entrant internal usage. See above

asyncWalkResolverMap()

function asyncWalkResolverMap(
  object: ResolverMap,
  inspector: AsyncEntryInspector = DefaultAsyncEntryInspector,
  wrap: boolean = true,
  path: Array<string> = []
): ResolverMap

Given a ResolverMap object, walk its properties and allow execution with each key, value pair. If the supplied function for handling a given entry returns null instead of an object with the format {key: value} then that entry will not be included in the final output.

Paths here are somewhat interesting and bear a moments discussion. A path of ['job', 'responsibilities'] would indicate that the entry in question is located at object.job.responsibilities[entry.key].

  • object an object containing string keys mapping to functions or objects nesting the same
  • inspector a function handler that takes a key, a value, a path which is an array of values from the top of the resolver map leading to the current entry and a reference to the resolver map itself.
  • wrap a boolean object, which defaults to true, and denotes that should a non-function, non-object, value be located then it should be wrapped in a function that returns that value instead
  • path an array of path strings; predominantly for re-entrant internal usage. See above

About

A utility that makes working with GraphQL Schema Definition Language far easier

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •