Skip to content

Instantly share code, notes, and snippets.

@wmertens
Created November 14, 2018 09:43
Show Gist options
  • Save wmertens/c291e074c74b88a482b2c61abe5b9947 to your computer and use it in GitHub Desktop.
Save wmertens/c291e074c74b88a482b2c61abe5b9947 to your computer and use it in GitHub Desktop.
Wrap graphql schema with enforced admin-only for mutations, timing etc
/* eslint-disable max-depth, no-console */
// TODO create adminSchema, leave adminQueries and mutations out of schema
// In the graphqlhandler, pass the right schema depending on isAdmin
import debug from 'debug'
import {get} from 'lodash'
import {GraphQLSchema, GraphQLObjectType} from 'graphql'
import ssrCache from 'stratokit/prerender/cache'
import {adminOnly} from './utils'
import {maskErrors} from 'graphql-errors'
import * as allQs from 'app/_server/graphql'
const dbg = debug('graphql')
const timings = {}
let lastDump = Date.now()
const checkDump = now => {
if (now - lastDump > 10000) {
lastDump = now
console.log(
'graphql timings',
Object.entries(timings)
.filter(o => o[1].count)
.map(
([name, m]) =>
`${name}: ${m.count}x ${m.min}<=${Math.round(m.total / m.count)}<=${
m.max
} ms`
)
)
}
}
const instrument = (q, name) => {
const {resolve} = q
const measurements = {
min: 9999999,
max: 0,
count: 0,
total: 0,
}
timings[name] = measurements
return {
...q,
resolve: async (...args) => {
const now = Date.now()
let out, t
try {
out = await resolve(...args)
t = Date.now() - now
measurements.total += t
measurements.count++
if (t < measurements.min) measurements.min = t
if (t > measurements.max) measurements.max = t
} finally {
dbg(`${name}: ${t >= 0 ? `${t}ms` : 'error'}`)
if (t > 5000)
console.error(
`!!! query ${name} took ${t}ms`,
JSON.stringify({
v: get(args, '3.variableValues'),
q: get(args, '3.operation.loc.source.body'),
}).slice(0, 1000)
)
checkDump(now)
}
return out
},
}
}
const parts = Object.values(allQs)
const safeForSSR = {}
parts.forEach(p => {
if (p.safeForSSR)
p.safeForSSR.forEach(k => {
safeForSSR[k] = true
})
})
const alias = {
query: 'queries',
mutation: 'mutations',
adminQuery: 'adminQueries',
openMutation: 'openMutations',
}
const schemaTypes = {
query: 'query',
mutation: 'mutation',
adminQuery: 'query',
openMutation: 'mutation',
}
const isMutation = {
mutation: true,
openMutation: true,
}
const isAdminOnly = {
mutation: true,
adminQuery: true,
}
const types = Object.keys(schemaTypes)
const fieldsByType = {
query: {},
mutation: {},
}
for (const type of types) {
const partsOfType = parts.map(p => p[type] || p[alias[type]]).filter(Boolean)
if (!partsOfType.length) continue
const fields = fieldsByType[schemaTypes[type]]
for (const toAdd of partsOfType) {
for (const k of Object.keys(toAdd)) {
if (fields[k]) throw new Error(`Duplicate graphql endpoint ${k}`)
fields[k] = instrument(toAdd[k], k)
if (isMutation[type] && !safeForSSR[k]) {
// Clear SSR cache on mutation
const {resolve} = fields[k]
fields[k].resolve = (...args) => {
dbg('resetting SSR cache')
ssrCache.reset()
return resolve(...args)
}
}
if (isAdminOnly[type]) {
fields[k] = adminOnly(fields[k])
}
}
}
}
const schema = {}
for (const type of Object.keys(fieldsByType)) {
schema[type] = new GraphQLObjectType({
name: type,
fields: fieldsByType[type],
})
}
const gqlSchema = new GraphQLSchema(schema)
// Hide error details from users
maskErrors(gqlSchema)
export default gqlSchema
@wmertens
Copy link
Author

Note that github doesn't send notifications for gists - if you want to contact me, use Wout.Mertens@gmail.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment