Created
November 14, 2018 09:43
-
-
Save wmertens/c291e074c74b88a482b2c61abe5b9947 to your computer and use it in GitHub Desktop.
Wrap graphql schema with enforced admin-only for mutations, timing etc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note that github doesn't send notifications for gists - if you want to contact me, use [email protected]