Skip to content

Commit 3b49d56

Browse files
1.0.12
1 parent adf261c commit 3b49d56

7 files changed

Lines changed: 138 additions & 37 deletions

File tree

dist/__tests__/merge.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,32 @@ describe('mergeQueries', () => {
162162
salaries {
163163
amount
164164
}
165+
}`,
166+
];
167+
const expected = ``;
168+
expect((0, merge_1.mergeQueries)(requestQuery, allowedQueries)).toBe(expected);
169+
});
170+
test('should handle subqueries', () => {
171+
const requestQuery = `query ExampleQuery($where: DepartmentWhereUniqueInput!) {
172+
department(where: $where) {
173+
id
174+
}
175+
}`;
176+
const allowedQueries = [
177+
`query {
178+
departments {
179+
id
180+
name
181+
employees {
182+
id
183+
name
184+
}
185+
}
186+
}`,
187+
`query {
188+
salaries {
189+
amount
190+
}
165191
}`,
166192
];
167193
const expected = ``;

dist/index.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ class GraphQLQueryPurifier {
3737
// Use mergeQueries to filter the incoming request query
3838
const filteredQuery = (0, merge_1.mergeQueries)(req.body.query, allowedQueries);
3939
if (!filteredQuery.trim()) {
40-
// Log the incident for monitoring
4140
console.warn(`Query was blocked due to security rules: ${req.body.query}`);
42-
return res.status(403).send('The requested query is not allowed.');
41+
req.body.query = '{ __typename }';
42+
delete req.body.operationName; // Remove the operation name
4343
}
4444
else {
4545
req.body.query = filteredQuery;
@@ -57,24 +57,38 @@ class GraphQLQueryPurifier {
5757
const files = glob_1.default.sync(`${this.gqlPath}/**/*.gql`.replace(/\\/g, '/'));
5858
files.forEach((file) => {
5959
if (path_1.default.extname(file) === '.gql') {
60-
const content = fs_1.default.readFileSync(file, 'utf8');
61-
const parsedQuery = (0, graphql_1.parse)(content);
62-
parsedQuery.definitions.forEach((definition) => {
63-
if (definition.kind === 'OperationDefinition') {
64-
const operationDefinition = definition;
65-
let queryName = operationDefinition.name?.value;
66-
if (!queryName) {
67-
// Extract the name from the first field of the selection set
68-
const firstField = operationDefinition.selectionSet.selections[0];
69-
if (firstField && firstField.kind === 'Field') {
70-
queryName = firstField.name.value;
60+
const content = fs_1.default.readFileSync(file, 'utf8').trim();
61+
if (!content) {
62+
console.warn(`Warning: Empty or invalid GraphQL file found: ${file}`);
63+
return;
64+
}
65+
try {
66+
const parsedQuery = (0, graphql_1.parse)(content);
67+
parsedQuery.definitions.forEach((definition) => {
68+
if (definition.kind === 'OperationDefinition') {
69+
const operationDefinition = definition;
70+
let queryName = operationDefinition.name?.value;
71+
if (!queryName) {
72+
// Extract the name from the first field of the selection set
73+
const firstField = operationDefinition.selectionSet.selections[0];
74+
if (firstField && firstField.kind === 'Field') {
75+
queryName = firstField.name.value;
76+
}
77+
}
78+
if (queryName) {
79+
this.queryMap[queryName] = content;
7180
}
7281
}
73-
if (queryName) {
74-
this.queryMap[queryName] = content;
75-
}
82+
});
83+
}
84+
catch (error) {
85+
if (error instanceof graphql_1.GraphQLError) {
86+
console.error(`Error parsing GraphQL file ${file}: ${error.message}`);
87+
}
88+
else {
89+
console.error(`Unexpected error processing file ${file}: ${error}`);
7690
}
77-
});
91+
}
7892
}
7993
});
8094
}

dist/merge.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ function mergeQueries(requestQuery, allowedQueries) {
3535
return undefined; // Continue visiting child nodes
3636
},
3737
});
38+
// Check if the modified query has any fields left in its selection set
39+
const hasFields = modifiedAST.definitions.some((def) => def.kind === 'OperationDefinition' &&
40+
def.selectionSet.selections.length > 0);
41+
if (!hasFields) {
42+
// Return a placeholder or minimal query
43+
return '';
44+
}
3845
return (0, graphql_1.print)(modifiedAST);
3946
}
4047
exports.mergeQueries = mergeQueries;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-query-purifier",
3-
"version": "1.0.11",
3+
"version": "1.0.12",
44
"description": "A small library to match .gql queries vs user input. Removes fields from user requests that are not expected by your frontend code.",
55
"main": "./dist/index.js",
66
"author": "multipliedtwice",

src/__tests__/merge.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,33 @@ describe('mergeQueries', () => {
174174
salaries {
175175
amount
176176
}
177+
}`,
178+
];
179+
const expected = ``;
180+
expect(mergeQueries(requestQuery, allowedQueries)).toBe(expected);
181+
});
182+
183+
test('should handle subqueries', () => {
184+
const requestQuery = `query ExampleQuery($where: DepartmentWhereUniqueInput!) {
185+
department(where: $where) {
186+
id
187+
}
188+
}`;
189+
const allowedQueries = [
190+
`query {
191+
departments {
192+
id
193+
name
194+
employees {
195+
id
196+
name
197+
}
198+
}
199+
}`,
200+
`query {
201+
salaries {
202+
amount
203+
}
177204
}`,
178205
];
179206
const expected = ``;

src/index.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextFunction, Request, Response } from 'express';
22
import fs from 'fs';
3-
import { OperationDefinitionNode, parse } from 'graphql';
3+
import { GraphQLError, OperationDefinitionNode, parse } from 'graphql';
44
import path from 'path';
55
// @ts-ignore
66
import glob from 'glob';
@@ -33,27 +33,43 @@ export class GraphQLQueryPurifier {
3333

3434
files.forEach((file: string) => {
3535
if (path.extname(file) === '.gql') {
36-
const content = fs.readFileSync(file, 'utf8');
37-
const parsedQuery = parse(content);
36+
const content = fs.readFileSync(file, 'utf8').trim();
3837

39-
parsedQuery.definitions.forEach((definition) => {
40-
if (definition.kind === 'OperationDefinition') {
41-
const operationDefinition = definition as OperationDefinitionNode;
38+
if (!content) {
39+
console.warn(`Warning: Empty or invalid GraphQL file found: ${file}`);
40+
return;
41+
}
4242

43-
let queryName = operationDefinition.name?.value;
44-
if (!queryName) {
45-
// Extract the name from the first field of the selection set
46-
const firstField = operationDefinition.selectionSet.selections[0];
47-
if (firstField && firstField.kind === 'Field') {
48-
queryName = firstField.name.value;
43+
try {
44+
const parsedQuery = parse(content);
45+
parsedQuery.definitions.forEach((definition) => {
46+
if (definition.kind === 'OperationDefinition') {
47+
const operationDefinition = definition as OperationDefinitionNode;
48+
49+
let queryName = operationDefinition.name?.value;
50+
if (!queryName) {
51+
// Extract the name from the first field of the selection set
52+
const firstField =
53+
operationDefinition.selectionSet.selections[0];
54+
if (firstField && firstField.kind === 'Field') {
55+
queryName = firstField.name.value;
56+
}
4957
}
50-
}
5158

52-
if (queryName) {
53-
this.queryMap[queryName] = content;
59+
if (queryName) {
60+
this.queryMap[queryName] = content;
61+
}
5462
}
63+
});
64+
} catch (error) {
65+
if (error instanceof GraphQLError) {
66+
console.error(
67+
`Error parsing GraphQL file ${file}: ${error.message}`
68+
);
69+
} else {
70+
console.error(`Unexpected error processing file ${file}: ${error}`);
5571
}
56-
});
72+
}
5773
}
5874
});
5975
}
@@ -90,12 +106,11 @@ export class GraphQLQueryPurifier {
90106
const filteredQuery = mergeQueries(req.body.query, allowedQueries);
91107

92108
if (!filteredQuery.trim()) {
93-
// Log the incident for monitoring
94109
console.warn(
95110
`Query was blocked due to security rules: ${req.body.query}`
96111
);
97-
98-
return res.status(403).send('The requested query is not allowed.');
112+
req.body.query = '{ __typename }';
113+
delete req.body.operationName; // Remove the operation name
99114
} else {
100115
req.body.query = filteredQuery;
101116
}

src/merge.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,17 @@ export function mergeQueries(
4141
},
4242
});
4343

44+
// Check if the modified query has any fields left in its selection set
45+
const hasFields = modifiedAST.definitions.some(
46+
(def) =>
47+
def.kind === 'OperationDefinition' &&
48+
def.selectionSet.selections.length > 0
49+
);
50+
51+
if (!hasFields) {
52+
// Return a placeholder or minimal query
53+
return '';
54+
}
55+
4456
return print(modifiedAST);
4557
}

0 commit comments

Comments
 (0)