Skip to content

Commit

Permalink
Merge branch '#3' into [email protected]
Browse files Browse the repository at this point in the history
# Conflicts:
#	bin/mock-config-server.ts
#	package.json
#	src/utils/helpers/isPlainObject.ts
#	src/utils/types/configs.ts
#	yarn.lock
  • Loading branch information
RiceWithMeat committed Mar 4, 2023
2 parents 53c1707 + c26a3f8 commit 8fd39a0
Show file tree
Hide file tree
Showing 36 changed files with 649 additions and 56 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.17",
"@types/flat": "^5.0.2",
"ansi-colors": "^4.1.3",
"body-parser": "^1.20.0",
"ejs": "^3.1.8",
"esbuild": "^0.17.8",
"express": "^4.18.1",
"flat": "^5.0.2",
Expand Down
8 changes: 2 additions & 6 deletions src/graphql/createGraphQLRoutes/createGraphQLRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ describe('createGraphQLRoutes', () => {
mockServerConfig.interceptors
);

const restBaseUrl = path.join(
const graphqlBaseUrl = path.join(
mockServerConfig.baseUrl ?? '/',
mockServerConfig.graphql?.baseUrl ?? '/'
);

server.use(express.json());
server.use(restBaseUrl, routerWithRoutes);
server.use(graphqlBaseUrl, routerWithRoutes);
return server;
};

Expand Down Expand Up @@ -224,14 +224,12 @@ describe('createGraphQLRoutes', () => {
.set({ key2: 'value2' });

expect(postResponse.statusCode).toBe(404);
expect(postResponse.body).toBe('No data for POST:/');

const getResponse = await request(server).get('/').set({ key2: 'value2' }).query({
query: 'query GetUsers { users { name } }'
});

expect(getResponse.statusCode).toBe(404);
expect(getResponse.body).toBe('No data for GET:/');
});

test('Should compare non plain object variables by full equal behavior', async () => {
Expand Down Expand Up @@ -292,7 +290,6 @@ describe('createGraphQLRoutes', () => {
});

expect(failedPostResponse.statusCode).toBe(404);
expect(failedPostResponse.body).toBe('No data for POST:/');

const failedGetResponse = await request(server)
.get('/')
Expand All @@ -304,7 +301,6 @@ describe('createGraphQLRoutes', () => {
});

expect(failedGetResponse.statusCode).toBe(404);
expect(failedGetResponse.body).toBe('No data for GET:/');
});

test('Should compare plain object variables by "includes" behavior', async () => {
Expand Down
21 changes: 7 additions & 14 deletions src/graphql/createGraphQLRoutes/createGraphQLRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IRouter, Request, Response } from 'express';
import { IRouter, NextFunction, Request, Response } from 'express';

import { isEntityValuesEqual } from '../../configs/isEntitiesEqual/isEntityValuesEqual';
import { callRequestInterceptors } from '../../routes/callRequestInterceptors/callRequestInterceptors';
Expand All @@ -21,24 +21,21 @@ export const createGraphQLRoutes = (
) => {
const preparedGraphQLRequestConfig = prepareGraphQLRequestConfigs(configs);

const graphqlMiddleware = (request: Request, response: Response) => {
const graphqlMiddleware = (request: Request, response: Response, next: NextFunction) => {
const graphQLInput = getGraphQLInput(request);

if (!graphQLInput || !graphQLInput.query) {
return response
.status(404)
.json(`No data for ${request.method}:${request.baseUrl}${request.path}`);
return response.status(400).json('Query is missing, you must pass a valid GraphQL query');
}

const query = parseQuery(graphQLInput.query);

if (!query) {
return response.status(400).json(`Query is invalid, you must use a valid GraphQL query`);
return response.status(400).json('Query is invalid, you must use a valid GraphQL query');
}

if (!query.operationName || !query.operationType) {
return response
.status(404)
.status(400)
.json(
`You should to specify operationName and operationType for ${request.method}:${request.baseUrl}${request.path}`
);
Expand All @@ -59,9 +56,7 @@ export const createGraphQLRoutes = (
});

if (!matchedRequestConfig) {
return response
.status(404)
.json(`No data for ${request.method}:${request.baseUrl}${request.path}`);
return next();
}

callRequestInterceptors({
Expand All @@ -86,9 +81,7 @@ export const createGraphQLRoutes = (
});

if (!matchedRouteConfig) {
return response
.status(404)
.json(`No data for ${request.method}:${request.baseUrl}${request.path}`);
return next();
}

const data = callResponseInterceptors({
Expand Down
10 changes: 5 additions & 5 deletions src/graphql/getGraphQLInput/getGraphQLInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Request } from 'express';
import { getGraphQLInput } from './getGraphQLInput';

describe('getGraphQLInput', () => {
test('Should get right graphQL input from GET request', async () => {
test('Should get right graphQL input from GET request', () => {
const mockRequest = {
method: 'GET',
query: {
Expand All @@ -20,7 +20,7 @@ describe('getGraphQLInput', () => {
});
});

test('Should get right graphQL input from GET request with empty variables', async () => {
test('Should get right graphQL input from GET request with empty variables', () => {
const mockRequest = {
method: 'GET',
query: {
Expand All @@ -36,7 +36,7 @@ describe('getGraphQLInput', () => {
});
});

test('Should get right graphQL input from POST request', async () => {
test('Should get right graphQL input from POST request', () => {
const mockRequest = {
method: 'POST',
body: {
Expand All @@ -53,7 +53,7 @@ describe('getGraphQLInput', () => {
});
});

test('Should get right graphQL input from POST with empty variables', async () => {
test('Should get right graphQL input from POST with empty variables', () => {
const mockRequest = {
method: 'POST',
body: {
Expand All @@ -69,7 +69,7 @@ describe('getGraphQLInput', () => {
});
});

test('Should get error if request is not GET or POST', async () => {
test('Should get error if request is not GET or POST', () => {
const deleteMockRequest = {
method: 'DELETE',
query: {
Expand Down
11 changes: 11 additions & 0 deletions src/graphql/parseGraphQLRequest/parseGraphQLRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Request } from 'express';

import { getGraphQLInput } from '../getGraphQLInput/getGraphQLInput';
import { parseQuery } from '../parseQuery/parseQuery';

export const parseGraphQLRequest = (request: Request): ReturnType<typeof parseQuery> => {
const graphQLInput = getGraphQLInput(request);
if (!graphQLInput.query) return null;

return parseQuery(graphQLInput.query);
};
6 changes: 3 additions & 3 deletions src/graphql/parseQuery/parseQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parseQuery } from './parseQuery';

describe('parseQuery', () => {
test('Should parse graphQL query', async () => {
test('Should parse graphQL query', () => {
const parsedQuery = parseQuery('query GetCharacters { characters { name } }');

expect(parsedQuery).toStrictEqual({
Expand All @@ -10,7 +10,7 @@ describe('parseQuery', () => {
});
});

test('Should parse graphQL mutation', async () => {
test('Should parse graphQL mutation', () => {
const parsedQuery = parseQuery(
'mutation CreateCharacters($name: String!) { createCharacters(name: $name) { name } }'
);
Expand All @@ -21,7 +21,7 @@ describe('parseQuery', () => {
});
});

test('Should parse graphQL query with empty operationName', async () => {
test('Should parse graphQL query with empty operationName', () => {
const parsedQuery = parseQuery('query { characters { name } }');

expect(parsedQuery).toStrictEqual({
Expand Down
120 changes: 120 additions & 0 deletions src/notFound/notFoundMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import express from 'express';
import path from 'path';
import request from 'supertest';

import { createGraphQLRoutes } from '../graphql/createGraphQLRoutes/createGraphQLRoutes';
import { createRestRoutes } from '../rest/createRestRoutes/createRestRoutes';
import type { MockServerConfig } from '../utils/types';

import { notFoundMiddleware } from './notFoundMiddleware';

describe('notFoundMiddleware', () => {
const baseUrl: MockServerConfig['baseUrl'] = '/base';

const rest: MockServerConfig['rest'] = {
baseUrl: '/rest',
configs: [
{
path: '/posts',
method: 'get',
routes: [{ data: {} }]
},
{
path: '/posts/:postId',
method: 'get',
routes: [{ data: {}, entities: { params: { postId: 1 } } }]
},

{
path: '/developers',
method: 'get',
routes: [{ data: {} }]
},
{
path: '/developers/:developerId',
method: 'get',
routes: [{ data: {}, entities: { params: { developerId: 1 } } }]
}
]
};

const graphql: MockServerConfig['graphql'] = {
baseUrl: '/graphql',
configs: [
{
operationName: 'GetPosts',
operationType: 'query',
routes: [{ data: {} }]
},
{
operationName: 'GetDevelopers',
operationType: 'query',
routes: [{ data: {} }]
}
]
};

const createServer = (
mockServerConfig: Pick<MockServerConfig, 'rest' | 'graphql' | 'interceptors' | 'baseUrl'>
) => {
const { baseUrl, rest, interceptors, graphql } = mockServerConfig;

const server = express();

const serverBaseUrl = baseUrl ?? '/';

const restBaseUrl = path.join(serverBaseUrl, rest?.baseUrl ?? '/');
const routerWithRestRoutes = createRestRoutes(
express.Router(),
rest?.configs ?? [],
interceptors
);
server.use(restBaseUrl, routerWithRestRoutes);

const graphqlBaseUrl = path.join(serverBaseUrl, graphql?.baseUrl ?? '/');
const routerWithGraphqlRoutes = createGraphQLRoutes(
express.Router(),
graphql?.configs ?? [],
interceptors
);
server.use(graphqlBaseUrl, routerWithGraphqlRoutes);

server.set('view engine', 'ejs');
server.use(express.json());

notFoundMiddleware({
server,
mockServerConfig
});

return server;
};

test('Should send correct REST suggestions', async () => {
const server = createServer({
baseUrl,
rest,
graphql
});

const response = await request(server).get('/bas/rst/pstss');

expect(response.statusCode).toBe(404);
expect(response.text).toContain('<h3>REST</h3>');
expect(response.text).toContain('/base/rest/posts');
});

test('Should send correct GraphQL suggestions', async () => {
const server = createServer({
baseUrl,
rest,
graphql
});

const response = await request(server).get('/bse/graql?query=query posts { posts }');

expect(response.statusCode).toBe(404);
expect(response.text).toContain('<h3>GraphQL</h3>');
expect(response.text).toContain('/base/graphql/GetPosts');
});
});
64 changes: 64 additions & 0 deletions src/notFound/notFoundMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Express, Request, Response } from 'express';

import { parseGraphQLRequest } from '../graphql/parseGraphQLRequest/parseGraphQLRequest';
import type { MockServerConfig, RestMethod } from '../utils/types';

import { getGraphqlUrlSuggestions, getRestUrlSuggestions } from './urlSuggestions';

interface NotFoundMiddlewareParams {
server: Express;
mockServerConfig: Pick<MockServerConfig, 'baseUrl' | 'rest' | 'graphql'>;
}

export const notFoundMiddleware = ({ server, mockServerConfig }: NotFoundMiddlewareParams) => {
const { baseUrl: serverBaseUrl, rest, graphql } = mockServerConfig;

const operationNames = graphql?.configs.map(({ operationName }) => operationName) ?? [];
const graphqlPatternUrlMeaningfulStrings = Array.from(
operationNames.reduce((acc, operationName) => {
if (typeof operationName === 'string')
acc.add(`${serverBaseUrl}${graphql?.baseUrl}/${operationName}`);
return acc;
}, new Set<string>())
);

const restPaths = rest?.configs.map(({ path }) => path) ?? [];
const patternUrls = Array.from(
restPaths.reduce((acc, patternPath) => {
if (typeof patternPath === 'string')
acc.add(`${serverBaseUrl}${rest?.baseUrl}${patternPath}`);
return acc;
}, new Set<string>())
);

server.use((request: Request, response: Response) => {
const url = new URL(`${request.protocol}://${request.get('host')}${request.originalUrl}`);

let graphqlUrlSuggestions: string[] = [];
if (graphql) {
const graphqlQuery = parseGraphQLRequest(request);

if (graphqlQuery) {
graphqlUrlSuggestions = getGraphqlUrlSuggestions({
url,
graphqlPatternUrlMeaningfulStrings
});
}
}

let restUrlSuggestions: string[] = [];
if (rest) {
restUrlSuggestions = getRestUrlSuggestions({
url,
patternUrls
});
}

response.status(404).render('notFound', {
requestMethod: request.method as RestMethod,
url: `${url.pathname}${url.search}`,
restUrlSuggestions,
graphqlUrlSuggestions
});
});
};
Loading

0 comments on commit 8fd39a0

Please sign in to comment.