Skip to content

Commit

Permalink
add caching to apollo queries
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinzwang committed Apr 15, 2024
1 parent e77c8c4 commit 1ae88cd
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 18 deletions.
105 changes: 105 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"dependencies": {
"@apollo/server": "^4.1.0",
"@apollo/server-plugin-response-cache": "^4.1.3",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/utils": "^10.0.7",
"axios": "^1.5.1",
Expand All @@ -66,6 +67,7 @@
"mongoose": "^7.6.3",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"redis": "^4.6.13",
"reflect-metadata": "^0.1.13"
},
"overrides": {
Expand Down
36 changes: 34 additions & 2 deletions backend/src/bootstrap/loaders/apollo.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
import { ApolloServer } from "@apollo/server";
import { buildSchema } from "../graphql/buildSchema";
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
import { KeyValueCache, KeyValueCacheSetOptions } from '@apollo/utils.keyvaluecache';
import responseCachePlugin from '@apollo/server-plugin-response-cache';
import { RedisClientType, createClient } from 'redis';

export default async () => {
class RedisCache implements KeyValueCache {
client: RedisClientType;
prefix: string = 'apollo-cache:';

constructor(client: RedisClientType) {
this.client = client;
}

async get(key: string) {
return await this.client.get(this.prefix + key) ?? undefined;
}

async set(key: string, value: string, _?: KeyValueCacheSetOptions | undefined) {
// ttl options are intentionally ignored because we will invalidate cache in update script
await this.client.set(this.prefix + key, value)
}

async delete(key: string) {
return await this.client.del(this.prefix + key) === 1;
}

}

export default async (redis: RedisClientType) => {
const schema = buildSchema();

const server = new ApolloServer({
schema,
plugins: [ApolloServerPluginLandingPageLocalDefault({ includeCookies: true })],
plugins: [
ApolloServerPluginLandingPageLocalDefault({ includeCookies: true }),
ApolloServerPluginCacheControl({ calculateHttpHeaders: false }),
responseCachePlugin(),
],
introspection: true, // TODO(production): disable introspection upon final deployment
cache: new RedisCache(redis),
});
await server.start();

Expand Down
7 changes: 6 additions & 1 deletion backend/src/bootstrap/loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { config } from "../../config";
import apolloLoader from "./apollo";
import expressLoader from "./express";
import mongooseLoader from "./mongoose";
import redisLoader from "./redis";

export default async (root: Application): Promise<void> => {
const app = Router() as Application;
Expand All @@ -13,9 +14,13 @@ export default async (root: Application): Promise<void> => {
console.log("Booting up mongo...");
await mongooseLoader();

// connect to redis
console.log("Booting up redis...");
const redis = await redisLoader();

// load apollo server config. must be loaded before express
console.log("Loading apollo...");
const server = await apolloLoader();
const server = await apolloLoader(redis);

// load everything related to express. depends on apollo
console.log("Loading express...");
Expand Down
6 changes: 6 additions & 0 deletions backend/src/bootstrap/loaders/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RedisClientType, createClient } from "redis"
import { config } from "../../config";

export default async (): Promise<RedisClientType> => {
return await createClient({ url: config.redisUri }).connect() as RedisClientType;
}
2 changes: 2 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface Config {
SESSION_SECRET: string;
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
redisUri: string;
}

// All your secrets, keys go here
Expand All @@ -51,4 +52,5 @@ export const config: Config = {
SESSION_SECRET: env("SESSION_SECRET"),
GOOGLE_CLIENT_ID: env("GOOGLE_CLIENT_ID"),
GOOGLE_CLIENT_SECRET: env("GOOGLE_CLIENT_SECRET"),
redisUri: env("REDIS_URI"),
};
15 changes: 15 additions & 0 deletions backend/src/generated-types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type Scalars = {
JSONObject: any;
};

export type CacheControlScope =
| 'PRIVATE'
| 'PUBLIC';

export type CatalogClass = {
__typename?: 'CatalogClass';
description?: Maybe<Scalars['String']>;
Expand Down Expand Up @@ -208,6 +212,7 @@ export type Query = {
*/
courseList?: Maybe<Array<Maybe<CourseListItem>>>;
grade?: Maybe<Grade>;
/** @deprecated test */
ping: Scalars['String'];
/** Takes in a schedule's ObjectID and returns a specific schedule. */
scheduleByID?: Maybe<Schedule>;
Expand Down Expand Up @@ -453,6 +458,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
CacheControlScope: CacheControlScope;
CatalogClass: ResolverTypeWrapper<CatalogClass>;
CatalogItem: ResolverTypeWrapper<CatalogItem>;
Class: ResolverTypeWrapper<Class>;
Expand Down Expand Up @@ -523,6 +529,14 @@ export type AuthDirectiveArgs = { };

export type AuthDirectiveResolver<Result, Parent, ContextType = any, Args = AuthDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;

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

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

export type CatalogClassResolvers<ContextType = any, ParentType extends ResolversParentTypes['CatalogClass'] = ResolversParentTypes['CatalogClass']> = {
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
enrollCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Expand Down Expand Up @@ -757,6 +771,7 @@ export type Resolvers<ContextType = any> = {

export type DirectiveResolvers<ContextType = any> = {
auth?: AuthDirectiveResolver<any, any, ContextType>;
cacheControl?: CacheControlDirectiveResolver<any, any, ContextType>;
};

export type IsoDate = Scalars["ISODate"];
Expand Down
18 changes: 9 additions & 9 deletions backend/src/modules/catalog/typedefs/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Query {
"""
Info shared between Classes within and across semesters.
"""
type Course {
type Course @cacheControl(maxAge: 1) {
classes(term: TermInput): [Class]!
crossListing: [Course]
sections(term: TermInput, primary: Boolean): [Section]!
Expand All @@ -48,7 +48,7 @@ type Course {
"""
Data for a specific class in a specific semester. There may be more than one Class for a given Course in a given semester.
"""
type Class {
type Class @cacheControl(maxAge: 1) {
course: Course!
primarySection: Section!
sections: [Section]!
Expand All @@ -74,7 +74,7 @@ type Class {
"""
Sections are each associated with one Class.
"""
type Section {
type Section @cacheControl(maxAge: 1) {
class: Class!
course: Course!
enrollmentHistory: [EnrollmentDay]
Expand All @@ -100,30 +100,30 @@ type Section {
lastUpdated: ISODate!
}
type Instructor {
type Instructor @cacheControl(maxAge: 1) {
familyName: String!
givenName: String!
}
type EnrollmentDay {
type EnrollmentDay @cacheControl(maxAge: 1) {
enrollCount: Int!
enrollMax: Int!
waitlistCount: Int!
waitlistMax: Int!
}
type CatalogItem {
type CatalogItem @cacheControl(maxAge: 1) {
subject: String!
number: String!
title: String!
title: String! @cacheControl(maxAge: 30)
description: String!
classes: [CatalogClass]!
gradeAverage: Float
lastUpdated: ISODate!
}
type CatalogClass {
type CatalogClass @cacheControl(maxAge: 1) {
number: String!
title: String
description: String
Expand All @@ -135,7 +135,7 @@ type CatalogClass {
lastUpdated: ISODate!
}
type CourseListItem {
type CourseListItem @cacheControl(maxAge: 1) {
subject: String!
number: String!
}
Expand Down
Loading

0 comments on commit 1ae88cd

Please sign in to comment.