Skip to content

Commit 9dc6655

Browse files
author
J J Walwyn
committed
Add caching using Redis
1 parent 59e4e8b commit 9dc6655

File tree

9 files changed

+302
-48
lines changed

9 files changed

+302
-48
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ SERVER_PORT=3000
1010
# Mongo connection string. This is the default string used with the database spun up in docker-compose.yml
1111
MONGO_URL=mongodb://admin:password@localhost:27017/location-events-api?authSource=admin
1212

13+
# OPTIONAL: A Redis URL for caching
14+
REDIS_URL=redis://localhost:6379
15+
1316
# Google Maps API key for address lookup
1417
# See: https://developers.google.com/maps/documentation/javascript/get-api-key to set this up
1518
GOOGLE_MAPS_API_KEY=

docker-compose.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ services:
1919
ME_CONFIG_MONGODB_SERVER: mongo
2020
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
2121
ME_CONFIG_MONGODB_ADMINPASSWORD: password
22+
23+
redis:
24+
image: 'redis'
25+
restart: 'always'
26+
ports:
27+
- '6379:6379'
28+

package-lock.json

Lines changed: 224 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@
5151
"typescript": "^4.8.2"
5252
},
5353
"dependencies": {
54+
"@apollo/utils.keyvadapter": "^1.1.2",
5455
"@googlemaps/google-maps-services-js": "^3.3.16",
5556
"@graphql-tools/merge": "^8.3.6",
57+
"@keyv/redis": "^2.5.1",
5658
"apollo-datasource-mongodb": "^0.5.4",
5759
"apollo-server-core": "^3.10.2",
5860
"apollo-server-fastify": "^3.10.2",
5961
"apollo-server-plugin-base": "^3.6.2",
6062
"dotenv": "^16.0.2",
6163
"fastify": "^3.29.2",
6264
"graphql": "^16.6.0",
65+
"keyv": "^4.5.0",
6366
"mongoose": "^6.6.1",
6467
"pino": "^8.5.0"
6568
}

src/apollo.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { KeyvAdapter } from '@apollo/utils.keyvadapter';
12
import {
23
ApolloServerPluginDrainHttpServer,
34
ApolloServerPluginLandingPageLocalDefault,
5+
Config,
46
} from 'apollo-server-core';
57
import { ApolloServer } from 'apollo-server-fastify';
68
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
79
import { FastifyInstance } from 'fastify';
10+
import Keyv from 'keyv';
811

12+
import { REDIS_URL } from './consts';
913
import { dataSources, resolvers, typeDefs } from './graphql';
1014

