Skip to content

Commit

Permalink
feat: Migrate to Apollo Server 3.x; use Apollo Sandbox
Browse files Browse the repository at this point in the history
Replaces GraphQL Playground with Apollo Sandbox
  • Loading branch information
baumandm committed Jan 11, 2022
1 parent 66271c8 commit 8401726
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 104 deletions.
2 changes: 1 addition & 1 deletion packages/backend/env/.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ LOG_TIMESTAMPS=true
PORT=3001
ENCRYPTION_KEY=
PUBLIC_URL=
GRAPHQL_PLAYGROUND=true
APOLLO_SANDBOX=true

# GITHUB
GITHUB_URL=
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/env/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ LOG_LEVEL=silly
# Server
HOST=localhost

GRAPHQL_PLAYGROUND=true
APOLLO_SANDBOX=true

# Activities
ACTIVITIES_IGNORE_LOGIN=true
43 changes: 26 additions & 17 deletions packages/backend/src/controllers/graphql.v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
*/

import logger from '@iex/shared/logger';
import { ApolloServer } from 'apollo-server-express';
import {
ApolloServerPluginLandingPageLocalDefault,
ApolloServerPluginLandingPageProductionDefault
} from 'apollo-server-core';
import { ApolloServer, ApolloServerExpressConfig } from 'apollo-server-express';
import { Container } from 'typedi';

import { schema } from '../lib/graphql';

const server = new ApolloServer({
const apolloConfig: ApolloServerExpressConfig = {
schema,
context: ({ req }) => {
const requestId = req.id;
Expand All @@ -41,20 +45,25 @@ const server = new ApolloServer({

return context;
},
// Enables a web UI at http://localhost:3001/api/v1/graphql
playground: process.env.GRAPHQL_PLAYGROUND === 'true',
plugins: [
{
requestDidStart: () => ({
willSendResponse(requestContext) {
// Remove the request's scoped container
logger.silly('[GRAPHQL.V1] Terminating Container ' + requestContext.context.requestId);
Container.reset(requestContext.context.requestId);
}
})
}
],
uploads: false
});

export default server;
async requestDidStart() {
return {
async willSendResponse(requestContext) {
// Remove the request's scoped container
logger.silly('[GRAPHQL.V1] Terminating Container ' + requestContext.context.requestId);
Container.reset(requestContext.context.requestId);
}
};
}
},
process.env.APOLLO_SANDBOX === 'true'
? // Enables a web UI at http://localhost:3001/api/v1/graphql
ApolloServerPluginLandingPageLocalDefault({ footer: false })
: ApolloServerPluginLandingPageProductionDefault({ footer: false })
]
};

const graphQLServer = new ApolloServer(apolloConfig);

export { graphQLServer };
3 changes: 2 additions & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type { Server } from 'http';

import { bootstrap, defaultKnex } from './lib/db';
import { deployMappings } from './lib/elasticsearch';
import app from './server';
import { createServer } from './server';
import logger from '@iex/shared/logger';

