Skip to main content

Migrating Apollo Server (express) to v3

Apollo Server v3 has been available for a moment now and keeping the version 2 is an open security issue with our server backends.

What's new ?

Quite a few breaking changes occurred with the upgrade to v3. Mainly, Apollo sever get rid of heavy load that is used by 2% of the projects. Apollo removes naive the schemaDirectives option.

  • Apollo Server 3 supports Node.js 12 and later. Apollo Server 2 supports back to Node.js 6. The new package is lighter and faster
  • Apollo Server 3 supports graphql v15.3.0 and later. Apollo Server 2 supported graphql v0.12 through v15.
  • Removing support for graphql-upload and the subscriptions-transport-ws
  • Discouraging developers to upload using GraphQL

Read more on this link

info

You can reenable all of these integrations as they exist in Apollo Server 2.

Migrating SchemaDirectives out of Apollo

The native support of schemaDirectives is completely removed.

We'll be using @graphql-tools/schema is to pass the generated schema to your implementation of these custom directives.

Update and install the new packages

yarn upgrade graphql@latest apollo-server-express@latest
yarn add @graphql-tools/schema @graphql-tools/utils

Update the directive files

The highlighted line are the most important, the rest is boiler plate. A big difference is now the typeDef is moved from the main schema file to directive file.

import { ApolloError, gql } from "apollo-server-express";
import { GraphQLSchema, defaultFieldResolver } from "graphql";
import { decode } from "@exiry/utils/jwt.utils";
import { User } from "@main/plugins/users/models";
import type { UserContext } from "@exiry/core/@types/context";
import { getDirective, MapperKind, mapSchema } from "@graphql-tools/utils";

export const directiveName = "isAuthenticated";

export function typeDef() {
return gql`
directive @isAuthenticated on OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION
`;
}

export function transformer(schema: GraphQLSchema) {
const typeDirectiveArgumentMaps: Record<string, any> = {};
return mapSchema(schema, {
[MapperKind.TYPE]: (type) => {
const authDirective = getDirective(schema, type, directiveName)?.[0];
if (authDirective) {
typeDirectiveArgumentMaps[type.name] = authDirective;
}
return undefined;
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const authDirective =
getDirective(schema, fieldConfig, directiveName)?.[0] ??
typeDirectiveArgumentMaps[typeName];
if (authDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
const ctx = await renderCtx(context);
return resolve(source, args, ctx, info);
};
return fieldConfig;
}
},
});
}

type Context = { [k: string]: any };
interface ContextWithUser extends Context {
user: UserContext;
}

export async function renderCtx<T extends Context>(
context: T
): Promise<ContextWithUser> {
const _user = await decode(context.req.headers.authorization);
if (!_user) {
const error = "You are not authenticated.";
throw new ApolloError(error, "UNAUTHENTICATED");
}
const user = await User.findOne({
attributes: ["id", "isActive", "email", "firstName", "lastName"],
where: { id: _user.id },
});
if (!user) {
const error = "User is not found in our Database.";
throw new ApolloError(error, "UNAUTHENTICATED");
}
if (!user.isActive) {
const error = "User is disabled.";
throw new ApolloError(error, "USER_DISABLED");
}
return {
...context,
user: {
id: user.id,
email: user.email || null,
fullName: user.firstName + " " + user.lastName,
},
};
}

Update the directive index files

The index.ts file of the Exiry's /src/core/directives directory needs to be updated too:

index.ts
import * as isAuthenticated from "./isAuthenticated";
import * as hasAccess from "./hasAccess";
import * as ip from "./ip";
import * as ua from "./ua";

export default {
typeDef: [isAuthenticated.typeDef, hasAccess.typeDef, ip.typeDef, ua.typeDef],
transformer: [
isAuthenticated.transformer,
hasAccess.transformer,
ip.transformer,
ua.transformer,
],
};

Update Exiry main server

Updating the Exiry's server Factory

The index.ts of Exiry's main folder needs to be updated according to Apollo Server v3 requirements.

core/index.ts
...

import mainSchema from "./main-schema.gql";
- import schemaDirectives from "./directives";
+ import directives from "./directives";
import scalarsResolvers from "./scalars";
+ import { makeExecutableSchema } from "@graphql-tools/schema";
+ import { ApolloServerPluginLandingPageDisabled } from "apollo-server-core";

...

export default async function server(port: number | string) {
const app = express();

+ // Create the schema
+ let schema = makeExecutableSchema({
+ typeDefs: [
+ mainSchema,
+ ...directives.typeDef,
+ ...plugins.schema,
+ ...modules.schema,
+ ],
+ resolvers: [scalarsResolvers, ...plugins.resolvers, ...modules.resolvers],
+ });
+
+ // Apply the directive to the schema
+ for (const transformer of directives.transformer) {
+ if (typeof transformer !== "function") continue;
+ schema = transformer(schema);
+ }

const server = new ApolloServer({
+ schema,
+ csrfPrevention: true,
+ plugins: [ApolloServerPluginLandingPageDisabled()],
- typeDefs: [mainSchema, ...plugins.schema, ...modules.schema],
- resolvers: [scalarsResolvers, ...plugins.resolvers, ...modules.resolvers],
- schemaDirectives,
- introspection: process.env.NODE_ENV !== "production",
- playground: process.env.NODE_ENV !== "production",
- uploads: {
- maxFileSize: 10000000, // 10 MB
- maxFiles: 20,
- },
context: (context) => {
// Initiate the context to null
return {
...context,
user: null,
privilege: null,
ip: null,
agent: null,
};
},
});

+ // Start the Apollo Server before going further
+ await server.start();

...

// Apply Middleware to the graphQl server
- server.applyMiddleware({ app, cors: true });
+ server.applyMiddleware({
+ app,
+ cors: true,
+ bodyParserConfig: true,
+ path: "/graphql",
+ });

...

return httpServer.listen(port, () => {
- const { graphqlPath, subscriptionsPath } = server;
+ const { graphqlPath } = server;
logger.info(`Server ready at */:${port}${graphqlPath}`);
- logger.info(`Subscriptions ready at ws://*/:${port}${subscriptionsPath}`);
});
}

Updating the Exiry's main schema

The directive definition is moved to their own respective files. We remove them from the main schema file:

main-schema.gql.ts
import { gql } from "apollo-server-express";

// Handel different Apollo Schemas and Resolvers
const schema = gql`
- directive @isAuthenticated on OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION
- directive @hasAccess(
- slug: String
- scope: String
- own: Boolean
- ) on OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION
- """
- Injects the user IP into the Context
- """
- directive @ip on OBJECT | FIELD_DEFINITION
- """
- Injects the User Agent into the Context
- see [express-useragent](https://www.npmjs.com/package/express-useragent) for details
- """
- directive @ua on OBJECT | FIELD_DEFINITION
-
"""
Date - Tekru custom scalar
Convert outgoing Date to integer for JSON