Skip to content

Commit

Permalink
Add caching using Redis
Browse files Browse the repository at this point in the history
  • Loading branch information
J J Walwyn committed Oct 10, 2022
1 parent 59e4e8b commit 9dc6655
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 48 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ SERVER_PORT=3000
# Mongo connection string. This is the default string used with the database spun up in docker-compose.yml
MONGO_URL=mongodb://admin:password@localhost:27017/location-events-api?authSource=admin

# OPTIONAL: A Redis URL for caching
REDIS_URL=redis://localhost:6379

# Google Maps API key for address lookup
# See: https://developers.google.com/maps/documentation/javascript/get-api-key to set this up
GOOGLE_MAPS_API_KEY=
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ services:
ME_CONFIG_MONGODB_SERVER: mongo
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password

redis:
image: 'redis'
restart: 'always'
ports:
- '6379:6379'

258 changes: 224 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@
"typescript": "^4.8.2"
},
"dependencies": {
"@apollo/utils.keyvadapter": "^1.1.2",
"@googlemaps/google-maps-services-js": "^3.3.16",
"@graphql-tools/merge": "^8.3.6",
"@keyv/redis": "^2.5.1",
"apollo-datasource-mongodb": "^0.5.4",
"apollo-server-core": "^3.10.2",
"apollo-server-fastify": "^3.10.2",
"apollo-server-plugin-base": "^3.6.2",
"dotenv": "^16.0.2",
"fastify": "^3.29.2",
"graphql": "^16.6.0",
"keyv": "^4.5.0",
"mongoose": "^6.6.1",
"pino": "^8.5.0"
}
Expand Down
21 changes: 18 additions & 3 deletions src/apollo.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { KeyvAdapter } from '@apollo/utils.keyvadapter';
import {
ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault,
Config,
} from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-fastify';
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import { FastifyInstance } from 'fastify';
import Keyv from 'keyv';

import { REDIS_URL } from './consts';
import { dataSources, resolvers, typeDefs } from './graphql';

