PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / Connection / PostObjectConnectionResolver.php

PostObjectConnectionResolver.php in WPGraphQL trunk, at src/Data/Connection/PostObjectConnectionResolver.php

711 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Data\Connection;
4
5 use GraphQL\Error\InvariantViolation;
6 use GraphQL\Type\Definition\ResolveInfo;
7 use WPGraphQL\AppContext;
8 use WPGraphQL\Model\Post;
9 use WPGraphQL\Utils\Utils;
10
11 /**
12 * Class PostObjectConnectionResolver
13 *
14 * @package WPGraphQL\Data\Connection
15 * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Query>
16 */
17 class PostObjectConnectionResolver extends AbstractConnectionResolver {
18
19 /**
20 * The name of the post type, or array of post types the connection resolver is resolving for
21 *
22 * @var mixed|string|string[]
23 */
24 protected $post_type;
25
26 /**
27 * {@inheritDoc}
28 *
29 * @param mixed|string|string[] $post_type The post type to resolve for
30 */
31 public function __construct( $source, array $args, AppContext $context, ResolveInfo $info, $post_type = 'any' ) {
32
33 /**
34 * The $post_type can either be a single value or an array of post_types to
35 * pass to WP_Query.
36 *
37 * If the value is revision or attachment, we will leave the value
38 * as a string, as we validate against this later.
39 *
40 * If the value is anything else, we cast as an array. For example
41 *
42 * $post_type = 'post' would become [ 'post ' ], as we check later
43 * for `in_array()` if the $post_type is not "attachment" or "revision"
44 */
45 if ( 'revision' === $post_type || 'attachment' === $post_type ) {
46 $this->post_type = $post_type;
47 } elseif ( 'any' === $post_type ) {
48 $post_types = \WPGraphQL::get_allowed_post_types();
49 $this->post_type = ! empty( $post_types ) ? array_values( $post_types ) : [];
50 } else {
51 $post_type = is_array( $post_type ) ? $post_type : [ $post_type ];
52 unset( $post_type['attachment'] );
53 unset( $post_type['revision'] );
54 $this->post_type = $post_type;
55 }
56
57 /**
58 * Call the parent construct to setup class data
59 */
60 parent::__construct( $source, $args, $context, $info );
61 }
62
63 /**
64 * {@inheritDoc}
65 */
66 protected function loader_name(): string {
67 return 'post';
68 }
69
70 /**
71 * {@inheritDoc}
72 */
73 protected function query_class(): string {
74 return \WP_Query::class;
75 }
76
77 /**
78 * {@inheritDoc}
79 *
80 * @throws \GraphQL\Error\InvariantViolation If the query has been modified to suppress_filters.
81 */
82 protected function query( array $query_args ) {
83 $query = parent::query( $query_args );
84
85 if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) {
86 throw new InvariantViolation( esc_html__( 'WP_Query has been modified by a plugin or theme to suppress_filters, which will cause issues with WPGraphQL Execution. If you need to suppress filters for a specific reason within GraphQL, consider registering a custom field to the WPGraphQL Schema with a custom resolver.', 'wp-graphql' ) );
87 }
88
89 return $query;
90 }
91
92 /**
93 * {@inheritDoc}
94 */
95 public function get_ids_from_query() {
96 /**
97 * @todo This is for b/c. We can just use $this->get_query().
98 */
99 $query = isset( $this->query ) ? $this->query : $this->get_query();
100
101 /** @var int[] */
102 $ids = ! empty( $query->posts ) ? $query->posts : [];
103
104 // If we're going backwards, we need to reverse the array.
105 $args = $this->get_args();
106
107 if ( ! empty( $args['last'] ) ) {
108 $ids = array_reverse( $ids );
109 }
110
111 return $ids;
112 }
113
114 /**
115 * {@inheritDoc}
116 */
117 public function should_execute() {
118 /**
119 * If the post_type is not revision we can just return the parent::should_execute().
120 *
121 * @todo This works because AbstractConnectionResolver::pre_should_execute does a permission check on the `Post` model )
122 */
123 if ( ! isset( $this->post_type ) || 'revision' !== $this->post_type ) {
124 return parent::should_execute();
125 }
126
127 // If the connection is from the RootQuery (i.e. it doesn't have a `Post` source), check if the user has the 'edit_posts' capability.
128 if ( ! $this->source instanceof Post && current_user_can( 'edit_posts' ) ) {
129 return true;
130 }
131
132 // For revisions, we only want to execute the connection query if the user has access to edit the parent post.
133 if ( $this->source instanceof Post && isset( $this->source->post_type ) ) {
134 $parent_post_type_obj = get_post_type_object( $this->source->post_type );
135
136 if ( isset( $parent_post_type_obj->cap->edit_post ) && current_user_can( $parent_post_type_obj->cap->edit_post, $this->source->databaseId ) ) {
137 return true;
138 }
139 }
140
141 return false;
142 }
143
144 /**
145 * {@inheritDoc}
146 */
147 protected function prepare_query_args( array $args ): array {
148 /**
149 * Prepare for later use
150 */
151 $last = ! empty( $args['last'] ) ? $args['last'] : null;
152
153 $query_args = [];
154 /**
155 * Ignore sticky posts by default
156 */
157 $query_args['ignore_sticky_posts'] = true;
158
159 /**
160 * Set the post_type for the query based on the type of post being queried
161 */
162 $query_args['post_type'] = ! empty( $this->post_type ) ? $this->post_type : 'post';
163
164 /**
165 * Don't calculate the total rows, it's not needed and can be expensive
166 */
167 $query_args['no_found_rows'] = true;
168
169 /**
170 * Set the post_status to "publish" by default
171 */
172 $query_args['post_status'] = 'publish';
173
174 /**
175 * Set posts_per_page the highest value of $first and $last, with a (filterable) max of 100
176 */
177 $query_args['posts_per_page'] = $this->one_to_one ? 1 : $this->get_query_amount() + 1;
178
179 // set the graphql cursor args
180 $query_args['graphql_cursor_compare'] = ! empty( $last ) ? '>' : '<';
181 $query_args['graphql_after_cursor'] = $this->get_after_offset();
182 $query_args['graphql_before_cursor'] = $this->get_before_offset();
183
184 /**
185 * If the cursor offsets not empty,
186 * ignore sticky posts on the query
187 */
188 if ( ! empty( $this->get_after_offset() ) || ! empty( $this->get_before_offset() ) ) {
189 $query_args['ignore_sticky_posts'] = true;
190 }
191
192 /**
193 * Pass the graphql $args to the WP_Query
194 */
195 $query_args['graphql_args'] = $args;
196
197 /**
198 * Collect the input_fields and sanitize them to prepare them for sending to the WP_Query
199 */
200 $input_fields = [];
201 if ( ! empty( $args['where'] ) ) {
202 $input_fields = $this->sanitize_input_fields( $args['where'] );
203 }
204
205 /**
206 * If the post_type is "attachment", use 'any' status to catch all attachment statuses.
207 *
208 * Most attachments have 'inherit' status (default), but plugins may change
209 * attachment status to 'publish' or other statuses. Using 'any' ensures we catch
210 * attachments regardless of their status. The Model's is_private() method will handle
211 * privacy checks based on the attachment's parent and status.
212 *
213 * For revisions, keep the default 'inherit' status.
214 */
215 if ( 'attachment' === $this->post_type ) {
216 $query_args['post_status'] = 'any';
217 } elseif ( 'revision' === $this->post_type ) {
218 $query_args['post_status'] = 'inherit';
219 }
220
221 /**
222 * Unset the "post_parent" for attachments, as we don't really care if they
223 * have a post_parent set by default
224 */
225 if ( 'attachment' === $this->post_type && isset( $input_fields['parent'] ) ) {
226 unset( $input_fields['parent'] );
227 }
228
229 /**
230 * Merge the input_fields with the default query_args
231 */
232 if ( ! empty( $input_fields ) ) {
233 $query_args = array_merge( $query_args, $input_fields );
234 }
235
236 /**
237 * If the query is a search, the source is not another Post, and the parent input $arg is not
238 * explicitly set in the query, unset the $query_args['post_parent'] so the search
239 * can search all posts, not just top level posts.
240 *
241 * The search input arg is mapped to `s` before this point (see sanitize_input_fields).
242 */
243 if ( ! $this->source instanceof \WP_Post && isset( $query_args['s'] ) && ! isset( $input_fields['parent'] ) ) {
244 unset( $query_args['post_parent'] );
245 }
246
247 if ( empty( $args['where']['orderby'] ) && ! empty( $query_args['post__in'] ) ) {
248 $post_in = $query_args['post__in'];
249 // Make sure the IDs are integers
250 $post_in = array_map(
251 static function ( $id ) {
252 return absint( $id );
253 },
254 $post_in
255 );
256
257 // If we're coming backwards, let's reverse the IDs
258 if ( ! empty( $args['last'] ) || ! empty( $args['before'] ) ) {
259 $post_in = array_reverse( $post_in );
260 }
261
262 $cursor_offset = $this->get_offset_for_cursor( $args['after'] ?? ( $args['before'] ?? 0 ) );
263
264 if ( ! empty( $cursor_offset ) ) {
265 // Determine if the offset is in the array
266 $key = array_search( $cursor_offset, $post_in, true );
267
268 // If the offset is in the array
269 if ( false !== $key ) {
270 $key = absint( $key );
271 $post_in = array_slice( $post_in, $key + 1, null, true );
272 }
273 }
274
275 $query_args['post__in'] = $post_in;
276 $query_args['orderby'] = 'post__in';
277 $query_args['order'] = isset( $last ) ? 'ASC' : 'DESC';
278 }
279
280 /**
281 * Map the orderby inputArgs to the WP_Query
282 */
283 if ( isset( $args['where']['orderby'] ) && is_array( $args['where']['orderby'] ) ) {
284 $query_args['orderby'] = [];
285
286 foreach ( $args['where']['orderby'] as $orderby_input ) {
287 // Create a type hint for orderby_input. This is an array with a field and order key.
288 /** @var array<string,string> $orderby_input */
289 if ( empty( $orderby_input['field'] ) ) {
290 continue;
291 }
292
293 /**
294 * These orderby options should not include the order parameter.
295 */
296 if ( in_array(
297 $orderby_input['field'],
298 [
299 'post__in',
300 'post_name__in',
301 'post_parent__in',
302 ],
303 true
304 ) ) {
305 $query_args['orderby'] = esc_sql( $orderby_input['field'] );
306
307 // If we're ordering explicitly, there's no reason to check other orderby inputs.
308 break;
309 }
310
311 $order = $orderby_input['order'];
312
313 if ( isset( $query_args['graphql_args']['last'] ) && ! empty( $query_args['graphql_args']['last'] ) ) {
314 if ( 'ASC' === $order ) {
315 $order = 'DESC';
316 } else {
317 $order = 'ASC';
318 }
319 }
320
321 $query_args['orderby'][ esc_sql( $orderby_input['field'] ) ] = esc_sql( $order );
322 }
323 }
324
325 /**
326 * Convert meta_value_num to separate meta_value value field which our
327 * graphql_wp_term_query_cursor_pagination_support knowns how to handle
328 */
329 if ( isset( $query_args['orderby'] ) && 'meta_value_num' === $query_args['orderby'] ) {
330 $query_args['orderby'] = [
331 'meta_value' => empty( $query_args['order'] ) ? 'DESC' : $query_args['order'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
332 ];
333 unset( $query_args['order'] );
334 $query_args['meta_type'] = 'NUMERIC';
335 }
336
337 /**
338 * If there's no orderby params in the inputArgs, set order based on the first/last argument
339 */
340 if ( empty( $query_args['orderby'] ) ) {
341 $query_args['order'] = ! empty( $last ) ? 'ASC' : 'DESC';
342 }
343
344 /**
345 * NOTE: Only IDs should be queried here as the Deferred resolution will handle
346 * fetching the full objects, either from cache of from a follow-up query to the DB
347 */
348 $query_args['fields'] = 'ids';
349
350 /**
351 * Filter the $query args to allow folks to customize queries programmatically
352 *
353 * @param array<string,mixed> $query_args The args that will be passed to the WP_Query
354 * @param mixed $source The source that's passed down the GraphQL queries
355 * @param array<string,mixed> $args The inputArgs on the field
356 * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree
357 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree
358 *
359 * @hookGroup connections
360 * @since 0.0.6
361 */
362 return apply_filters( 'graphql_post_object_connection_query_args', $query_args, $this->source, $args, $this->context, $this->info );
363 }
364
365 /**
366 * This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_Query
367 * friendly keys. There's probably a cleaner/more dynamic way to approach this, but
368 * this was quick. I'd be down to explore more dynamic ways to map this, but for
369 * now this gets the job done.
370 *
371 * @param array<string,mixed> $where_args The args passed to the connection
372 *
373 * @return array<string,mixed>
374 * @since 0.0.5
375 */
376 public function sanitize_input_fields( array $where_args ) {
377 $arg_mapping = [
378 'authorIn' => 'author__in',
379 'authorName' => 'author_name',
380 'authorNotIn' => 'author__not_in',
381 'categoryId' => 'cat',
382 'categoryIn' => 'category__in',
383 'categoryName' => 'category_name',
384 'categoryNotIn' => 'category__not_in',
385 'contentTypes' => 'post_type',
386 'dateQuery' => 'date_query',
387 'hasPassword' => 'has_password',
388 'id' => 'p',
389 'in' => 'post__in',
390 'mimeType' => 'post_mime_type',
391 'nameIn' => 'post_name__in',
392 'notIn' => 'post__not_in',
393 'parent' => 'post_parent',
394 'parentIn' => 'post_parent__in',
395 'parentNotIn' => 'post_parent__not_in',
396 'password' => 'post_password',
397 'search' => 's',
398 'stati' => 'post_status',
399 'status' => 'post_status',
400 'tagId' => 'tag_id',
401 'tagIds' => 'tag__and',
402 'tagIn' => 'tag__in',
403 'tagNotIn' => 'tag__not_in',
404 'tagSlugAnd' => 'tag_slug__and',
405 'tagSlugIn' => 'tag_slug__in',
406 ];
407
408 /**
409 * Map and sanitize the input args to the WP_Query compatible args
410 */
411 $query_args = Utils::map_input( $where_args, $arg_mapping );
412
413 if ( ! empty( $query_args['post_status'] ) ) {
414 $allowed_stati = $this->sanitize_post_stati( $query_args['post_status'] );
415 $query_args['post_status'] = ! empty( $allowed_stati ) ? $allowed_stati : [ 'publish' ];
416 }
417
418 /**
419 * Filter the result set by sticky posts.
420 *
421 * WPGraphQL never floats sticky posts to the top of the results (ignore_sticky_posts
422 * is always true, which is also cursor-pagination safe). Instead `isSticky` filters
423 * the result set, modeling the WP REST API `sticky` parameter: true limits the query
424 * to sticky posts, false excludes them.
425 */
426 if ( isset( $where_args['isSticky'] ) ) {
427 $sticky_posts = get_option( 'sticky_posts', [] );
428 $sticky_posts = is_array( $sticky_posts ) ? array_map( 'absint', $sticky_posts ) : [];
429
430 if ( true === $where_args['isSticky'] ) {
431 // Limit to sticky posts, intersecting with any explicit `in` filter.
432 $query_args['post__in'] = ! empty( $query_args['post__in'] )
433 ? array_intersect( $sticky_posts, $query_args['post__in'] )
434 : $sticky_posts;
435
436 // WP_Query ignores an empty post__in, so force an impossible ID to return no results.
437 if ( empty( $query_args['post__in'] ) ) {
438 $query_args['post__in'] = [ 0 ];
439 }
440 } elseif ( ! empty( $sticky_posts ) ) {
441 // Exclude sticky posts, merging with any explicit `notIn` filter.
442 $query_args['post__not_in'] = array_merge( // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
443 ! empty( $query_args['post__not_in'] ) ? $query_args['post__not_in'] : [],
444 $sticky_posts
445 );
446 }
447 }
448
449 /**
450 * Filter the connection by the per-post template assignment (`_wp_page_template`),
451 * which covers both classic page templates and block-theme custom templates.
452 *
453 * This is the one place a core `where` arg introduces a meta_query. It is bounded to a
454 * single indexed meta key, but a meta_query is still more expensive than the indexed
455 * post columns the other args use.
456 *
457 * TODO (Query Cost): when the query complexity / cost analysis system lands
458 * (see plans/001-query-complexity-validation-rule.md and the filter/sort RFC #1385),
459 * this arg MUST be assigned a cost weighting so meta_query-backed filters can be priced
460 * and guarded. It is intentionally shipped without one now because that system does not
461 * yet exist.
462 */
463 if ( isset( $where_args['template'] ) && is_string( $where_args['template'] ) && '' !== $where_args['template'] ) {
464 if ( 'default' === $where_args['template'] ) {
465 // Content on the default template has no specific template assigned: the meta is
466 // absent, empty, or the literal "default".
467 $query_args['meta_query'][] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query, SlevomatCodingStandard.Arrays.DisallowPartiallyKeyed.DisallowedPartiallyKeyed
468 'relation' => 'OR',
469 [
470 'key' => '_wp_page_template',
471 'compare' => 'NOT EXISTS',
472 ],
473 [
474 'key' => '_wp_page_template',
475 'value' => [ '', 'default' ],
476 'compare' => 'IN',
477 ],
478 ];
479 } else {
480 $query_args['meta_query'][] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
481 'key' => '_wp_page_template',
482 'value' => $where_args['template'],
483 ];
484 }
485 }
486
487 /**
488 * Filter the input fields
489 * This allows plugins/themes to hook in and alter what $args should be allowed to be passed
490 * from a GraphQL Query to the WP_Query
491 *
492 * @param array<string,mixed> $query_args The mapped query arguments
493 * @param array<string,mixed> $args Query "where" args
494 * @param mixed $source The query results for a query calling this
495 * @param array<string,mixed> $all_args All of the arguments for the query (not just the "where" args)
496 * @param \WPGraphQL\AppContext $context The AppContext object
497 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
498 * @param mixed|string|string[] $post_type The post type for the query
499 *
500 * @hookGroup connections
501 * @since 0.0.5
502 */
503 $query_args = apply_filters( 'graphql_map_input_fields_to_wp_query', $query_args, $where_args, $this->source, $this->get_args(), $this->context, $this->info, $this->post_type );
504
505 /**
506 * Return the Query Args
507 */
508 return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
509 }
510
511 /**
512 * Limit the status of posts a user can query.
513 *
514 * By default, published posts are public, and other statuses require permission to access.
515 *
516 * This strips the status from the query_args if the user doesn't have permission to query for
517 * posts of that status.
518 *
519 * @param string[]|string $stati The status(es) to sanitize.
520 *
521 * @return string[]|null
522 */
523 public function sanitize_post_stati( $stati ) {
524 /**
525 * If no stati is explicitly set by the input, default to publish. This will be the
526 * most common scenario.
527 */
528 if ( empty( $stati ) ) {
529 $stati = [ 'publish' ];
530 }
531
532 /**
533 * Parse the list of stati
534 */
535 $statuses = wp_parse_slug_list( $stati );
536
537 /**
538 * Get the Post Type object
539 */
540 $post_type_objects = [];
541 if ( is_array( $this->post_type ) ) {
542 foreach ( $this->post_type as $post_type ) {
543 $post_type_objects[] = get_post_type_object( $post_type );
544 }
545 } else {
546 $post_type_objects[] = get_post_type_object( $this->post_type );
547 }
548
549 /**
550 * Make sure the statuses are allowed to be queried by the current user. If so, allow it,
551 * otherwise remove it from the $allowed_statuses that will be passed to WP_Query.
552 *
553 * For connections spanning multiple post types, a status is only allowed if the current
554 * user can query it for every post type in the connection (the most restrictive choice).
555 */
556 $allowed_statuses = array_values(
557 array_filter(
558 $statuses,
559 function ( $status ) use ( $post_type_objects ) {
560 foreach ( $post_type_objects as $post_type_object ) {
561 if ( ! $this->can_query_post_status( (string) $status, $post_type_object ) ) {
562 return false;
563 }
564 }
565
566 return true;
567 }
568 )
569 );
570
571 /**
572 * Filters the post statuses the current user is allowed to query in this connection.
573 *
574 * Allows extensions to adjust which statuses are queryable (for example to expose an
575 * additional custom status to specific users, or to further restrict access).
576 *
577 * @param string[] $allowed_statuses The statuses determined to be queryable by the current user.
578 * @param string[] $requested_statuses The statuses requested via the connection `stati` where arg.
579 * @param array<\WP_Post_Type|null> $post_type_objects The post type objects the connection resolves.
580 * @param \WPGraphQL\Data\Connection\PostObjectConnectionResolver $resolver The connection resolver instance.
581 *
582 * @hookGroup connections
583 * @since 2.17.0
584 */
585 $allowed_statuses = apply_filters( 'graphql_allowed_post_stati', $allowed_statuses, $statuses, $post_type_objects, $this );
586
587 /**
588 * If there are no allowed statuses to pass to WP_Query, prevent the connection
589 * from executing
590 *
591 * For example, if a subscriber tries to query:
592 *
593 * {
594 * posts( where: { stati: [ DRAFT ] } ) {
595 * ...fields
596 * }
597 * }
598 *
599 * We can safely prevent the execution of the query because they are asking for content
600 * in a status that we know they can't ask for.
601 */
602 if ( empty( $allowed_statuses ) ) {
603 $this->should_execute = false;
604 }
605
606 /**
607 * Return the $allowed_statuses to the query args
608 */
609 return $allowed_statuses;
610 }
611
612 /**
613 * Determines whether the current user can query posts of the given status for the given post type.
614 *
615 * Published content, and any status whose `public` flag is true, are queryable by everyone,
616 * the same way they are exposed on the WordPress front-end and the REST API (e.g. custom
617 * statuses registered with `'public' => true`). The `private` status requires the post type's
618 * `read_private_posts` capability. All other statuses (draft, pending, future, trash, and
619 * custom non-public statuses) require the post type's `edit_posts` capability.
620 *
621 * @param string $status The post status to check.
622 * @param \WP_Post_Type|null $post_type_object The post type object the status is being queried for.
623 */
624 protected function can_query_post_status( string $status, $post_type_object ): bool {
625 // An unregistered/invalid post type can't be queried.
626 if ( ! $post_type_object instanceof \WP_Post_Type ) {
627 return false;
628 }
629
630 if ( 'publish' === $status ) {
631 return true;
632 }
633
634 // A status flagged public is queryable by anyone, mirroring the WP front-end and REST API.
635 $status_object = get_post_status_object( $status );
636 if ( $status_object instanceof \stdClass && true === $status_object->public ) {
637 return true;
638 }
639
640 // The private status is queryable by users who can read private posts (CPT-aware via the cap object).
641 if ( 'private' === $status ) {
642 return isset( $post_type_object->cap->read_private_posts ) && current_user_can( $post_type_object->cap->read_private_posts );
643 }
644
645 // All other statuses require edit capabilities.
646 return isset( $post_type_object->cap->edit_posts ) && current_user_can( $post_type_object->cap->edit_posts );
647 }
648
649 /**
650 * {@inheritDoc}
651 */
652 protected function prepare_args( array $args ): array {
653 if ( ! empty( $args['where'] ) ) {
654 // Ensure all IDs are converted to database IDs.
655 foreach ( $args['where'] as $input_key => $input_value ) {
656 if ( empty( $input_value ) ) {
657 continue;
658 }
659
660 switch ( $input_key ) {
661 case 'in':
662 case 'notIn':
663 case 'parent':
664 case 'parentIn':
665 case 'parentNotIn':
666 case 'authorIn':
667 case 'authorNotIn':
668 case 'categoryIn':
669 case 'categoryNotIn':
670 case 'tagId':
671 case 'tagIn':
672 case 'tagNotIn':
673 if ( is_array( $input_value ) ) {
674 $args['where'][ $input_key ] = array_map(
675 static function ( $id ) {
676 return Utils::get_database_id_from_id( $id );
677 },
678 $input_value
679 );
680 break;
681 }
682
683 $args['where'][ $input_key ] = Utils::get_database_id_from_id( $input_value );
684 break;
685 }
686 }
687 }
688
689 /**
690 * Filters the GraphQL args before they are used in get_query_args().
691 *
692 * @param array<string,mixed> $args The GraphQL args passed to the resolver.
693 * @param self $resolver Instance of the ConnectionResolver.
694 * @param array<string,mixed> $unfiltered_args Array of arguments input in the field as part of the GraphQL query.
695 *
696 * @hookGroup connections
697 * @since 1.11.0
698 */
699 return apply_filters( 'graphql_post_object_connection_args', $args, $this, $this->get_unfiltered_args() );
700 }
701
702 /**
703 * {@inheritDoc}
704 *
705 * @param int $offset The ID of the node used in the cursor offset.
706 */
707 public function is_valid_offset( $offset ) {
708 return (bool) get_post( absint( $offset ) );
709 }
710 }
711