Skip to content

Commit cf28e96

Browse files
committed
feat: create new cache directive
1 parent 219f086 commit cf28e96

File tree

4 files changed

+175
-1
lines changed

4 files changed

+175
-1
lines changed

server/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { startStandaloneServer } from '@apollo/server/standalone';
33
import { makeExecutableSchema } from '@graphql-tools/schema';
44
import encodingDirective from '@src/directives/encode';
55
import regexDirective from '@src/directives/regex';
6+
import cacheDirective from '@src/directives/cache';
67

78
const typeDefs = String.raw`#graphql
89
type User {
910
firstName: String @regex(pattern: "(Eddie|Sam)")
1011
lastName: String @regex(pattern: "\\b[A-Z]\\w+\\b")
11-
age: Int
12+
age: Int @cache(key: "user_age", ttl: 3000)
1213
}
1314
1415
type Query {
@@ -28,16 +29,19 @@ const resolvers = {
2829

2930
const { encodingDirectiveTypeDefs, encodingDirectiveTransformer } = encodingDirective('encode')
3031
const { regexDirectiveTypeDefs, regexDirectiveTransformer } = regexDirective('regex')
32+
const { cacheDirectiveTypeDefs, cacheDirectiveTransformer } = cacheDirective('cache')
3133

3234
const transformers = [
3335
encodingDirectiveTransformer,
3436
regexDirectiveTransformer,
37+
cacheDirectiveTransformer,
3538
]
3639

3740
let schema = makeExecutableSchema(({
3841
typeDefs: [
3942
encodingDirectiveTypeDefs,
4043
regexDirectiveTypeDefs,
44+
cacheDirectiveTypeDefs,
4145
typeDefs
4246
],
4347
resolvers

src/directives/cache.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { MapperKind, getDirective, mapSchema } from '@graphql-tools/utils'
2+
import { GraphQLSchema, defaultFieldResolver } from 'graphql'
3+
4+
interface CachingImpl {
5+
has: (key: string) => boolean
6+
get: (key: string) => string
7+
set: (key: string, value: string) => void
8+
delete: (key: string) => boolean
9+
}
10+
11+
const cacheDirective = (directiveName: string, cache: CachingImpl = new Map<string, string>()) => {
12+
return {
13+
cacheDirectiveTypeDefs: `directive @${directiveName}(key: String, ttl: Int) on FIELD_DEFINITION`,
14+
cacheDirectiveTransformer: (schema: GraphQLSchema) => mapSchema(schema, {
15+
[MapperKind.OBJECT_FIELD]: fieldConfig => {
16+
const { resolve = defaultFieldResolver } = fieldConfig
17+
const cacheDirective = getDirective(schema, fieldConfig, directiveName)?.[0]
18+
if (cacheDirective) {
19+
const { ttl, key } = cacheDirective
20+
return {
21+
...fieldConfig,
22+
resolve: async (source, args, context, info) => {
23+
const { returnType } = info
24+
if (cache.has(key)) {
25+
const value = cache.get(key)
26+
if (returnType.toString() === 'string') {
27+
return value
28+
}
29+
if (returnType.toString() === 'boolean') {
30+
const boolValue = (/true/).test(value);
31+
return boolValue
32+
}
33+
if (returnType.toString() === 'Int') {
34+
return Number(value)
35+
}
36+
}
37+
const result = await resolve(source, args, context, info)
38+
cache.set(key, JSON.stringify(result))
39+
setTimeout(() => {
40+
cache.delete(key)
41+
}, ttl)
42+
return result
43+
}
44+
}
45+
}
46+
}
47+
}
48+
)
49+
}
50+
}
51+
52+
export default cacheDirective

src/directives/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import cacheDirective from '@src/directives/cache'
12
import encodingDirective from '@src/directives/encode'
23
import regexDirective from '@src/directives/regex'
34

45
const directives = Object.freeze({
6+
cacheDirective,
57
encodingDirective,
68
regexDirective,
79
})

test/cache.test.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { ApolloServer } from '@apollo/server';
2+
import cacheDirective from '@src/directives/cache';
3+
import { buildSchema } from './util';
4+
import assert from 'assert';
5+
6+
jest.useFakeTimers();
7+
jest.spyOn(global, 'setTimeout');
8+
9+
const cache = new Map<string, string>()
10+
11+
const cacheCallback = {
12+
has: jest.fn(key => cache.has(key)),
13+
get: jest.fn(key => cache.get(key)),
14+
delete: jest.fn(key => cache.delete(key)),
15+
set: jest.fn((key: string, value: string) => {
16+
cache.set(key, value)
17+
}),
18+
}
19+
20+
const { cacheDirectiveTypeDefs, cacheDirectiveTransformer } = cacheDirective('cache', cacheCallback)
21+
22+
describe('@cache directive', () => {
23+
let testServer: ApolloServer;
24+
25+
const resolvers = {
26+
Query: {
27+
user: () => ({
28+
age: 28
29+
})
30+
},
31+
};
32+
33+
const testQuery = `
34+
query ExampleQuery {
35+
user {
36+
age
37+
}
38+
}
39+
`
40+
41+
afterEach(async () => {
42+
if (testServer) {
43+
await testServer.stop()
44+
}
45+
})
46+
47+
it('will cache a field value before returning a response with correct ttl', async () => {
48+
const ttl = 8000
49+
const schema = buildSchema({
50+
typeDefs: [
51+
`type User {
52+
age: Int @cache(key: "user_age", ttl: ${ttl})
53+
}
54+
55+
type Query {
56+
user: User
57+
}
58+
`,
59+
cacheDirectiveTypeDefs,
60+
],
61+
resolvers,
62+
transformers: [cacheDirectiveTransformer],
63+
})
64+
65+
testServer = new ApolloServer({ schema })
66+
67+
const response = await testServer.executeOperation<{ user: { age: number } }>({
68+
query: testQuery
69+
})
70+
71+
assert(response.body.kind === 'single');
72+
expect(response.body.singleResult.errors).toBeUndefined();
73+
expect(response.body.singleResult.data.user.age).toEqual(28);
74+
expect(setTimeout).toHaveBeenCalledTimes(1);
75+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), ttl);
76+
expect(cacheCallback.set).toHaveBeenCalled()
77+
})
78+
79+
it('will delete a cache field value if the ttl has long expired', async () => {
80+
const ttl = 3000
81+
const key = 'user_age'
82+
const schema = buildSchema({
83+
typeDefs: [
84+
`type User {
85+
age: Int @cache(key: "${key}", ttl: ${ttl})
86+
}
87+
88+
type Query {
89+
user: User
90+
}
91+
`,
92+
cacheDirectiveTypeDefs,
93+
],
94+
resolvers,
95+
transformers: [cacheDirectiveTransformer],
96+
})
97+
98+
testServer = new ApolloServer({ schema })
99+
100+
const response = await testServer.executeOperation<{ user: { age: number } }>({
101+
query: testQuery
102+
})
103+
104+
assert(response.body.kind === 'single');
105+
expect(response.body.singleResult.errors).toBeUndefined();
106+
expect(response.body.singleResult.data.user.age).toEqual(28);
107+
108+
expect(cacheCallback.set).toHaveBeenCalled()
109+
expect(cacheCallback.set).toHaveBeenCalledWith(key, JSON.stringify(response.body.singleResult.data.user.age))
110+
111+
jest.advanceTimersByTime(ttl + 5000)
112+
113+
expect(cacheCallback.delete).toHaveBeenCalled()
114+
expect(cacheCallback.delete).toHaveBeenCalledWith(key)
115+
})
116+
})

0 commit comments

Comments
 (0)