blips
Version:
State management for the GraphQL heads
246 lines (207 loc) • 6.96 kB
JavaScript
import { GraphQLError, execute, subscribe, validate, } from 'graphql'
import { makeExecutableSchema, } from 'graphql-tools/dist/schemaGenerator'
import { PubSub, withFilter, } from 'graphql-subscriptions'
import { createApolloFetch, } from 'apollo-fetch'
import Clerk from 'state-clerk'
import {
when,
isEmpty,
isType,
toObservable,
extendContext,
getDocument,
promiseBatch,
validateWithoutSchema,
} from './utils'
export const CLIENTGRAPHQL_DEPRECATION_WARNING = ` The "graphql" method is deprecated and will be removed in the 1.0 release. Use the "fetch" method instead.`
export const CREATESTORE_DEPRECATION_WARNING = `The "createStore" method is deprecated and will be removed in the 1.0 release. Use "new BlipsClient(...)" instead`
export const FETCH_NOT_CONFIGURED_ERROR = `You are trying to use "BlipsClient.fetch" without it being configured.`
export const NO_SUBSCRIPTION_ERROR = `No subscription operation defined in query`
export const LONE_SUBSCRIPTION_OPERATION_ERROR = `Only one subscription operation is allowed per query`
export const incorrectMiddlewareOrAfterwareType = (
name = 'middleware/afterware'
) => `The fetch ${name} must be a function or an array of functions`
const loggedWarnings = {}
export function BlipsClient (
{ typeDefs, resolvers, } = {},
initialState,
{ variables = {}, context = {}, fetch = {}, } = {}
) {
const _pubsub = new PubSub()
let _state
let _schema
let _resolvers
let _clerk
let _context
let _variables
let _apolloFetch
function _validateDocument (sourceOrDocument) {
let document = sourceOrDocument
try {
document = getDocument(sourceOrDocument)
} catch (syntaxError) {
return { document, errors: [ syntaxError, ], }
}
let errors = _schema
? validate(_schema, document)
: validateWithoutSchema(document)
return { document, ...(!isEmpty(errors) && { errors, }), }
}
function _runner (fn) {
return function (
document,
{ variables = {}, context = {}, } = {},
operationName,
resolveFn
) {
return (0, fn)(
_schema,
document,
{},
extendContext(_context, context),
{ ..._variables, ...variables, },
operationName,
resolveFn
)
}
}
function _setUpMiddlewareOrAfterware (listOrFn, name) {
if (!listOrFn) return
if (isType('function', listOrFn)) {
_apolloFetch.use(listOrFn)
} else if (isType('array', listOrFn)) {
for (const middleware of listOrFn) {
_apolloFetch.use(middleware)
}
} else {
throw new TypeError(incorrectMiddlewareOrAfterwareType(name))
}
}
const _executor = _runner(execute)
const _subscriber = _runner(subscribe)
function _execute (operationType) {
return (sourceOrDocument, options, operationName, resolveFn) => {
const { document, errors, } = _validateDocument(sourceOrDocument)
if (errors) return Promise.resolve({ errors, })
// If an operationName is provided, go ahead and execute that
if (operationName) {
return _executor(document, options, operationName, resolveFn)
}
const executableOperations = document.definitions.filter(
definition => definition.operation === operationType
)
// If there's just one operation definition of the correct type execute it
if (executableOperations.length === 1) {
return _executor(document, options, operationName, resolveFn)
}
// otherwise:
return promiseBatch(
executableOperations.map(operation => {
return _executor(document, options, operation.name.value, resolveFn)
})
)
}
}
;(function constructor () {
// store the initial state
_state = { ...initialState, } || {}
// create and store the clerk instance
_clerk = new Clerk(_state)
// compute and store the resolvers
_resolvers = when(
typeof resolvers === 'function',
resolvers({ pubsub: _pubsub, withFilter, }),
resolvers
)
_schema = makeExecutableSchema({
typeDefs,
resolvers: _resolvers,
})
// store the default variables
_variables = variables
// compute and store the default context
_context = extendContext(
{
// add store to the context.
// it will not be replaced by any `store` props passed through the default options
store: {
..._clerk,
},
},
context
)
// create apolloFetch
if (fetch.uri) {
_apolloFetch = createApolloFetch({ uri: fetch.uri, })
// set up middleware and afterware
_setUpMiddlewareOrAfterware(fetch.middleware, 'middleware')
_setUpMiddlewareOrAfterware(fetch.afterware, 'afterware')
}
})()
return {
/**
* returns the state object
* @return {Object}
*/
get state () {
return _state
},
/**
* returns the schema
* @return {GraphQLSchema}
*/
get schema () {
return _schema
},
query: _execute('query'),
mutate: _execute('mutation'),
subscribe: async (sourceOrDocument, options, operationName, resolveFn) => {
const { document, errors, } = _validateDocument(sourceOrDocument)
if (errors) return Promise.resolve({ errors, })
const operations = document.definitions.filter(
definition => definition.operation === 'subscription'
)
if (operations.length < 1) {
return Promise.resolve({
errors: [ new GraphQLError(NO_SUBSCRIPTION_ERROR), ],
})
} else if (operations.length > 1) {
return Promise.resolve({
errors: [ new GraphQLError(LONE_SUBSCRIPTION_OPERATION_ERROR), ],
})
}
const iterator = await _subscriber(
document,
options,
operationName,
resolveFn
)
return toObservable(iterator)
},
fetch: (query, { variables = {}, } = {}, operationName) => {
if (!_apolloFetch || !isType('function', _apolloFetch)) {
throw new Error(FETCH_NOT_CONFIGURED_ERROR)
}
return _apolloFetch({ query, variables, operationName, })
},
graphql: (query, { variables = {}, } = {}, operationName) => {
if (!loggedWarnings[CLIENTGRAPHQL_DEPRECATION_WARNING]) {
// eslint-disable-next-line
console.warn(CLIENTGRAPHQL_DEPRECATION_WARNING)
loggedWarnings[CLIENTGRAPHQL_DEPRECATION_WARNING] = true
}
if (!_apolloFetch || !isType('function', _apolloFetch)) {
throw new Error(FETCH_NOT_CONFIGURED_ERROR)
}
return _apolloFetch({ query, variables, operationName, })
},
}
}
export function createStore (...args) {
if (!loggedWarnings[CREATESTORE_DEPRECATION_WARNING]) {
// eslint-disable-next-line
console.warn(CREATESTORE_DEPRECATION_WARNING)
loggedWarnings[CREATESTORE_DEPRECATION_WARNING] = true
}
return new BlipsClient(...args)
}