Skip to content

Commit

Permalink
Merge pull request #29 from wayfair-incubator/mfaga-graphiql
Browse files Browse the repository at this point in the history
  • Loading branch information
mjfaga authored Jun 28, 2023
2 parents 430a36e + 5022ad4 commit 0ff146e
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 7 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ and this project adheres to

## [Unreleased]

## [v1.1.0] - 2023-06-28

### Added

- feat: add support for serving a GraphQL IDE from the mock servers `/graphql`
route. Three options are available:
- **DEFAULT** `GraphQLIDE.ApolloSandbox`: Serve's the
[Apollo Sandbox](https://www.apollographql.com/docs/graphos/explorer/sandbox/)
experience.
- `GraphQLIDE.GraphiQL`: Serve's the latest version of
[GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme)
- `GraphQLIDE.None`: Disables the GraphQL IDE experience

### Fixed

- fix: don't require sequenceId when executing a query
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [`async GraphqlMockingContext.operation`](#async-graphqlmockingcontextoperation)
- [`async GraphqlMockingContext.networkError`](#async-graphqlmockingcontextnetworkerror)
- [Mock server endpoints](#mock-server-endpoints)
- [GET `http:localhost:<port>/graphql`](#get-httplocalhostportgraphql)
- [POST `http:localhost:<port>/graphql`](#post-httplocalhostportgraphql)
- [POST `http:localhost:<port>/schema/register`](#post-httplocalhostportschemaregister)
- [POST `http:localhost:<port>/seed/operation`](#post-httplocalhostportseedoperation)
Expand Down Expand Up @@ -190,6 +191,19 @@ Registers a seed for a network error.

### Mock server endpoints

#### GET `http:localhost:<port>/graphql/:operationName?`

This endpoint supports serving a GraphQL IDE from the mock servers `/graphql`
route. Three options are available:

- **DEFAULT** `GraphQLIDE.ApolloSandbox`: Serve's the
[Apollo Sandbox](https://www.apollographql.com/docs/graphos/explorer/sandbox/)
experience.
- `GraphQLIDE.GraphiQL`: Serve's the latest version of
[GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme)
- `GraphQLIDE.None`: Disables the GraphQL IDE experience, and therefore this
endpoint

#### POST `http:localhost:<port>/graphql/:operationName?`

Send GraphQL queries to this endpoint to retrieve mocked data. Seeds are
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wayfair/gqmock",
"version": "1.0.0",
"version": "1.1.0",
"description": "GQMock - GraphQL Mocking Service",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
5 changes: 5 additions & 0 deletions src/GraphQLIDE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum GraphQLIDE {
None = 'None',
GraphiQL = 'GraphiQL',
ApolloSandbox = 'ApolloSandbox',
}
7 changes: 6 additions & 1 deletion src/GraphqlMockingService.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import GraphqlMockingContext from './GraphqlMockingContext';
import {GraphQLIDE} from './GraphQLIDE';
import MockServer from './MockServer';

type GraphqlMockingServiceOptions = {
port?: number;
subgraph?: boolean;
graphQLIDE?: GraphQLIDE;
};

export default class GraphqlMockingService {
readonly server;
private subgraph;
constructor(private options: GraphqlMockingServiceOptions = {}) {
this.server = new MockServer({port: options.port});
this.server = new MockServer({
port: options.port,
graphQLIDE: options.graphQLIDE,
});
this.subgraph = options.subgraph || false;
}

Expand Down
8 changes: 6 additions & 2 deletions src/MockServer.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import fetch, {Response} from 'node-fetch';
import createApp from './utilities/createApp';
import {Seed} from './seed/types';
import {GraphQLIDE} from './GraphQLIDE';

type MockServerOptions = {
port?: number;
graphQLIDE?: GraphQLIDE;
};

type SchemaRegistrationOptions = {
subgraph: boolean;
};

class MockServer {
readonly port;
readonly port: number;
readonly graphQLIDE: GraphQLIDE;
private appServer;
constructor(options: MockServerOptions) {
this.port = options.port || 5000;
this.graphQLIDE = options.graphQLIDE || GraphQLIDE.ApolloSandbox;
}

async start(): Promise<void> {
const app = createApp();
const app = createApp({graphQLIDE: this.graphQLIDE, port: this.port});

this.appServer = await app.listen({port: this.port}, () =>
console.log(
Expand Down
57 changes: 57 additions & 0 deletions src/__tests__/GraphqlMockingService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import fetch from 'node-fetch';
import {GraphQLIDE} from '../GraphQLIDE';
import GraphqlMockingService from '../GraphqlMockingService';

const schema = fs.readFileSync(
Expand Down Expand Up @@ -1113,5 +1114,61 @@ query itemsQuery { officeItems: items(type: "office") { ...commonItems3 } homeIt
secondOperationResult.data.productByName.id
);
});

it('returns the Apollo Sandbox GraphQL IDE by default', async () => {
await fetch(`http://localhost:${port}/graphql`, {
method: 'get',
})
.then((res) => {
return res.text();
})
.then((text) => {
expect(text).toContain('.apollographql.com');
})
.catch(() => {
throw new Error('Expected a 200 response');
});
});

it('returns the GraphiQL GraphQL IDE when configured', async () => {
// This is pretty gross. I plan on comming back to refactor this test suite.
await mockingService.stop();
mockingService = new GraphqlMockingService({
port,
graphQLIDE: GraphQLIDE.GraphiQL,
});
await mockingService.start();
await fetch(`http://localhost:${port}/graphql`, {
method: 'get',
})
.then((res) => {
return res.text();
})
.then((text) => {
expect(text).toContain('GraphiQL');
})
.catch(() => {
throw new Error('Expected a 200 response');
});
});

it('should return a 404 when no GraphQL IDE UI is configured', async () => {
// This is pretty gross. I plan on comming back to refactor this test suite.
await mockingService.stop();
mockingService = new GraphqlMockingService({
port,
graphQLIDE: GraphQLIDE.None,
});
await mockingService.start();
await fetch(`http://localhost:${port}/graphql`, {
method: 'get',
})
.then((res) => {
expect(res.status).toEqual(404);
})
.catch(() => {
throw new Error('Expected a 404 response');
});
});
});
});
48 changes: 48 additions & 0 deletions src/html/graphiql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// HTML based on https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn
export default `
<!DOCTYPE html>
<html lang="en">
<head>
<title>GraphiQL</title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
</head>
<body>
<div id="graphiql">Loading...</div>
<script
src="https://unpkg.com/graphiql/graphiql.min.js"
type="application/javascript"
></script>
<script>
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
root.render(
React.createElement(GraphiQL, {
fetcher: GraphiQL.createFetcher({
url: '/graphql',
}),
defaultEditorToolsVisibility: true,
})
);
</script>
</body>
</html>
`;
21 changes: 20 additions & 1 deletion src/routes/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,33 @@ import createRouter from '../utilities/createRouter';
import SeedManager from '../seed/SeedManager';
import ApolloServerManager from '../ApolloServerManager';
import {SeededOperationResponse} from '../seed/types';
import {GraphQLIDE} from '../GraphQLIDE';
import graphiqlHtml from '../html/graphiql';

const graphqlRoutes = (
{seedManager, apolloServerManager} = {
{graphQLIDE, port, seedManager, apolloServerManager} = {
graphQLIDE: GraphQLIDE.ApolloSandbox,
port: 5001,
seedManager: new SeedManager(),
apolloServerManager: new ApolloServerManager(),
}
): express.Router => {
const router = createRouter();

// If a GraphQL IDE is configured, set up a GET route to serve it
if (graphQLIDE === GraphQLIDE.ApolloSandbox) {
router.get('/', (_req, res) => {
res.redirect(
301,
`https://studio.apollographql.com/sandbox/explorer?endpoint=http://localhost:${port}/graphql`
);
});
} else if (graphQLIDE === GraphQLIDE.GraphiQL) {
router.get('/', (_req, res) => {
res.send(graphiqlHtml);
});
}

// Allow additional information in the /graphql route to allow for common patterns
// like putting operation names in the path for usage in APM modules
router.post('/:operationName?', async (req, res) => {
Expand Down
17 changes: 15 additions & 2 deletions src/utilities/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import seedRoutes from '../routes/seed';
import schemaRoutes from '../routes/schema';
import SeedManager from '../seed/SeedManager';
import ApolloServerManager from '../ApolloServerManager';
import {GraphQLIDE} from '../GraphQLIDE';

/**
* @param {object} root0 - The root object
* @param {GraphQLIDE} root0.graphQLIDE - The type of GraphQL IDE to use
* @param {number} root0.port - The port to run the server on
* @returns {express.Express} An express server instance
*/
export default function createApp(): express.Express {
export default function createApp({
graphQLIDE,
port,
}: {
graphQLIDE: GraphQLIDE;
port: number;
}): express.Express {
const app = express();
const seedManager = new SeedManager();
const apolloServerManager = new ApolloServerManager();
Expand All @@ -30,7 +40,10 @@ export default function createApp(): express.Express {
return res;
});

app.use('/graphql', graphqlRoutes({seedManager, apolloServerManager}));
app.use(
'/graphql',
graphqlRoutes({graphQLIDE, seedManager, apolloServerManager, port})
);
app.use('/schema', schemaRoutes({apolloServerManager}));
app.use('/seed', seedRoutes({seedManager}));

Expand Down

0 comments on commit 0ff146e

Please sign in to comment.