Skip to content

Commit 98f3540

Browse files
authored
Merge pull request #269 from siberiacancode/fix/params
Fix/params
2 parents e09a864 + 38f5aad commit 98f3540

35 files changed

Lines changed: 746 additions & 152 deletions

File tree

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"dist"
3838
],
3939
"engines": {
40-
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
40+
"node": "^16.0.0 || >=17.0.0"
4141
},
4242
"scripts": {
4343
"prepublishOnly": "git diff --exit-code",

packages/server/src/core/graphql/createGraphQLRoute/createGraphQLRoute.test.ts

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
import { urlJoin } from '@/utils/helpers';
1515

1616
import { createGraphQLRoute } from './createGraphQLRoute';
17-
import { calculateGraphQLRouteConfigWeight } from './helpers';
17+
import { calculateGraphQLRouteConfigWeight, prepareGraphQLRequestArtifacts } from './helpers';
1818

1919
const createServer = (
2020
mockServerConfig: Pick<BaseServerConfig, 'baseUrl' | 'interceptors'> & {
@@ -33,13 +33,10 @@ const createServer = (
3333

3434
createGraphQLRoute({
3535
server,
36-
graphQLRequestArtifacts: graphql.configs
37-
.reduce((acc, config) => {
36+
graphQLRequestArtifacts: prepareGraphQLRequestArtifacts(
37+
graphql.configs.reduce((acc, config) => {
3838
config.routes.forEach((route) => {
3939
acc.push({
40-
key: `${baseUrl}${graphql.baseUrl}/${config.operationType}/${
41-
'operationName' in config ? config.operationName : config.query
42-
}`,
4340
baseUrl: urlJoin(baseUrl ?? '/', graphql?.baseUrl ?? '/') as BaseUrl,
4441
operationType: config.operationType,
4542
operationName: 'operationName' in config ? config.operationName : undefined,
@@ -59,7 +56,7 @@ const createServer = (
5956

6057
return acc;
6158
}, [] as GraphQLRequestArtifact[])
62-
.toSorted((first, second) => second.weight - first.weight)
59+
)
6360
});
6461

6562
return server;
@@ -180,49 +177,6 @@ describe('createGraphQLRoute: routing', () => {
180177
expect(getResponse.body).toStrictEqual({ name: 'John', surname: 'Doe' });
181178
});
182179

183-
it('Should return 400 and description text for invalid query', async () => {
184-
const server = createServer({
185-
graphql: {
186-
configs: [
187-
{
188-
operationName: 'GetUsers',
189-
operationType: 'query',
190-
routes: [
191-
{
192-
entities: {
193-
headers: {
194-
key1: 'value1',
195-
key2: 'value2'
196-
},
197-
query: {
198-
key1: 'value1'
199-
}
200-
},
201-
data: { name: 'John', surname: 'Doe' }
202-
}
203-
]
204-
}
205-
]
206-
}
207-
});
208-
209-
const postResponse = await request(server).post('/').send({ query: 'invalid query' });
210-
211-
expect(postResponse.statusCode).toBe(400);
212-
expect(postResponse.body).toStrictEqual({
213-
message: 'Query is invalid, you must use a valid GraphQL query'
214-
});
215-
216-
const getResponse = await request(server).get('/').query({
217-
query: 'invalid query'
218-
});
219-
220-
expect(postResponse.statusCode).toBe(400);
221-
expect(getResponse.body).toStrictEqual({
222-
message: 'Query is invalid, you must use a valid GraphQL query'
223-
});
224-
});
225-
226180
it('Should return 404 for no matched request configs', async () => {
227181
const server = createServer({
228182
graphql: {

packages/server/src/core/graphql/createGraphQLRoute/createGraphQLRoute.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import {
2020
convertToEntityDescriptor,
2121
getGraphQLInput,
2222
isEntityDescriptor,
23+
normalizeUrl,
2324
parseQuery,
2425
resolveEntityValues,
2526
sleep
2627
} from '@/utils/helpers';
2728

29+
import { matchGraphQLRequestArtifacts } from './helpers';
30+
2831
interface CreateGraphQLRouteParams {
2932
graphQLRequestArtifacts: GraphQLRequestArtifact[];
3033
server: Express;
@@ -36,34 +39,19 @@ export const createGraphQLRoute = ({ server, graphQLRequestArtifacts }: CreateGr
3639
if (request.method !== 'GET' && request.method !== 'POST') return next();
3740

3841
const graphQLInput = getGraphQLInput(request);
39-
if (!graphQLInput.query) {
40-
return response.status(400).json({
41-
message: 'Query is missing, you must pass a valid GraphQL query'
42-
});
43-
}
42+
if (!graphQLInput.query) return next();
4443

4544
const query = parseQuery(graphQLInput.query);
46-
if (!query) {
47-
return response.status(400).json({
48-
message: 'Query is invalid, you must use a valid GraphQL query'
49-
});
50-
}
51-
52-
const matchedRequestArtifacts = graphQLRequestArtifacts.filter((artifact) => {
53-
if (artifact.operationType !== query.operationType) return false;
54-
55-
if (artifact.query) {
56-
return artifact.query.replace(/\s+/g, '') === graphQLInput.query?.replace(/\s+/g, '');
45+
if (!query) return next();
46+
47+
const matchedRequestArtifacts = matchGraphQLRequestArtifacts({
48+
artifacts: graphQLRequestArtifacts,
49+
meta: {
50+
path: normalizeUrl(request.path),
51+
query: graphQLInput.query,
52+
operationType: query.operationType,
53+
operationName: query.operationName
5754
}
58-
59-
if (artifact.operationName) {
60-
if (!query.operationName) return false;
61-
return artifact.operationName instanceof RegExp
62-
? new RegExp(artifact.operationName).test(query.operationName)
63-
: artifact.operationName === query.operationName;
64-
}
65-
66-
return true;
6755
});
6856

6957
if (!matchedRequestArtifacts.length) return next();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { calculateGraphQLRouteConfigWeight } from './calculateGraphQLRouteConfigWeight';
4+
5+
describe('calculateGraphQLRouteConfigWeight', () => {
6+
it('Should return 0 when entities is absent', () => {
7+
expect(calculateGraphQLRouteConfigWeight({ data: {} } as any)).toBe(0);
8+
});
9+
10+
it('Should sum keys of entities', () => {
11+
expect(
12+
calculateGraphQLRouteConfigWeight({
13+
data: {},
14+
entities: {
15+
headers: { key: 'value' },
16+
cookies: { key: 'value' },
17+
query: { key: 'value' },
18+
variables: { key: 'value' }
19+
}
20+
})
21+
).toBe(4);
22+
});
23+
24+
it('Should add one for variables exists/notExists descriptor', () => {
25+
expect(
26+
calculateGraphQLRouteConfigWeight({
27+
data: {},
28+
entities: {
29+
variables: { checkMode: 'exists' }
30+
}
31+
})
32+
).toBe(1);
33+
});
34+
35+
it('Should count keys of variables descriptor value when it is a plain object', () => {
36+
expect(
37+
calculateGraphQLRouteConfigWeight({
38+
data: {},
39+
entities: {
40+
variables: {
41+
checkMode: 'equals',
42+
value: { a: 'value', b: 'value', c: 'value' }
43+
}
44+
}
45+
})
46+
).toBe(3);
47+
});
48+
49+
it('Should count plain object variables without descriptor', () => {
50+
expect(
51+
calculateGraphQLRouteConfigWeight({
52+
data: {},
53+
entities: {
54+
variables: { a: 'value', b: 'value' }
55+
}
56+
})
57+
).toBe(2);
58+
});
59+
});

packages/server/src/core/graphql/createGraphQLRoute/helpers/calculateGraphQLRouteConfigWeight.ts renamed to packages/server/src/core/graphql/createGraphQLRoute/helpers/calculateGraphQLRouteConfigWeight/calculateGraphQLRouteConfigWeight.ts

File renamed without changes.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export * from './calculateGraphQLRouteConfigWeight';
1+
export * from './calculateGraphQLRouteConfigWeight/calculateGraphQLRouteConfigWeight';
2+
export * from './matchGraphQLRequestArtifacts/matchGraphQLRequestArtifacts';
3+
export * from './prepareGraphQLRequestArtifacts/prepareGraphQLRequestArtifacts';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import type { BaseUrl, GraphQLRequestArtifact } from '@/utils/types';
4+
5+
import { matchGraphQLRequestArtifacts } from './matchGraphQLRequestArtifacts';
6+
7+
const makeArtifact = (overrides: Partial<GraphQLRequestArtifact>): GraphQLRequestArtifact =>
8+
({
9+
baseUrl: '/' as BaseUrl,
10+
operationType: 'query',
11+
config: { data: { ok: true } },
12+
weight: 0,
13+
...overrides
14+
}) as GraphQLRequestArtifact;
15+
16+
describe('matchGraphQLRequestArtifacts', () => {
17+
it('Should not match request path to artifact baseUrl', () => {
18+
const matched = matchGraphQLRequestArtifacts({
19+
artifacts: [makeArtifact({ baseUrl: '/v1', operationName: 'GetUsers' })],
20+
meta: {
21+
path: '/v2',
22+
operationType: 'query',
23+
operationName: 'GetUsers'
24+
}
25+
});
26+
expect(matched).toHaveLength(0);
27+
});
28+
29+
it('Should return empty when operationType mismatches', () => {
30+
const matched = matchGraphQLRequestArtifacts({
31+
artifacts: [makeArtifact({ operationName: 'GetUsers' })],
32+
meta: {
33+
path: '/',
34+
operationType: 'mutation',
35+
operationName: 'GetUsers'
36+
}
37+
});
38+
expect(matched).toHaveLength(0);
39+
});
40+
41+
it('Should match equivalent queries with different insignificant whitespace', () => {
42+
const matched = matchGraphQLRequestArtifacts({
43+
artifacts: [makeArtifact({ query: 'query { User { name } }' })],
44+
meta: {
45+
path: '/',
46+
operationType: 'query',
47+
query: `query {
48+
User { name }
49+
}`
50+
}
51+
});
52+
expect(matched).toHaveLength(1);
53+
});
54+
55+
it('Should fail query match when meta query is missing', () => {
56+
const matched = matchGraphQLRequestArtifacts({
57+
artifacts: [makeArtifact({ query: 'query { User { name } }' })],
58+
meta: {
59+
path: '/',
60+
operationType: 'query'
61+
}
62+
});
63+
expect(matched).toHaveLength(0);
64+
});
65+
66+
it('Should fail query match when query strings differ', () => {
67+
const matched = matchGraphQLRequestArtifacts({
68+
artifacts: [makeArtifact({ query: 'query { User { name } }' })],
69+
meta: {
70+
path: '/',
71+
operationType: 'query',
72+
query: 'query { User { id } }'
73+
}
74+
});
75+
expect(matched).toHaveLength(0);
76+
});
77+
78+
it('Should match operation name string', () => {
79+
const matched = matchGraphQLRequestArtifacts({
80+
artifacts: [makeArtifact({ operationName: 'GetUsers' })],
81+
meta: {
82+
path: '/',
83+
operationType: 'query',
84+
query: 'query GetUsers { users { id } }',
85+
operationName: 'GetUsers'
86+
}
87+
});
88+
expect(matched).toHaveLength(1);
89+
});
90+
91+
it('Should fail operation name string when names differ', () => {
92+
const matched = matchGraphQLRequestArtifacts({
93+
artifacts: [makeArtifact({ operationName: 'GetUsers' })],
94+
meta: {
95+
path: '/',
96+
operationType: 'query',
97+
operationName: 'GetOther'
98+
}
99+
});
100+
expect(matched).toHaveLength(0);
101+
});
102+
103+
it('Should fail operationName match when meta operation name is missing', () => {
104+
const matched = matchGraphQLRequestArtifacts({
105+
artifacts: [makeArtifact({ operationName: 'GetUsers' })],
106+
meta: {
107+
path: '/',
108+
operationType: 'query',
109+
query: 'query { users { id } }'
110+
}
111+
});
112+
expect(matched).toHaveLength(0);
113+
});
114+
115+
it('Should match operation name regexp', () => {
116+
const matched = matchGraphQLRequestArtifacts({
117+
artifacts: [makeArtifact({ operationName: /^Get(.+?)sers$/g })],
118+
meta: {
119+
path: '/',
120+
operationType: 'query',
121+
query: 'query GetUsers { users { id } }',
122+
operationName: 'GetUsers'
123+
}
124+
});
125+
expect(matched).toHaveLength(1);
126+
});
127+
128+
it('Should match operation name regexp with case-insensitive flag', () => {
129+
const matched = matchGraphQLRequestArtifacts({
130+
artifacts: [makeArtifact({ operationName: /^getusers$/i })],
131+
meta: {
132+
path: '/',
133+
operationType: 'query',
134+
operationName: 'GetUsers'
135+
}
136+
});
137+
expect(matched).toHaveLength(1);
138+
});
139+
140+
it('Should skip artifact with neither query nor operationName', () => {
141+
const warn = vi.spyOn(console, 'warn');
142+
const artifact = makeArtifact({});
143+
144+
const matched = matchGraphQLRequestArtifacts({
145+
artifacts: [artifact],
146+
meta: {
147+
path: '/',
148+
operationType: 'query',
149+
operationName: 'GetUsers'
150+
}
151+
});
152+
153+
expect(matched).toHaveLength(0);
154+
expect(warn).toHaveBeenCalledWith(
155+
`[mock-config] GraphQL artifact with no query or operationName was skipped: ${JSON.stringify(
156+
artifact
157+
)}`
158+
);
159+
});
160+
161+
it('Should fail operation name regexp when pattern does not match', () => {
162+
const matched = matchGraphQLRequestArtifacts({
163+
artifacts: [makeArtifact({ operationName: /^Other$/ })],
164+
meta: {
165+
path: '/',
166+
operationType: 'query',
167+
query: 'query GetUsers { users { id } }',
168+
operationName: 'GetUsers'
169+
}
170+
});
171+
expect(matched).toHaveLength(0);
172+
});
173+
});

0 commit comments

Comments
 (0)