Skip to content

Commit 15502cf

Browse files
authored
feat: throw on null or empty object input (#61)
* Throw by default when null or empty object inputs are encountered * Add tests * Document new plugin options
1 parent d4e20c1 commit 15502cf

File tree

6 files changed

+238
-37
lines changed

6 files changed

+238
-37
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ app.use(
4848
app.listen(5000);
4949
```
5050

51+
## Handling `null` and empty objects
52+
53+
By default, this plugin will throw an error when `null` literals or empty objects (`{}`) are included in `filter` input objects. This prevents queries with ambiguous semantics such as `filter: { field: null }` and `filter: { field: { equalTo: null } }` from returning unexpected results. For background on this decision, see https://github.com/graphile-contrib/postgraphile-plugin-connection-filter/issues/58.
54+
55+
To allow `null` and `{}` in inputs, use the `connectionFilterAllowNullInput` and `connectionFilterAllowEmptyObjectInput` options documented under [Plugin Options](https://github.com/graphile-contrib/postgraphile-plugin-connection-filter#plugin-options). Please note that even with `connectionFilterAllowNullInput` enabled, `null` is never interpreted as a SQL `NULL`; fields with `null` values are simply ignored when resolving the query.
56+
5157
## Operators
5258

5359
The following filter operators are exposed by default:
@@ -452,6 +458,44 @@ postgraphile(pgConfig, schema, {
452458

453459
</details>
454460

461+
<details>
462+
463+
<summary>connectionFilterAllowNullInput</summary>
464+
465+
Allow/forbid `null` literals in input:
466+
467+
``` js
468+
postgraphile(pgConfig, schema, {
469+
graphileBuildOptions: {
470+
connectionFilterAllowNullInput: true, // default: false
471+
},
472+
})
473+
```
474+
475+
When `false`, passing `null` as a field value will throw an error.
476+
When `true`, passing `null` as a field value is equivalent to omitting the field.
477+
478+
</details>
479+
480+
<details>
481+
482+
<summary>connectionFilterAllowEmptyObjectInput</summary>
483+
484+
Allow/forbid empty objects (`{}`) in input:
485+
486+
``` js
487+
postgraphile(pgConfig, schema, {
488+
graphileBuildOptions: {
489+
connectionFilterAllowEmptyObjectInput: true, // default: false
490+
},
491+
})
492+
```
493+
494+
When `false`, passing `{}` as a field value will throw an error.
495+
When `true`, passing `{}` as a field value is equivalent to omitting the field.
496+
497+
</details>
498+
455499
## Development
456500

457501
To establish a test environment, create an empty Postgres database (e.g. `graphile_build_test`) and set a `TEST_DATABASE_URL` environment variable with your connection string (e.g. `postgres://localhost:5432/graphile_build_test`). Ensure that `psql` is installed locally and then run:

__tests__/fixtures/queries/connections-filter.null-and-empty.graphql renamed to __tests__/fixtures/queries/connections-filter.null-and-empty-allowed.graphql

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# all queries in this file should result in no filter being applied, thus returning all rows
1+
# All queries in this file should result in no filter being applied, thus returning all rows
2+
# (queries are an exact copy of null-and-empty-forbidden.graphql)
23
query {
34
a: allFilterables(filter: null) {
45
totalCount
@@ -40,37 +41,31 @@ query {
4041
k: allFilterables(filter: { and: null }) {
4142
totalCount
4243
}
43-
l: allFilterables(filter: { and: [] }) {
44+
l: allFilterables(filter: { and: [{}] }) {
4445
totalCount
4546
}
46-
n: allFilterables(filter: { and: [{}] }) {
47+
m: allFilterables(filter: { and: [{}, {}] }) {
4748
totalCount
4849
}
49-
o: allFilterables(filter: { and: [{}, {}] }) {
50+
n: allFilterables(filter: { or: null }) {
5051
totalCount
5152
}
52-
q: allFilterables(filter: { or: null }) {
53+
o: allFilterables(filter: { or: [{}] }) {
5354
totalCount
5455
}
55-
r: allFilterables(filter: { or: [] }) {
56+
p: allFilterables(filter: { or: [{}, {}] }) {
5657
totalCount
5758
}
58-
t: allFilterables(filter: { or: [{}] }) {
59+
q: allFilterables(filter: { not: null }) {
5960
totalCount
6061
}
61-
u: allFilterables(filter: { or: [{}, {}] }) {
62+
r: allFilterables(filter: { not: {} }) {
6263
totalCount
6364
}
64-
w: allFilterables(filter: { not: null }) {
65+
s: allFilterables(filter: { not: { string: null } }) {
6566
totalCount
6667
}
67-
x: allFilterables(filter: { not: {} }) {
68-
totalCount
69-
}
70-
y: allFilterables(filter: { not: { string: null } }) {
71-
totalCount
72-
}
73-
z: allFilterables(filter: { not: { string: {} } }) {
68+
t: allFilterables(filter: { not: { string: {} } }) {
7469
totalCount
7570
}
7671
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# All queries in this file should throw an error
2+
# (queries are an exact copy of null-and-empty-allowed.graphql)
3+
query {
4+
a: allFilterables(filter: null) {
5+
totalCount
6+
}
7+
b: allFilterables(filter: {}) {
8+
totalCount
9+
}
10+
c: allFilterables(filter: { string: null }) {
11+
totalCount
12+
}
13+
d: allFilterables(filter: { string: {} }) {
14+
totalCount
15+
}
16+
e: allFilterables(filter: { string: null, int: null }) {
17+
totalCount
18+
}
19+
f: allFilterables(filter: { string: {}, int: {} }) {
20+
totalCount
21+
}
22+
g: allFilterables(filter: { string: null, int: {} }) {
23+
totalCount
24+
}
25+
h: allFilterables(filter: { string: { equalTo: null } }) {
26+
totalCount
27+
}
28+
i: allFilterables(filter: { string: { equalTo: null, notEqualTo: null } }) {
29+
totalCount
30+
}
31+
j: allFilterables(
32+
filter: {
33+
string: { equalTo: null, notEqualTo: null }
34+
int: { equalTo: null }
35+
numeric: {}
36+
boolean: null
37+
}
38+
) {
39+
totalCount
40+
}
41+
k: allFilterables(filter: { and: null }) {
42+
totalCount
43+
}
44+
l: allFilterables(filter: { and: [{}] }) {
45+
totalCount
46+
}
47+
m: allFilterables(filter: { and: [{}, {}] }) {
48+
totalCount
49+
}
50+
n: allFilterables(filter: { or: null }) {
51+
totalCount
52+
}
53+
o: allFilterables(filter: { or: [{}] }) {
54+
totalCount
55+
}
56+
p: allFilterables(filter: { or: [{}, {}] }) {
57+
totalCount
58+
}
59+
q: allFilterables(filter: { not: null }) {
60+
totalCount
61+
}
62+
r: allFilterables(filter: { not: {} }) {
63+
totalCount
64+
}
65+
s: allFilterables(filter: { not: { string: null } }) {
66+
totalCount
67+
}
68+
t: allFilterables(filter: { not: { string: {} } }) {
69+
totalCount
70+
}
71+
}

__tests__/integration/__snapshots__/queries.test.js.snap

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ Object {
10401040
}
10411041
`;
10421042

1043-
exports[`connections-filter.null-and-empty.graphql 1`] = `
1043+
exports[`connections-filter.null-and-empty-allowed.graphql 1`] = `
10441044
Object {
10451045
"data": Object {
10461046
"a": Object {
@@ -1079,40 +1079,83 @@ Object {
10791079
"l": Object {
10801080
"totalCount": 5,
10811081
},
1082-
"n": Object {
1083-
"totalCount": 5,
1084-
},
1085-
"o": Object {
1086-
"totalCount": 5,
1087-
},
1088-
"q": Object {
1082+
"m": Object {
10891083
"totalCount": 5,
10901084
},
1091-
"r": Object {
1085+
"n": Object {
10921086
"totalCount": 5,
10931087
},
1094-
"t": Object {
1088+
"o": Object {
10951089
"totalCount": 5,
10961090
},
1097-
"u": Object {
1091+
"p": Object {
10981092
"totalCount": 5,
10991093
},
1100-
"w": Object {
1094+
"q": Object {
11011095
"totalCount": 5,
11021096
},
1103-
"x": Object {
1097+
"r": Object {
11041098
"totalCount": 5,
11051099
},
1106-
"y": Object {
1100+
"s": Object {
11071101
"totalCount": 5,
11081102
},
1109-
"z": Object {
1103+
"t": Object {
11101104
"totalCount": 5,
11111105
},
11121106
},
11131107
}
11141108
`;
11151109

1110+
exports[`connections-filter.null-and-empty-forbidden.graphql 1`] = `
1111+
Object {
1112+
"data": Object {
1113+
"a": null,
1114+
"b": null,
1115+
"c": null,
1116+
"d": null,
1117+
"e": null,
1118+
"f": null,
1119+
"g": null,
1120+
"h": null,
1121+
"i": null,
1122+
"j": null,
1123+
"k": null,
1124+
"l": null,
1125+
"m": null,
1126+
"n": null,
1127+
"o": null,
1128+
"p": null,
1129+
"q": null,
1130+
"r": null,
1131+
"s": null,
1132+
"t": null,
1133+
},
1134+
"errors": Array [
1135+
[GraphQLError: Null literals are forbidden in filter argument input.],
1136+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1137+
[GraphQLError: Null literals are forbidden in filter argument input.],
1138+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1139+
[GraphQLError: Null literals are forbidden in filter argument input.],
1140+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1141+
[GraphQLError: Null literals are forbidden in filter argument input.],
1142+
[GraphQLError: Null literals are forbidden in filter argument input.],
1143+
[GraphQLError: Null literals are forbidden in filter argument input.],
1144+
[GraphQLError: Null literals are forbidden in filter argument input.],
1145+
[GraphQLError: Null literals are forbidden in filter argument input.],
1146+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1147+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1148+
[GraphQLError: Null literals are forbidden in filter argument input.],
1149+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1150+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1151+
[GraphQLError: Null literals are forbidden in filter argument input.],
1152+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1153+
[GraphQLError: Null literals are forbidden in filter argument input.],
1154+
[GraphQLError: Empty objects are forbidden in filter argument input.],
1155+
],
1156+
}
1157+
`;
1158+
11161159
exports[`connections-filter.relations.graphql 1`] = `
11171160
Object {
11181161
"data": Object {

__tests__/integration/queries.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ beforeAll(() => {
3232
dynamicJson,
3333
relations,
3434
simpleCollections,
35+
nullAndEmptyAllowed,
3536
] = await Promise.all([
3637
createPostGraphileSchema(pgClient, ["p"], {
3738
skipPlugins: [require("graphile-build-pg").PgConnectionArgCondition],
3839
appendPlugins: [require("../../index.js")],
3940
}),
4041
createPostGraphileSchema(pgClient, ["p"], {
41-
dynamicJson: true,
4242
skipPlugins: [require("graphile-build-pg").PgConnectionArgCondition],
4343
appendPlugins: [require("../../index.js")],
44+
dynamicJson: true,
4445
}),
4546
createPostGraphileSchema(pgClient, ["p"], {
4647
skipPlugins: [require("graphile-build-pg").PgConnectionArgCondition],
@@ -50,9 +51,17 @@ beforeAll(() => {
5051
},
5152
}),
5253
createPostGraphileSchema(pgClient, ["p"], {
54+
skipPlugins: [require("graphile-build-pg").PgConnectionArgCondition],
55+
appendPlugins: [require("../../index.js")],
5356
simpleCollections: "only",
57+
}),
58+
createPostGraphileSchema(pgClient, ["p"], {
5459
skipPlugins: [require("graphile-build-pg").PgConnectionArgCondition],
5560
appendPlugins: [require("../../index.js")],
61+
graphileBuildOptions: {
62+
connectionFilterAllowNullInput: true,
63+
connectionFilterAllowEmptyObjectInput: true,
64+
},
5665
}),
5766
]);
5867
debug(printSchema(normal));
@@ -61,6 +70,7 @@ beforeAll(() => {
6170
dynamicJson,
6271
relations,
6372
simpleCollections,
73+
nullAndEmptyAllowed,
6474
};
6575
});
6676

@@ -92,6 +102,8 @@ beforeAll(() => {
92102
"connections-filter.relations.graphql": gqlSchemas.relations,
93103
"connections-filter.simple-collections.graphql":
94104
gqlSchemas.simpleCollections,
105+
"connections-filter.null-and-empty-allowed.graphql":
106+
gqlSchemas.nullAndEmptyAllowed,
95107
};
96108
const gqlSchema = schemas[fileName]
97109
? schemas[fileName]

0 commit comments

Comments
 (0)