/**
Expand All @@ -16,7 +20,9 @@ import { dataSources, resolvers, typeDefs } from './graphql';
*
* @returns apollo server plugin
*/
const fastifyAppClosePlugin = (fastify: FastifyInstance): ApolloServerPlugin => ({
const fastifyAppClosePlugin = (
fastify: FastifyInstance,
): ApolloServerPlugin => ({
/* eslint-disable @typescript-eslint/require-await */
serverWillStart: async () => ({
async drainServer() {
Expand All @@ -39,14 +45,23 @@ export default async (fastify: FastifyInstance): Promise<ApolloServer> => {
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
];

const apollo = new ApolloServer({
const config: Config = {
dataSources,
typeDefs,
resolvers,
csrfPrevention: true,
cache: 'bounded',
plugins,
});
};

if (REDIS_URL) {
// Add Redis Cache if URL set
config.cache = new KeyvAdapter(new Keyv(REDIS_URL));
} else {
config.cache = 'bounded';
}

const apollo = new ApolloServer(config);

await apollo.start();

Expand Down
3 changes: 3 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export const SERVER_HOST = process.env.SERVER_HOST || '127.0.0.1';
// Mongo DB Connection
export const MONGO_URL = process.env.MONGO_URL;

// REDIS Connection
export const REDIS_URL = process.env.REDIS_URL;

// Google Maps API key
export const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;
17 changes: 17 additions & 0 deletions src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export type AddressInput = {
region?: InputMaybe<Scalars['String']>;
};

export enum CacheControlScope {
Private = 'PRIVATE',
Public = 'PUBLIC'
}

export type DeleteResult = {
__typename?: 'DeleteResult';
_id: Scalars['ID'];
Expand Down Expand Up @@ -396,6 +401,7 @@ export type ResolversTypes = {
Address: ResolverTypeWrapper<Address>;
AddressInput: AddressInput;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
CacheControlScope: CacheControlScope;
Date: ResolverTypeWrapper<Scalars['Date']>;
DeleteResult: ResolverTypeWrapper<DeleteResult>;
Event: ResolverTypeWrapper<Event>;
Expand Down Expand Up @@ -464,6 +470,14 @@ export type ResolversParentTypes = {
UpdateResult: UpdateResult;
};

export type CacheControlDirectiveArgs = {
inheritMaxAge?: Maybe<Scalars['Boolean']>;
maxAge?: Maybe<Scalars['Int']>;
scope?: Maybe<CacheControlScope>;
};

export type CacheControlDirectiveResolver<Result, Parent, ContextType = MyContext, Args = CacheControlDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;

export type AddressResolvers<ContextType = MyContext, ParentType extends ResolversParentTypes['Address'] = ResolversParentTypes['Address']> = {
city?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
country?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
Expand Down Expand Up @@ -614,3 +628,6 @@ export type Resolvers<ContextType = MyContext> = {
UpdateResult?: UpdateResultResolvers<ContextType>;
};

export type DirectiveResolvers<ContextType = MyContext> = {
cacheControl?: CacheControlDirectiveResolver<any, any, ContextType>;
};
6 changes: 3 additions & 3 deletions src/graphql/dataSources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
import { MongoDataSource } from 'apollo-datasource-mongodb';
import { MongoDataSource, Options } from 'apollo-datasource-mongodb';

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

Expand Down
32 changes: 24 additions & 8 deletions src/graphql/resolvers/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import orderBy from './order';
import * as Models from '../../db/models';
import * as Types from '../../generated/graphql';

// TTL for collection and document caches.
// Documents get removed from the cache automatically on update
// TODO: Add smart cache clearing and warming
const TTL_COLLECTIONS = 60; // A minute
const TTL_DOCUMENTS = 1209600; // 2 weeks

/**
* Possible arguments for collection filtering
*/
Expand Down Expand Up @@ -34,21 +40,24 @@ const getCollection =
// Find documents from data source
const dataSource =
ctx.dataSources[dataSourceName as keyof typeof ctx.dataSources];
let results = (await dataSource.findByFields(
args.filter || {},
)) as unknown[] as T[];
let results = (await dataSource.findByFields(args.filter || {}, {
ttl: TTL_COLLECTIONS,
})) as unknown[] as T[];

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

if ((args.order.by as string) in orderBy) {
// Check if we have a specific type of ordering function for this method
const sortFunc = orderBy[args.order.by as string as keyof typeof orderBy];
const sortFunc =
orderBy[args.order.by as string as keyof typeof orderBy];
results = results.sort(sortFunc<T>(dir));
} else {
// Otherwise we'll use the default sort method
results = results.sort(orderBy.default<T>(dir, args.order.by as string));
results = results.sort(
orderBy.default<T>(dir, args.order.by as string),
);
}
}

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

export default {
Expand All @@ -100,7 +110,9 @@ export default {
Organisation: {
// Find one to one
location: (event: Types.Event, _: never, ctx: MyContext) =>
ctx.dataSources.locations.findOneById(event.location),
ctx.dataSources.locations.findOneById(event.location, {
ttl: TTL_DOCUMENTS,
}),
// Find one to many
findEvents: (
organisation: Types.Organisation,
Expand All @@ -115,9 +127,13 @@ export default {
},
Event: {
organisation: (event: Types.Event, _: never, ctx: MyContext) =>
ctx.dataSources.organisations.findOneById(event.organisation),
ctx.dataSources.organisations.findOneById(event.organisation, {
ttl: TTL_DOCUMENTS,
}),
location: (event: Types.Event, _: never, ctx: MyContext) =>
ctx.dataSources.locations.findOneById(event.location),
ctx.dataSources.locations.findOneById(event.location, {
ttl: TTL_DOCUMENTS,
}),
},
Location: {
// Find one to many
Expand Down

0 comments on commit 9dc6655

Please sign in to comment.