mercurius-cache
Advanced tools
Comparing version 0.10.0 to 0.11.0
104
index.js
@@ -5,13 +5,13 @@ 'use strict' | ||
const { Cache } = require('async-cache-dedupe') | ||
const { validateOpts } = require('./lib/validation') | ||
module.exports = fp(async function (app, { all, policy, ttl, cacheSize, skip, storage, onHit, onMiss, onSkip, ...other }) { | ||
if (typeof policy !== 'object' && !all) { | ||
throw new Error('policy must be an object') | ||
} else if (all && policy) { | ||
throw new Error('policy and all options are exclusive') | ||
module.exports = fp(async function (app, opts) { | ||
validateOpts(opts) | ||
let { all, policy, ttl, cacheSize, skip, storage, onHit, onMiss, onSkip, logInterval, logReport } = opts | ||
if (!logReport) { | ||
logReport = defaultLogReport | ||
} | ||
// TODO validate mercurius is already registered | ||
// TODO validate policy | ||
onHit = onHit || noop | ||
@@ -22,7 +22,14 @@ onMiss = onMiss || noop | ||
let cache = null | ||
let logTimer = null | ||
let cacheReport = null | ||
const cacheReportingEnabled = logInterval && ((policy && policy.Query) || all) | ||
if (cacheReportingEnabled) { | ||
initCacheReport() | ||
} | ||
app.graphql.cache = { | ||
refresh () { | ||
buildCache() | ||
setupSchema(app.graphql.schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip) | ||
setupSchema(app.graphql.schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip, cacheReport) | ||
}, | ||
@@ -32,2 +39,4 @@ | ||
cache.clear() | ||
logReport(cacheReport) | ||
clearCacheReport(cacheReport) | ||
} | ||
@@ -38,8 +47,17 @@ } | ||
app.graphql.cache.refresh() | ||
if (cacheReportingEnabled) { | ||
logTimer = setInterval(logAndClearCacheReport, logInterval * 1000, cacheReport).unref() | ||
} | ||
}) | ||
app.addHook('onClose', () => { | ||
if (logTimer) { | ||
clearInterval(logTimer) | ||
} | ||
}) | ||
// Add hook to regenerate the resolvers when the schema is refreshed | ||
app.graphql.addHook('onGatewayReplaceSchema', async (instance, schema) => { | ||
buildCache() | ||
setupSchema(schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip) | ||
setupSchema(schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip, cacheReport) | ||
}) | ||
@@ -53,6 +71,43 @@ | ||
} | ||
function initCacheReport () { | ||
cacheReport = {} | ||
const schema = app.graphql.schema | ||
const fields = all ? Object.keys(schema.getQueryType().getFields()) : Object.keys(policy.Query) | ||
for (const field of fields) { | ||
const name = 'Query.' + field | ||
cacheReport[name] = {} | ||
cacheReport[name].hits = 0 | ||
cacheReport[name].misses = 0 | ||
} | ||
} | ||
function clearCacheReport (cacheReport) { | ||
if (!cacheReport) return | ||
for (const item of Object.keys(cacheReport)) { | ||
cacheReport[item].hits = 0 | ||
cacheReport[item].misses = 0 | ||
} | ||
} | ||
function defaultLogReport (cacheReport) { | ||
app.log.info({ cacheReport }, 'mercurius-cache report') | ||
} | ||
function logAndClearCacheReport (cacheReport) { | ||
logReport(cacheReport) | ||
clearCacheReport(cacheReport) | ||
} | ||
}, { | ||
fastify: '^3.x', | ||
dependencies: ['mercurius'] | ||
}) | ||
function setupSchema (schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip) { | ||
function setupSchema (schema, policy, all, cache, skip, storage, onHit, onMiss, onSkip, cacheReport) { | ||
const schemaTypeMap = schema.getTypeMap() | ||
let queryKeys = policy ? Object.keys(policy.Query) : [] | ||
for (const schemaType of Object.values(schemaTypeMap)) { | ||
@@ -69,6 +124,8 @@ const fieldPolicy = all || policy[schemaType] | ||
if (all || policy) { | ||
// validate schema vs query values | ||
queryKeys = queryKeys.filter(key => key !== fieldName) | ||
// Override resolvers for caching purposes | ||
if (typeof field.resolve === 'function') { | ||
const originalFieldResolver = field.resolve | ||
field.resolve = makeCachedResolver(schemaType.toString(), fieldName, cache, originalFieldResolver, policy, skip, storage, onHit, onMiss, onSkip) | ||
field.resolve = makeCachedResolver(schemaType.toString(), fieldName, cache, originalFieldResolver, policy, skip, storage, onHit, onMiss, onSkip, cacheReport) | ||
} | ||
@@ -79,5 +136,6 @@ } | ||
} | ||
if (queryKeys.length) { throw new Error(`Query does not match schema: ${queryKeys}`) } | ||
} | ||
function makeCachedResolver (prefix, fieldName, cache, originalFieldResolver, policy, skip, storage, onHit, onMiss, onSkip) { | ||
function makeCachedResolver (prefix, fieldName, cache, originalFieldResolver, policy, skip, storage, onHit, onMiss, onSkip, cacheReport) { | ||
const name = prefix + '.' + fieldName | ||
@@ -88,4 +146,19 @@ onHit = onHit.bind(null, prefix, fieldName) | ||
let onCacheHit = null | ||
let onCacheMiss = null | ||
if (cacheReport) { | ||
onCacheHit = () => { | ||
cacheReport[name].hits++ | ||
onHit() | ||
} | ||
onCacheMiss = () => { | ||
cacheReport[name].misses++ | ||
onMiss() | ||
} | ||
} | ||
cache.define(name, { | ||
onHit, | ||
onHit: onCacheHit || onHit, | ||
ttl: policy && policy.ttl, | ||
@@ -126,3 +199,4 @@ cacheSize: policy && policy.cacheSize, | ||
} | ||
onMiss() | ||
onCacheMiss ? onCacheMiss() : onMiss() | ||
const res = await originalFieldResolver(self, arg, ctx, info) | ||
@@ -129,0 +203,0 @@ if (storage) { |
{ | ||
"name": "mercurius-cache", | ||
"version": "0.10.0", | ||
"version": "0.11.0", | ||
"description": "Cache the results of your GraphQL resolvers, for Mercurius", | ||
@@ -32,2 +32,3 @@ "main": "index.js", | ||
"snazzy": "^9.0.0", | ||
"split2": "^4.1.0", | ||
"standard": "^16.0.3", | ||
@@ -34,0 +35,0 @@ "tap": "^15.0.9", |
@@ -240,2 +240,25 @@ # mercurius-cache | ||
- **logInterval** | ||
This option enables cache report with hit/miss count for all queries specified in the policy. | ||
The value of the interval is in *seconds*. | ||
Example | ||
```js | ||
logInterval: 3 | ||
``` | ||
- **logReport** | ||
custom function for logging cache hits/misses. called every `logInterval` seconds when the cache report is logged. | ||
Example | ||
```js | ||
logReport (cacheReport) { | ||
console.log(`Cache report - ${cacheReport}`) | ||
} | ||
``` | ||
## Benchmarks | ||
@@ -242,0 +265,0 @@ |
@@ -671,2 +671,99 @@ 'use strict' | ||
equal(hitCount, 0) | ||
}); | ||
[ | ||
{ | ||
title: 'using all option as string', | ||
cacheConfig: { all: 'true' }, | ||
expect: 'all must be an boolean' | ||
}, | ||
{ | ||
title: 'using ttl option as string', | ||
cacheConfig: { ttl: '10' }, | ||
expect: 'ttl must be a number' | ||
}, | ||
{ | ||
title: 'using cacheSize option as string', | ||
cacheConfig: { cacheSize: '1024' }, | ||
expect: 'cacheSize must be a number' | ||
}, | ||
{ | ||
title: 'using onHit option as string', | ||
cacheConfig: { onHit: 'not a function' }, | ||
expect: 'onHit must be a function' | ||
}, | ||
{ | ||
title: 'using onMiss option as string', | ||
cacheConfig: { onMiss: 'not a function' }, | ||
expect: 'onMiss must be a function' | ||
}, | ||
{ | ||
title: 'using onSkip option as string', | ||
cacheConfig: { onSkip: 'not a function' }, | ||
expect: 'onSkip must be a function' | ||
}, | ||
{ | ||
title: 'using policy option as string', | ||
cacheConfig: { policy: 'not an object' }, | ||
expect: 'policy must be an object' | ||
} | ||
].forEach(useCase => { | ||
test(useCase.title, async (t) => { | ||
t.plan(1) | ||
const app = fastify() | ||
app.register(mercurius) | ||
await t.rejects(app.register(cache, useCase.cacheConfig), useCase.expect) | ||
}) | ||
}) | ||
test('Unmatched schema for Query', async ({ rejects, teardown }) => { | ||
const app = fastify() | ||
teardown(app.close.bind(app)) | ||
const schema = ` | ||
type Query { | ||
add(x: Int, y: Int): Int | ||
} | ||
` | ||
const resolvers = { | ||
Query: { | ||
async add (_, { x, y }) { | ||
await immediate() | ||
return x + y | ||
} | ||
} | ||
} | ||
app.register(mercurius, { | ||
schema, | ||
resolvers | ||
}) | ||
app.register(cache, { | ||
policy: { | ||
Query: { | ||
add: true, | ||
foo: 'bar' | ||
} | ||
} | ||
}) | ||
await Promise.all([ | ||
query(), | ||
query() | ||
]) | ||
async function query () { | ||
const query = '{ add(x: 2, y: 2) }' | ||
await rejects(app.inject({ | ||
method: 'POST', | ||
url: '/graphql', | ||
body: { | ||
query | ||
} | ||
}), 'Query does not match schema: foo') | ||
} | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
75568
26
2548
368
11