1115
/**
@@ -16,7 +20,9 @@ import { dataSources, resolvers, typeDefs } from './graphql';
1620
*
1721
* @returns apollo server plugin
1822
*/
19-
const fastifyAppClosePlugin = (fastify: FastifyInstance): ApolloServerPlugin => ({
23+
const fastifyAppClosePlugin = (
24+
fastify: FastifyInstance,
25+
): ApolloServerPlugin => ({
2026
/* eslint-disable @typescript-eslint/require-await */
2127
serverWillStart: async () => ({
2228
async drainServer() {
@@ -39,14 +45,23 @@ export default async (fastify: FastifyInstance): Promise<ApolloServer> => {
3945
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
4046
];
4147

42-
const apollo = new ApolloServer({
48+
const config: Config = {
4349
dataSources,
4450
typeDefs,
4551
resolvers,
4652
csrfPrevention: true,
4753
cache: 'bounded',
4854
plugins,
49-
});
55+
};
56+
57+
if (REDIS_URL) {
58+
// Add Redis Cache if URL set
59+
config.cache = new KeyvAdapter(new Keyv(REDIS_URL));
60+
} else {
61+
config.cache = 'bounded';
62+
}
63+
64+
const apollo = new ApolloServer(config);
5065

5166
await apollo.start();
5267

src/consts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ export const SERVER_HOST = process.env.SERVER_HOST || '127.0.0.1';
1515
// Mongo DB Connection
1616
export const MONGO_URL = process.env.MONGO_URL;
1717

18+
// REDIS Connection
19+
export const REDIS_URL = process.env.REDIS_URL;
20+
1821
// Google Maps API key
1922
export const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;

src/generated/graphql.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export type AddressInput = {
3535
region?: InputMaybe<Scalars['String']>;
3636
};
3737

38+
export enum CacheControlScope {
39+
Private = 'PRIVATE',
40+
Public = 'PUBLIC'
41+
}
42+
3843
export type DeleteResult = {
3944
__typename?: 'DeleteResult';
4045
_id: Scalars['ID'];
@@ -396,6 +401,7 @@ export type ResolversTypes = {
396401
Address: ResolverTypeWrapper<Address>;
397402
AddressInput: AddressInput;
398403
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
404+
CacheControlScope: CacheControlScope;
399405
Date: ResolverTypeWrapper<Scalars['Date']>;
400406
DeleteResult: ResolverTypeWrapper<DeleteResult>;
401407
Event: ResolverTypeWrapper<Event>;
@@ -464,6 +470,14 @@ export type ResolversParentTypes = {
464470
UpdateResult: UpdateResult;
465471
};
466472

473+
export type CacheControlDirectiveArgs = {
474+
inheritMaxAge?: Maybe<Scalars['Boolean']>;
475+
maxAge?: Maybe<Scalars['Int']>;
476+
scope?: Maybe<CacheControlScope>;
477+
};
478+
479+
export type CacheControlDirectiveResolver<Result, Parent, ContextType = MyContext, Args = CacheControlDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
480+
467481
export type AddressResolvers<ContextType = MyContext, ParentType extends ResolversParentTypes['Address'] = ResolversParentTypes['Address']> = {
468482
city?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
469483
country?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -614,3 +628,6 @@ export type Resolvers<ContextType = MyContext> = {
614628
UpdateResult?: UpdateResultResolvers<ContextType>;
615629
};
616630

631+
export type DirectiveResolvers<ContextType = MyContext> = {
632+
cacheControl?: CacheControlDirectiveResolver<any, any, ContextType>;
633+
};

src/graphql/dataSources.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-classes-per-file */
2-
import { MongoDataSource } from 'apollo-datasource-mongodb';
2+
import { MongoDataSource, Options } from 'apollo-datasource-mongodb';
33

44
import type { Event, Location, Organisation } from '../generated/graphql';
55
import * as Models from '../db/models';
@@ -9,8 +9,8 @@ export class MyDataSource<T> extends MongoDataSource<T> {
99
/**
1010
* This allows us to grab ObjectIds from GraphQL types
1111
*/
12-
public findOneById(id: unknown) {
13-
return super.findOneById(id as string);
12+
public findOneById(id: unknown, opts?: Options) {
13+
return super.findOneById(id as string, opts);
1414
}
1515
}
1616

src/graphql/resolvers/query.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import orderBy from './order';
55
import * as Models from '../../db/models';
66
import * as Types from '../../generated/graphql';
77

8+
// TTL for collection and document caches.
9+
// Documents get removed from the cache automatically on update
10+
// TODO: Add smart cache clearing and warming
11+
const TTL_COLLECTIONS = 60; // A minute
12+
const TTL_DOCUMENTS = 1209600; // 2 weeks
13+
814
/**
915
* Possible arguments for collection filtering
1016
*/
@@ -34,21 +40,24 @@ const getCollection =
3440
// Find documents from data source
3541
const dataSource =
3642
ctx.dataSources[dataSourceName as keyof typeof ctx.dataSources];
37-
let results = (await dataSource.findByFields(
38-
args.filter || {},
39-
)) as unknown[] as T[];
43+
let results = (await dataSource.findByFields(args.filter || {}, {
44+
ttl: TTL_COLLECTIONS,
45+
})) as unknown[] as T[];
4046

4147
// Order results by a field (or subfield)
4248
if (args.order?.by) {
4349
const dir = args.order.dir || Types.QueryOrderDir.Desc;
4450

4551
if ((args.order.by as string) in orderBy) {
4652
// Check if we have a specific type of ordering function for this method
47-
const sortFunc = orderBy[args.order.by as string as keyof typeof orderBy];
53+
const sortFunc =
54+
orderBy[args.order.by as string as keyof typeof orderBy];
4855
results = results.sort(sortFunc<T>(dir));
4956
} else {
5057
// Otherwise we'll use the default sort method
51-
results = results.sort(orderBy.default<T>(dir, args.order.by as string));
58+
results = results.sort(
59+
orderBy.default<T>(dir, args.order.by as string),
60+
);
5261
}
5362
}
5463

@@ -74,6 +83,7 @@ const getById =
7483
(_: never, args: { id: string }, ctx: MyContext) =>
7584
ctx.dataSources[dataSourceName as keyof typeof ctx.dataSources].findOneById(
7685
args.id,
86+
{ ttl: TTL_DOCUMENTS },
7787
);
7888

7989
export default {
@@ -100,7 +110,9 @@ export default {
100110
Organisation: {
101111
// Find one to one
102112
location: (event: Types.Event, _: never, ctx: MyContext) =>
103-
ctx.dataSources.locations.findOneById(event.location),
113+
ctx.dataSources.locations.findOneById(event.location, {
114+
ttl: TTL_DOCUMENTS,
115+
}),
104116
// Find one to many
105117
findEvents: (
106118
organisation: Types.Organisation,
@@ -115,9 +127,13 @@ export default {
115127
},
116128
Event: {
117129
organisation: (event: Types.Event, _: never, ctx: MyContext) =>
118-
ctx.dataSources.organisations.findOneById(event.organisation),
130+
ctx.dataSources.organisations.findOneById(event.organisation, {
131+
ttl: TTL_DOCUMENTS,
132+
}),
119133
location: (event: Types.Event, _: never, ctx: MyContext) =>
120-
ctx.dataSources.locations.findOneById(event.location),
134+
ctx.dataSources.locations.findOneById(event.location, {
135+
ttl: TTL_DOCUMENTS,
136+
}),
121137
},
122138
Location: {
123139
// Find one to many

0 commit comments

Comments
 (0)