// Safeguard to prevent the application from crashing.
Expand All @@ -59,6 +59,7 @@ const startup = async (): Promise<Server> => {

// Start Express server
logger.debug('[INDEX] Starting Express server');
const app = await createServer();
const server = app.listen(app.get('port'), () => {
logger.info(`IEX Server started in ${app.get('env')} mode on port: ${app.get('port')}`);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/middleware/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import helmet from 'helmet';
*/
export const security = helmet({
contentSecurityPolicy: {
reportOnly: false,
directives: {
'default-src': ["'self'"],
'base-uri': ["'self'"],
Expand Down Expand Up @@ -52,19 +51,20 @@ export const security = helmet({
'object-src': ["'self'", '*'],

// Unsafe-inline and unsafe-eval are required by React/webpack or something
// cdn.jsdelivr.net is used by GraphQL Playground
'script-src': [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
'https://*.google-analytics.com',
'https://cdn.jsdelivr.net',
'https://*.cdn.apollographql.com',
'*'
],
'script-src-attr': ["'none'"],

'style-src': ["'self'", 'https:', "'unsafe-inline'"]
}
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: false,
frameguard: false
});
67 changes: 36 additions & 31 deletions packages/backend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { graphqlUploadExpress } from 'graphql-upload';
import * as assetController from './controllers/assets.v1';
import * as avatarController from './controllers/avatars.v1';
import * as draftsController from './controllers/drafts.v1';
import graphQlController from './controllers/graphql.v1';
import { graphQLServer } from './controllers/graphql.v1';
import * as homeController from './controllers/home.v1';
import * as importController from './controllers/import.v1';
import * as insightsController from './controllers/insights.v1';
Expand All @@ -30,41 +30,46 @@ import { oktaAuthenticator } from './middleware/okta-authenticator';
import { requestId } from './middleware/request-id';
import { requireAuth } from './middleware/require-auth';

const router = Router();
const v1Router = Router();
export async function createRouter(): Promise<Router> {
const router = Router();
const v1Router = Router();

/*
* API routes
*/
// Wait for the graphql server to be ready
await graphQLServer.start();

router.get('/api/', homeController.getIndex);
router.use(requestId);
router.use('/api/v1', v1Router);
/*
* API routes
*/

/*
* API /v1/ Routes
*/
router.get('/api/', homeController.getIndex);
router.use(requestId);
router.use('/api/v1', v1Router);

/*
* API /v1/ Routes
*/

v1Router.use(
'/graphql',
oktaAuthenticator,
graphqlUploadExpress({ maxFileSize: 104_857_600, maxFiles: 50 }),
graphQlController.getMiddleware({ path: '/' })
);
v1Router.all('/webhook', webhookController.hook);
v1Router.use(
'/graphql',
oktaAuthenticator,
graphqlUploadExpress({ maxFileSize: 104_857_600, maxFiles: 50 }),
graphQLServer.getMiddleware({ path: '/' })
);
v1Router.all('/webhook', webhookController.hook);

// These routes require authentication
v1Router.get('/insights/search', oktaAuthenticator, requireAuth, insightsController.search);
v1Router.get('/insights/:namespace/:name', oktaAuthenticator, requireAuth, insightsController.getInsight);
// These routes require authentication
v1Router.get('/insights/search', oktaAuthenticator, requireAuth, insightsController.search);
v1Router.get('/insights/:namespace/:name', oktaAuthenticator, requireAuth, insightsController.getInsight);

// These routes do not require authentication
v1Router.get('/avatars/:key', avatarController.getAvatar);
v1Router.head('/insights/:namespace/:name/assets/:filepath*', insightsController.headInsightFile);
v1Router.get('/insights/:namespace/:name/assets/:filepath*', insightsController.getInsightFile);
v1Router.get('/drafts/:draftKey/assets/:attachmentKey', draftsController.getDraftAttachment);
// These routes do not require authentication
v1Router.get('/avatars/:key', avatarController.getAvatar);
v1Router.head('/insights/:namespace/:name/assets/:filepath*', insightsController.headInsightFile);
v1Router.get('/insights/:namespace/:name/assets/:filepath*', insightsController.getInsightFile);
v1Router.get('/drafts/:draftKey/assets/:attachmentKey', draftsController.getDraftAttachment);

v1Router.get('/changelog', assetController.getChangelog);
v1Router.get('/markdown', assetController.getMarkdown);
v1Router.all('/import', cors({ origin: true }), importController.importToDraft);
v1Router.get('/changelog', assetController.getChangelog);
v1Router.get('/markdown', assetController.getMarkdown);
v1Router.all('/import', cors({ origin: true }), importController.importToDraft);

export default router;
return router;
}
92 changes: 47 additions & 45 deletions packages/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,59 +26,61 @@ import { StatusCodes } from 'http-status-codes';
import morgan from 'morgan';

import { security } from './middleware/security';
import router from './router';
import { createRouter } from './router';

// Init express
const app = express();
export async function createServer(): Promise<express.Express> {
// Init express
const app = express();

app.set('port', Number(process.env.PORT || 3000));
app.set('env', process.env.NODE_ENV);
app.set('port', Number(process.env.PORT || 3000));
app.set('env', process.env.NODE_ENV);

// Show routes called in console during development
switch (process.env.NODE_ENV) {
case 'development':
logger.info('Loading development middleware');
app.use(security);
break;
case 'production':
logger.info('Loading production middleware');
app.use(security);
break;
default:
logger.info('Actually ' + process.env.NODE_ENV);
}
// Show routes called in console during development
switch (process.env.NODE_ENV) {
case 'development':
logger.info('Loading development middleware');
app.use(security);
break;
case 'production':
logger.info('Loading production middleware');
app.use(security);
break;
default:
logger.info('Actually ' + process.env.NODE_ENV);
}

app.use(compression());
app.use(express.json({ limit: '20mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
app.use(express.json({ limit: '20mb' }));
app.use(express.urlencoded({ extended: true }));

if (process.env.LOG_REQUESTS === 'true') {
app.use(morgan('dev'));
}
if (process.env.LOG_REQUESTS === 'true') {
app.use(morgan('dev'));
}

// Add APIs
app.use('/', router);
// Add APIs
const router = await createRouter();
app.use('/', router);

// Print API errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error(err.message, err);
return res.status(StatusCodes.BAD_REQUEST).json({
error: err.message
// Print API errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error(err.message, err);
return res.status(StatusCodes.BAD_REQUEST).json({
error: err.message
});
});
});

// Serve front-end
const frontendDir = path.join(__dirname, '../../../frontend/build');
// Serve front-end
const frontendDir = path.join(__dirname, '../../../frontend/build');

app.use(
express.static(frontendDir, {
redirect: false
})
);
app.get(/^(?!\/api).+/, (req: Request, res: Response) => {
res.sendFile('index.html', { root: frontendDir });
});
app.use(
express.static(frontendDir, {
redirect: false
})
);
app.get(/^(?!\/api).+/, (req: Request, res: Response) => {
res.sendFile('index.html', { root: frontendDir });
});

// Export express instance
export default app;
return app;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ export const Footer = (props) => {
const bgColor = useColorModeValue('unset', 'gray.700');
const color = useColorModeValue('gray.700', 'gray.200');

let graphQlPlaygroundLink;
let apolloSandboxLink;
if (window.location.origin === 'http://localhost:3000') {
graphQlPlaygroundLink = 'http://localhost:3001/api/v1/graphql';
apolloSandboxLink = 'http://localhost:3001/api/v1/graphql';
} else {
graphQlPlaygroundLink = '/api/v1/graphql';
apolloSandboxLink = '/api/v1/graphql';
}

return (
Expand Down Expand Up @@ -64,10 +64,10 @@ export const Footer = (props) => {
</FooterItem>
</Link>
)}
<Link href={graphQlPlaygroundLink} isExternal={true}>
<Link href={apolloSandboxLink} isExternal={true}>
<FooterItem>
<Icon as={iconFactory('graphql')} />
<Text>GraphQL Playground</Text>
<Text>Apollo Sandbox</Text>
</FooterItem>
</Link>
</HStack>
Expand Down

0 comments on commit 8401726

Please sign in to comment.