@@ -47,12 +47,41 @@ public function manipulateArgDefinition(
47
47
FieldDefinitionNode &$ parentField ,
48
48
ObjectTypeDefinitionNode |InterfaceTypeDefinitionNode &$ parentType ,
49
49
): void {
50
+ // A hack to get the return type, assuming all lists are paginated...
51
+ $ returnType = Str::replaceEnd ('Connection ' , '' , $ parentField ->type ->type ->name ->value );
52
+
50
53
$ multiFilterName = ASTHelper::qualifiedArgType ($ argDefinition , $ parentField , $ parentType ) . 'MultiFilterInput ' ;
51
54
$ argDefinition ->type = Parser::namedType ($ multiFilterName );
52
55
53
- $ defaultFilterType = Str::replaceEnd ('Connection ' , '' , $ parentField ->type ->type ->name ->value ) . 'FilterInput ' ;
54
- $ inputType = $ this ->directiveArgValue ('inputType ' , $ defaultFilterType );
55
- $ documentAST ->setTypeDefinition ($ this ->createMultiFilterInput ($ multiFilterName , $ inputType ));
56
+ $ defaultFilterType = $ returnType . 'FilterInput ' ;
57
+ $ filterName = $ this ->directiveArgValue ('inputType ' , $ defaultFilterType );
58
+
59
+ if (
60
+ $ this ->getSubFilterableFieldsForType ($ argDefinition , $ parentType ) === []
61
+ || !str_ends_with ($ parentField ->type ->type ->name ->value , 'Connection ' )
62
+ || $ this ->getSubFilterableFieldsForType ($ argDefinition , $ documentAST ->types [$ returnType ]) === []
63
+ ) {
64
+ // Don't create a relationship filter input type because this type has no relationships
65
+ $ documentAST ->setTypeDefinition ($ this ->createMultiFilterInput ($ multiFilterName , $ filterName , null ));
66
+ } else {
67
+ $ relatedFieldRelationshipFilterName = Str::replaceEnd ('Connection ' , '' , $ parentField ->type ->type ->name ->value ) . 'RelationshipFilterInput ' ;
68
+ $ documentAST ->setTypeDefinition ($ this ->createMultiFilterInput ($ multiFilterName , $ filterName , $ relatedFieldRelationshipFilterName ));
69
+ }
70
+
71
+ // We only have to create the relationship filter input type once per type, and don't create it at all
72
+ // if there are no relationships present.
73
+ $ relationshipFilterName = $ parentType ->name ->value . 'RelationshipFilterInput ' ;
74
+ if (
75
+ !array_key_exists ($ relationshipFilterName , $ documentAST ->types )
76
+ && $ this ->getSubFilterableFieldsForType ($ argDefinition , $ parentType ) !== []
77
+ ) {
78
+ $ documentAST ->setTypeDefinition (
79
+ $ this ->createRelationshipFilterInput (
80
+ $ relationshipFilterName ,
81
+ $ this ->getSubFilterableFieldsForType ($ argDefinition , $ parentType )
82
+ )
83
+ );
84
+ }
56
85
}
57
86
58
87
/**
@@ -69,37 +98,49 @@ public function handleBuilder(QueryBuilder|EloquentBuilder|Relation $builder, mi
69
98
throw new InvalidArgumentException ('$value parameter must be array ' );
70
99
}
71
100
101
+ if ($ builder instanceof QueryBuilder) {
102
+ throw new InvalidArgumentException ('Query builder is not allowed. ' );
103
+ }
104
+
72
105
$ this ->applyFilters ($ builder , $ value );
73
106
74
107
return $ builder ;
75
108
}
76
109
77
- protected function applyFilters (QueryBuilder | EloquentBuilder |Relation $ builder , mixed $ filter , string $ context = 'and ' , ? string $ table = null ): void
110
+ protected function applyFilters (EloquentBuilder |Relation $ builder , mixed $ filter , string $ context = 'and ' ): void
78
111
{
79
- // The query builder isn't able to provide the table being queried, so we pass it down the chain from
80
- // eloquent-derived builders which can.
81
- if ($ builder instanceof EloquentBuilder || $ builder instanceof Relation) {
82
- $ table = $ builder ->getModel ()->getTable ();
83
- }
112
+ $ table = $ builder ->getModel ()->getTable ();
84
113
85
114
if (array_key_exists ('all ' , $ filter )) {
86
- $ builder ->whereNested (
87
- function ($ subfilterBuilder ) use ($ filter, $ table ): void {
115
+ $ builder ->where (
116
+ function ($ subfilterBuilder ) use ($ filter ): void {
88
117
foreach ($ filter ['all ' ] as $ subfilter ) {
89
- $ this ->applyFilters ($ subfilterBuilder , $ subfilter , 'and ' , $ table );
118
+ $ this ->applyFilters ($ subfilterBuilder , $ subfilter , 'and ' );
90
119
}
91
120
},
92
- $ context
121
+ boolean: $ context,
93
122
);
94
123
} elseif (array_key_exists ('any ' , $ filter )) {
95
- $ builder ->whereNested (
96
- function ($ subfilterBuilder ) use ($ filter, $ table ): void {
124
+ $ builder ->where (
125
+ function ($ subfilterBuilder ) use ($ filter ): void {
97
126
foreach ($ filter ['any ' ] as $ subfilter ) {
98
- $ this ->applyFilters ($ subfilterBuilder , $ subfilter , 'or ' , $ table );
127
+ $ this ->applyFilters ($ subfilterBuilder , $ subfilter , 'or ' );
99
128
}
100
129
},
101
- $ context ,
130
+ boolean: $ context ,
102
131
);
132
+ } elseif (array_key_exists ('has ' , $ filter )) {
133
+ $ relationshipName = array_key_first ($ filter ['has ' ]);
134
+ $ relationshipFilters = $ filter ['has ' ][$ relationshipName ];
135
+ if ($ context === 'and ' ) {
136
+ $ builder ->whereHas ($ relationshipName , function ($ subfilterBuilder ) use ($ relationshipFilters ): void {
137
+ $ this ->applyFilters ($ subfilterBuilder , $ relationshipFilters );
138
+ });
139
+ } else {
140
+ $ builder ->orWhereHas ($ relationshipName , function ($ subfilterBuilder ) use ($ relationshipFilters ): void {
141
+ $ this ->applyFilters ($ subfilterBuilder , $ relationshipFilters );
142
+ });
143
+ }
103
144
} else {
104
145
$ operators = [
105
146
'eq ' => '= ' ,
@@ -147,26 +188,73 @@ function ($subfilterBuilder) use ($filter, $table): void {
147
188
* @throws SyntaxError
148
189
* @throws JsonException
149
190
*/
150
- protected function createMultiFilterInput (string $ multiFilterName , string $ filterName ): InputObjectTypeDefinitionNode
191
+ protected function createMultiFilterInput (string $ multiFilterName , string $ filterName, ? string $ relationshipFilterName ): InputObjectTypeDefinitionNode
151
192
{
193
+ if ($ relationshipFilterName !== null ) {
194
+ $ hasFilter = '"Find nodes which have one or more related notes which match the provided filter." ' . PHP_EOL ;
195
+ $ hasFilter .= 'has: ' . $ relationshipFilterName . '@rules(apply: ["prohibits:any,all,eq,ne,gt,lt,contains"]) ' ;
196
+ } else {
197
+ $ hasFilter = '' ;
198
+ }
199
+
152
200
return Parser::inputObjectTypeDefinition (/* @lang GraphQL */ <<<GRAPHQL
153
201
input {$ multiFilterName } {
154
202
"Find nodes which match at least one of the provided filters."
155
- any: [ {$ multiFilterName }] @rules(apply: ["prohibits:all,eq,ne,gt,lt,contains"])
203
+ any: [ {$ multiFilterName }] @rules(apply: ["prohibits:all,has, eq,ne,gt,lt,contains"])
156
204
"Find nodes which match all of the provided filters."
157
- all: [ {$ multiFilterName }] @rules(apply: ["prohibits:any,eq,ne,gt,lt,contains"])
205
+ all: [ {$ multiFilterName }] @rules(apply: ["prohibits:any,has,eq,ne,gt,lt,contains"])
206
+ {$ hasFilter }
158
207
"Find nodes where the provided field is equal to the provided value."
159
- eq: {$ filterName } @rules(apply: ["prohibits:any,all,ne,gt,lt,contains"])
208
+ eq: {$ filterName } @rules(apply: ["prohibits:any,all,has, ne,gt,lt,contains"])
160
209
"Find nodes where the provided field is not equal to the provided value."
161
- ne: {$ filterName } @rules(apply: ["prohibits:any,all,eq,gt,lt,contains"])
210
+ ne: {$ filterName } @rules(apply: ["prohibits:any,all,has, eq,gt,lt,contains"])
162
211
"Find nodes where the provided field is greater than the provided value."
163
- gt: {$ filterName } @rules(apply: ["prohibits:any,all,eq,ne,lt,contains"])
212
+ gt: {$ filterName } @rules(apply: ["prohibits:any,all,has, eq,ne,lt,contains"])
164
213
"Find nodes where the provided field is less than the provided value."
165
- lt: {$ filterName } @rules(apply: ["prohibits:any,all,eq,ne,gt,contains"])
214
+ lt: {$ filterName } @rules(apply: ["prohibits:any,all,has, eq,ne,gt,contains"])
166
215
"Find nodes where the provided field contains the provided value."
167
- contains: {$ filterName } @rules(apply: ["prohibits:any,all,eq,ne,gt,lt"])
216
+ contains: {$ filterName } @rules(apply: ["prohibits:any,all,has, eq,ne,gt,lt"])
168
217
}
169
218
GRAPHQL
170
219
);
171
220
}
221
+
222
+ /**
223
+ * @param array<string,string> $subFilterableFields
224
+ *
225
+ * @throws JsonException
226
+ * @throws SyntaxError
227
+ */
228
+ protected function createRelationshipFilterInput (string $ relationshipFilterName , array $ subFilterableFields ): InputObjectTypeDefinitionNode
229
+ {
230
+ $ typeDefinition = "input {$ relationshipFilterName } { " . PHP_EOL ;
231
+ foreach ($ subFilterableFields as $ fieldName => $ fieldType ) {
232
+ $ typeDefinition .= "{$ fieldName }: {$ fieldType }" . PHP_EOL ;
233
+ }
234
+ $ typeDefinition .= '} ' . PHP_EOL ;
235
+ return Parser::inputObjectTypeDefinition ($ typeDefinition );
236
+ }
237
+
238
+ /**
239
+ * Find a list of filterable relationships by finding those which return a list and have a @filter directive.
240
+ *
241
+ * @return array<string,string> A mapping of field names to their respective ...MultiFilterInput types
242
+ */
243
+ private function getSubFilterableFieldsForType (InputValueDefinitionNode $ argDefinition , ObjectTypeDefinitionNode |InterfaceTypeDefinitionNode $ type ): array
244
+ {
245
+ $ subFilterableFieldNames = [];
246
+ foreach ($ type ->fields as $ field ) {
247
+ foreach ($ field ->arguments as $ argument ) {
248
+ foreach ($ argument ->directives as $ directive ) {
249
+ // We abuse the fact that all list return values are paginated to exclude filterable fields which
250
+ // cannot have subqueries.
251
+ if ($ directive ->name ->value === 'filter ' && str_ends_with ($ field ->type ->type ->name ->value , 'Connection ' )) {
252
+ $ subFilterableFieldNames [(string ) $ field ->name ->value ] = ASTHelper::qualifiedArgType ($ argDefinition , $ field , $ type ) . 'MultiFilterInput ' ;
253
+ break 2 ;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return $ subFilterableFieldNames ;
259
+ }
172
260
}
0 commit comments