gqutils
v1.8.2
Published
Utilities For GraphQL
Downloads
68
Readme
gqutils
Utilities for GraphQL
Extra GraphQL Types
Int
Float
String
StringOrInt
StringOriginal
:String
is automatically trimmed of whitespaces. If you want an untrimmed string use this.Boolean
ID
IntID
: this can be used where input is either an integer or a numeric string. value is casted as an integer.Email
URL
DateTime
UUID
JSON
JSONObject
: A valid JSON object (arrays and other json values are invalid), most of the times you'd want to useJSONObject
instead ofJSON
Functions
makeSchemaFromModules(modules, opts)
Create a graphQL schema from various modules. If the module is a folder, it'll automatically require it.
const modules = [
'employee/Employee',
'Category',
];
const {schemas} = makeSchemaFromModules(modules, {
baseFolder: `${__dirname}/lib`,
schema: ['admin', 'public'],
allowUndefinedInResolve: false,
resolverValidationOptions: {},
});
// schemas will be {default: GraphqlSchema, admin: GraphqlSchema, public: GraphqlSchema}
This function returns {schemas, pubsub}
, you can ignore pubsub if you're not using graphql subscriptions.
Each module can either export {schema, resolvers}
or the {schema}
can contain resolvers in itself.
Concept of Schemas
makeSchemasFromModules
, returns multiple graphql schemas. You have to list all possible schema names in the schema
option. Each graphql schema will only contain the types/queries/mutations etc, that have listed that schema name in their schema
option.
To be included in a particular schema, the following must be true:
- The schema name is defined in the
schema
option - The schema name is also defined in the parent's
schema
option
eg. if a query returns a particular type, then it'll not be included in a schema the that type doesn't have the schema name in its schema
option. In short, it works like intersection of schema
options of parent and child.
In case of fields, args & values, if you haven't defined schema
option, it'll be included in all schemas. So, generally speaking in case of args & values, only define schema
when you want them to exclude from a particular schema and that schema is listed in its parent's schema
.
Regardless of the schema option, a default schema named default
contains all the types/fields.
Example Schema
const Employee = {
graphql: 'type',
fields: {
id: 'ID!',
smartprixId: 'ID',
name: 'String',
email: 'String',
phone: 'String',
createdAt: 'DateTime',
updatedAt: 'DateTime',
},
schema: ['admin', 'public'],
relayConnection: true,
};
const getEmployee = {
graphql: 'query',
name: 'employee',
type: 'Employee',
args: {
$default: ['id', 'email'],
},
schema: ['admin', 'public'],
};
const getEmployees = {
graphql: 'query',
name: 'employess',
type: 'EmployeeConnection',
args: {
$default: ['name', '$paging'],
},
schema: ['admin', 'public'],
};
const saveEmployee = {
graphql: 'mutation',
args: {
$default: ['id', 'name', 'email', 'phone'],
smartprixId: {
type: 'ID',
default: 0,
schema: ['admin'],
},
},
schema: ['admin'],
};
const deleteEmployee = {
graphql: 'mutation',
args: {
id: 'ID!',
},
schema: ['admin'],
};
const employeeAdded = {
graphql: 'subscription',
type: 'Employee',
};
const employeeChanged = {
graphql: 'subscription',
type: 'Employee',
args: {
'id': 'ID!',
},
};
const resolvers = {
Query: {
employee: getEmployee,
employees: getEmployees,
},
Mutation: {
saveEmployee,
deleteEmployee,
},
// You can also declare Subscription
// For Subscription Related Things
// Every resolver can contain {subscribe, filter, resolve}
// Only subscribe is required. Rest are optional.
// subscribe: return an async iterator that will contain data to be returned to the client
// filter: Filter events from pubsub async iterator
// resolve: Modify event data before sending to client
Subscription: {
employeeAdded: {
subscribe() {
return pubsub.asyncIterator('employeeAdded');
},
resolve(employee) {
if (employee.password) employee.password = '******';
return employee;
},
},
employeeChanged: {
subscribe() {
return pubsub.asyncIterator('employeeChanged');
},
filter(employee, args) {
return employee.id === args.id;
},
resolve(employee) {
if (employee.password) employee.password = '******';
return employee;
},
},
},
};
export {
schema: {
Employee,
getEmployee,
getEmployeees,
saveEmployee,
deleteEmployee,
employeeAdded,
employeeChanged,
},
resolvers,
};
makeSchemaFromDirectory(directory, opts = {})
Create a graphQL schema from a directory. It'll automatically require all the schemas & resolvers from inside the directory and create a schema using that.
It'll require all the files with format:
export {schema}
export schema from
module.exports = {schema}
exports.schema =
Object.defineProperty(exports, "schema",
Example
const {schemas} = makeSchemaFromDirectory(`${__dirname}/lib`, {
schema: ['admin', 'public'],
allowUndefinedInResolve: false,
resolverValidationOptions: {},
});
// schemas will be {default: GraphqlSchema, admin: GraphqlSchema, public: GraphqlSchema}
makeSchemaFromConfig(opts = {})
Create a graphQL schema from config defined in package.json
(gqutils
key), gqutils.js
or sm-config.js
(gqutils
key) in the root directory.
// in package.json
"gqutils": {
"schemaDirectory": "dist/lib",
"schema": ["admin", "public"],
"allowUndefinedInResolve": false,
};
const {schemas} = makeSchemaFromConfig();
// schemas will be {default: GraphqlSchema, admin: GraphqlSchema, public: GraphqlSchema}
Gql Class
The Gql class provides a way to execute the schema and to construct queries.
Executable Schemas
There are two ways you can use Gql to get an executable schema:
Config
const gql = Gql.fromConfig({
schema: ['admin', 'public'],
schemaName: 'admin',
...
});
Provide the same options you would provide to makeSchemaFromConfig under the config field in the constructor options.
Select the schema you want to execute against using the schemaName option (default is the default
schema)
Schemas
If you have multiple schemas and would like to have multiple Gql instances each executing different schemas then use it this way. It takes the output of one of the makeSchema
functions plus some options as input.
const output = makeSchemaFromConfig();
const adminGql = Gql.fromSchemas({
...output,
schemaName: 'admin',
});
const publicGql = Gql.fromSchemas({
...output,
schemaName: 'public',
})
Execute against API
If you would like to use Gql against a GraphQL API:
const apiGql = Gql.fromApi({
endpoint: 'https://example.com/api',
headers: {},
cookies: {},
});
Client Side Use
Browser:
If you are using Gql against Api from browser then use it like:
import {GqlApi} from 'gqutils';
// A default implementation of post request using `fetch`
import postRequest from 'gqutils/dist/postRequestBrowser'
GqlApi.postRequest = postRequest;
const apiGql = new GqlApi({
endpoint: 'https://example.com/api',
headers: {},
cookies: {},
});
Server Side Client:
If you want to use the client without adding graphql
and graphql-tools
peer dependencies then import GqlApi directly from it's file like:
import GqlApi from 'gqutils/dist/GqlApi';
// A default implementation of post request using `Connect` from `sm-utils`
import postRequest from 'gqutils/dist/postRequestNode'
GqlApi.postRequest = postRequest;
const apiGql = new GqlApi({
endpoint: 'https://example.com/api',
headers: {},
cookies: {},
});
Query Building And Execution
gql.tag
This is a Tag function used to build GraphQL Queries, it'll automatically convert args and arg objects. Some examples:
const query = gql.tag`
query {
employee(name: ${'admin'}) {
id
}
}`;
const args = {
name: 'admin',
};
const query = gql.tag`
query {
employee(${args}) {
id
}
}`;
These all give us the query:
query {
employee(name: "admin") {
id
}
}
gql.enum
As the tag
function adds "
to all string args, enums need to be handled separately.
const query = gql.tag`
query {
employee(type: ${gql.enum('MANAGER'))}) {
id
}
}`;
This gives us the query:
query {
employee(type: MANAGER) {
id
}
}
gql.fragment
Note: This function is only available to use with execuatble schemas and not with API
Use this function to add a fragment declared in the schema to the query. eg.:
/** schema.js */
const employeeFragment = {
graphql: 'fragment',
type: 'Employee',
fields: [
'id',
'name',
'email',
'phone',
'createdAt',
],
};
const query = gql.tag`
query {
employee(name: ${'admin'}) {
${gql.fragment('employeeFragment')}
}
}`;
This will give us the query:
query {
employee(name: "admin") {
...employeeFragment
}
fragment employeeFragment on Employee {
id
name
email
phone
createdAt
}
}
gql.exec
gql.getAll
is an alias
Execute a query. Let's consider the following query example:
const query = gql.tag`
query($name: String) {
employee(name: $name) {
id
name
email
phone
}
}`;
async function getEmployeeByName(name) {
const res = await gql.exec(query, {
variables: {name},
});
return res.employee;
}
gql.get
For the previous query there was only one field in the result. To simplify that use case we have the get function that automatically gets the nested field if only one field was queried.
It goes one level deep if the nested field is nodes
and is the only field.
const query = gql.tag`
query($name: String) {
employee(name: $name) {
id
name
email
phone
}
}`;
async function getEmployeeByName(name) {
return gql.get(query, {
variables: {name},
});
}
Extra Methods Executing Against Schema
These methods are specific to GqlSchema
class instances. GqlSchema
is the result of Gql.fromConfig
and Gql.fromSchemas
methods.
gql.parse & gql.execParsed
This allows us to pre parse and validate a query to generate a GraphQL DocumentNode
which can be cached.
This is only useful if your query is not dependent on any input that can't be moved to variables. This also allows you to validate the queries at build time or in your tests.
let _parsedQuery;
const getQuery = () => {
if (_parsedQuery) return _parsedQuery;
const query = gql.tag`
query($name: String) {
employee(name: $name) {
id
name
email
phone
}
}`;
// Validate's default value is the one provided in config/constructor
_parsedQuery = gql.parse(query, {validate: true});
return _parsedQuery;
};
const getResult = async () => gql.execParsed(
getQuery(),
{
context,
variables: {
name: 'abc',
}
});
A more complex example of a query with directives being used with variables to move dynamic logic to the query itself:
query($name: String, $getDetails: Boolean!) {
employee(name: $name) {
id
... EmployeeDetails @include(if: $getDetails)
}
}
fragment EmployeeDetails on Employee {
smartprixId
name
email
phone
createdAt
updatedAt
}
Language Reference
graphql option reference
type
: for object typeinput
: for input object typeunion
: for unioninterface
: for interfaceenum
: for enumscalar
: for scalarsquery
: for root querymutation
: for root mutationsubscription
: for root subscriptionfragment
: for declaring common fragments (To be used with Gql)
These are available in the type definitions, so can be imported as 'GQUtilsSchema' and type checked.
Types
Defined with graphql: type
const Employee = {
// graphql = type means it's a graphql type
graphql: 'type',
// name (optional): name of the type
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {Employee}}
name: 'Employee',
// description (optional): description that'll displayed in docs
description: 'An employee',
// interfaces (optional): interfaces this type implements
interfaces: ['Person'],
// relayConnection (optional, default=false): generate a relay connection type automatically
// if this is true, a connection type (EmployeeConnection here) will be added to the schema
// relayConnection can also be an object with fields {edgeFields, fields}
// edgeFields and fields will be merged with EmployeeEdge and EmployeeConnection respectively
// eg. relayConnection: {
// edgeFields: {title: 'String!'},
// fields: {timeTaken: 'Int!'}
// }
relayConnection: true,
// schema (required): schemas that this type is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// fields (required): fields of the type
// see Fields definition for more details
fields: {
id: 'ID!',
name: 'String',
},
}
Input Types
Defined with graphql: input
Its denition is mostly same as type.
const EmployeeInput = {
// graphql = input means it's a graphql input type
graphql: 'input',
// name (optional): name of the input type
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {EmployeeInput}}
name: 'EmployeeInput',
// description (optional): description that'll displayed in docs
description: 'An employee input',
// schema (required): schemas that this input type is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// fields (required): fields of the input type
// see Fields definition for more details
fields: {
id: 'ID!',
name: 'String',
},
}
Unions
Defined with graphql: union
const User = {
// graphql = union means it's a graphql union
graphql: 'union',
// name (optional): name of the union
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {User}}
name: 'User',
// description (optional): description that'll displayed in docs
description: 'An employee or a guest',
// schema (required): schemas that this union is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// types (required): types that this union contains
types: ['Employee', 'Guest'],
// resolveType (optional): function for determining which type is actually used when the value is resolved
resolveType: (value, info) => 'Type',
}
Interface
Defined with graphql: interface
Interfaces in gqutils
work more like extends
, i.e. any type
that implements an interface automatically has the fields of that interface.
This can be used to have a set of default fields. (Along with default resolver implementations)
const Vehicle = {
// graphql = interface means it's a graphql iterface
graphql: 'interface',
// name (optional): name of the interface
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {Vehicle}}
name: 'Vehicle',
// description (optional): description that'll displayed in docs
description: 'A vehicle (can be a car or bike or bus etc)',
// schema (required): schemas that this interface is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// extends (optional): extend other interfaces(s)
// Fields of other interfaces will be merged in this interface.
// (To expose all common fields in graphql api)
// If some interface(s) do(es) not belong to the currently used schema
// then that interface(s) will be ignored.
extends: ['Transport'],
// fields (required): fields of the interface
// see Fields definition for more details
fields: {
id: 'ID!',
modelName: 'String',
variantName: 'String',
name: {
type: 'String',
// Interface fields can also have resolvers, these work like default resolvers. Type resolvers over ride these if provided
resolve: (root) => {
return `${root.modelName} - ${root.variantName}`;
}
}
},
// resolveType (optional): function for determining which type is actually used when the value is resolved
resolveType: (value, info) => 'Type',
}
Enum
Defined with graphql: enum
const Color = {
// graphql = enum means it's a graphql enum
graphql: 'enum',
// name (optional): name of the enum
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {Vehicle}}
name: 'Color',
// description (optional): description that'll displayed in docs
description: 'color you know C-O-L-O-R',
// schema (required): schemas that this enum is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// values (required): enum values
// see Field definition for more details
values: {
// both name and value are RED,
RED: 'RED',
// name is WHITE, value is white
WHITE: 'white',
// name is BLACK, value is 0
BLACK: 0,
// you can also define this as an object
BLUE: {
// value (optional): if value is not given, name is used as value
value: 'blue',
// description (optional): description that'll displayed in docs
description: 'the best color obviously',
// deprecationReason (optional): reason for deprecation
deprecationReason: 'too much blue is happening',
// schema (optional): schemas that this value is available in
// if schema is not given, it will be available in its parent's schemas
schema: ['admin'],
},
},
// resolveType (optional): function for determining which type is actually used when the value is resolved
resolveType: (value, info) => 'Type',
}
Scalar
Defined with graphql: scalar
You need to give either resolve
or serialize, parseValue, parseLiteral
const URL = {
// graphql = scalar means it's a graphql scalar
graphql: 'scalar',
// name (optional): name of the scalar
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {URL}}
name: 'URL',
// description (optional): description that'll displayed in docs
description: 'A url',
// schema (required): schemas that this scalar is available in
// if schema is not given, it won't be available in any schema
schema: ['admin', 'public'],
// resolve (required/optional): Already defined graphql scalar you can resolve it with
// if resolve is not given then, serialize, parseValue, parseLiteral must be given
resolve: GraphQLURL
// serialize (optional, default=identity function): send value to client
serialize: (value) => serializedValue,
// parseValue(optional, default=identity function): parse value coming from client
parseValue: (value) => parsedValue,
// parseLiteral (required/optional): parse ast tree built after value coming from client
parseLiteral: (ast) => parsedValue,
}
Query / Mutation / Subscription
- Defined as
graphql: query
=> for Query - Defined as
graphql: mutation
=> for Mutation - Defined as
graphql: subscription
=> for Subscription
const getEmployees = {
// graphql = query means it's a graphql query
graphql: 'query',
// name (optional): name of the query
// if name is not given it'll be taken from the object where it is exported
// eg. export {schema: {employees: getEmployees}}
name: 'employees',
// description (optional): description that'll displayed in docs
description: 'Get employees',
// type (required): type that this query returns
type: 'EmployeeConnection',
// schema (optional): schemas that this type is available in
// if schema is not given, it will be available in its parent's schemas (Employee's)
schema: ['admin', 'public'],
// resolve (optional): resolver for this query
// this can also be defined in resolvers
resolve: (root, args, ctx, info) => {}
// args (optional): arguments of the query
// see Fields / Args definition for more details
args: {
$default: ['id', '$paging'],
name: 'String',
email: 'String',
},
}
Fields / Args
const Employee = {
graphql: 'type',
name: 'Employee',
// fields
fields: {
// key is field's name, value is field's type
id: 'ID!',
// name is email, type is String
email: 'String',
// you can use ! for non null, and [] for list same as graphql
emails: '[String!]',
// you can also define it as an object
teams: {
// type (required): type of the field
type: 'TeamConnection',
// description (optional): description that'll displayed in docs
description: 'teams that the employee belongs to',
// default (optional): default value of the field
default: 'yo',
// schema (optional): schemas that this type is available in
// if schema is not given, it will be available in its parent's schemas (Employee's)
schema: ['admin'],
// deprecationReason (optional): reason why this field was deprecated
deprecationReason: 'teams are so old fashioned',
// resolve (optional): resolver for this field
// this can also be defined in resolvers
resolve: (root, args, ctx, info) => {}
// args (optional): arguments that this field takes
// NOTE: args are defined as the same way fields are
args: {
// $default is special
// fields defined in $default will be taken from parent's (TeamConnection's) fields
// fields in $default will not have required condition even if mentioned in the type
// to enforce required condition add `!` to the field's name
// $paging is used for paging parameters (first, after, last, before)
// $order is used for order parameters (orderBy & orderDirection)
// $sort is used for order params (sort & order)
// $sort uses String type for order while order uses Enum
$default: ['id', 'phone!', '$paging', '$sort'],
// rest of the parameters are defined in same way as field definition
search: 'String',
status: {
type: 'String',
default: 'active',
schema: ['admin'],
},
},
},
}
}
Fragments
For use with Gql's fragment function while building queries.
const EmployeeFragment = {
graphql: 'fragment',
// Type on which fragment is to be declared
type: 'Employee',
fields: [
'id',
// Can also provide options for fields
{
// will be queried as `contact: email`
alias: 'contact',
name: 'email',
},
{
name: 'teams',
// This is converted to arg options like in `Gql.tag`
args: {
status: 'active',
},
// Can also nest fields
fields: [
'id',
'phone',
]
},
{
name: 'transport',
fields: [
'id',
{
name: 'Vehicle',
// this is converted to inline fragment
inline: true,
// Can also nest fields
fields: [
'number',
'model',
],
},
],
},
],
};
Output:
fragment EmployeeFragment on Employee {
id
contact: email
teams(status: "active") {
id
phone
}
transport {
id
... on Vehicle {
number
model
}
}
}
getConnectionResolver(query, args, options = {})
Given a query (xorm query) and its arguments, it'll automatically generate a resolver for a relay connection.
options can be {resolvers: { fields }}
if you want to override default resolvers or specify any extra resolver.
async function getEmployees(root, args) {
const query = Employee.query();
if (args.name) {
query.where('name', 'like', `%${args.name}%`);
}
return getConnectionResolver(query, args);
}
async function getReviews(root, args) {
const query = Review.query();
if (args.name) {
query.where('name', 'like', `%${args.name}%`);
}
return getConnectionResolver(query, args, {
resolvers: {
totalCount: 0,
edges: {
format: (node, i, {offset}) => `${offset + i}. ${node.title}`,
}
}
});
}
formatError
Use this function to format the errors sent to the client, so that you can display them in a user friendly way.
It'll add fields
to each error, which you can use to display errors on front end.
import {formatError} from 'gqutils';
route.post('/api', apolloKoa({
schema: graphqlSchema,
formatError: formatError,
}));
Generate type definitions from schema
Using https://github.com/dangcuuson/graphql-schema-typescript#readme to generate types. Read more about them here https://medium.com/@pongsatt/how-to-generate-typescript-types-from-graphql-schemas-8d63ed6cda2e
Pass the generated schema to generateTypesFromSchema
and it will output type definitions in 'typings/graphql' folder.
Or use the cli after creating gqutils config or adding to package.json
CLI
Usage: gqutils types [options]
Use to generate types from graphql schema
$ gqutils types
Only build specific schema:
$ gqutils types --schema admin
Options:
-v, --version output the version number
-s, --schema [name] Specify schema (default: all)
-d, --dest [dir] Specify Destination Directory (default: typings/graphql) (default: "typings/graphql")
-h, --help output usage information
Config File
Have a file 'gqutils.js' in the projects root directory which exports the following options:
moudles.exports = {
modules: [
'Array',
'of',
'modules',
],
baseFolder: 'dist/lib',
schema: ['schemaNames', 'to', 'generate' 'types', 'for'],
contextType: 'any', // Or a custom type you have declared globally in a .d.ts file
// Options of 'graphql-schema-typescript' (https://github.com/dangcuuson/graphql-schema-typescript/blob/master/src/types.ts)
generateTypeOptions: {
tabSpaces: 4,
},
}
Or provide these properties in your package.json under the key 'gqutils'.