Added dependency plugins

This commit is contained in:
Moris Zen
2018-06-25 00:00:37 +02:00
parent 720a1c31a4
commit f069f6782f
698 changed files with 289637 additions and 1 deletions

View File

@@ -0,0 +1,53 @@
<?php
namespace WPGraphQL;
/**
* Class AppContext
* Creates an object that contains all of the context for the GraphQL query
* This class gets instantiated and populated in the main WPGraphQL class
*
* @package WPGraphQL
*/
class AppContext {
/**
* Stores the url string for the current site
*
* @var string $root_url
* @access public
*/
public $root_url;
/**
* Stores the WP_User object of the current user
*
* @var \WP_User $viewer
* @access public
*/
public $viewer;
/**
* Stores everything from the $_REQUEST global
*
* @var \mixed $request
* @access public
*/
public $request;
/**
* Stores additional $config properties
* @var \mixed $config
* @access public
*/
public $config;
/**
* AppContext constructor.
*/
public function __construct() {
$this->config = apply_filters( 'graphql_app_context_config', $this->config );
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace WPGraphQL\Data;
/**
* Class Config
*
* This class contains configurations for various data-related things, such as query filters for cursor pagination.
*
* @package WPGraphQL\Data
*/
class Config {
/**
* Config constructor.
*/
public function __construct() {
/**
* Filter the term_clauses in the WP_Term_Query to allow for cursor pagination support where a Term ID
* can be used as a point of comparison when slicing the results to return.
*/
add_filter( 'comments_clauses', [ $this, 'graphql_wp_comments_query_cursor_pagination_support' ], 10, 2 );
/**
* Filter the WP_Query to support cursor based pagination where a post ID can be used
* as a point of comparison when slicing the results to return.
*/
add_filter( 'posts_where', [ $this, 'graphql_wp_query_cursor_pagination_support' ], 10, 2 );
/**
* Filter the term_clauses in the WP_Term_Query to allow for cursor pagination support where a Term ID
* can be used as a point of comparison when slicing the results to return.
*/
add_filter( 'terms_clauses', [ $this, 'graphql_wp_term_query_cursor_pagination_support' ], 10, 3 );
}
/**
* This filters the WPQuery 'where' $args, enforcing the query to return results before or
* after the referenced cursor
*
* @param string $where The WHERE clause of the query.
* @param \WP_Query $query The WP_Query instance (passed by reference).
*
* @return string
*/
public function graphql_wp_query_cursor_pagination_support( $where, \WP_Query $query ) {
/**
* Access the global $wpdb object
*/
global $wpdb;
/**
* If there's a graphql_cursor_offset in the query, we should check to see if
* it should be applied to the query
*/
if ( defined( 'GRAPHQL_REQUEST' ) && GRAPHQL_REQUEST ) {
$cursor_offset = ! empty( $query->query_vars['graphql_cursor_offset'] ) ? $query->query_vars['graphql_cursor_offset'] : 0;
/**
* Ensure the cursor_offset is a positive integer
*/
if ( is_integer( $cursor_offset ) && 0 < $cursor_offset ) {
$compare = ! empty( $query->get( 'graphql_cursor_compare' ) ) ? $query->get( 'graphql_cursor_compare' ) : '>';
$compare = in_array( $compare, [ '>', '<' ], true ) ? $compare : '>';
$compare_opposite = ( '<' === $compare ) ? '>' : '<';
// Get the $cursor_post
$cursor_post = get_post( $cursor_offset );
/**
* If the $cursor_post exists (hasn't been deleted), modify the query to compare based on the ID and post_date values
* But if the $cursor_post no longer exists, we're forced to just compare with the ID
*
*/
if ( ! empty( $cursor_post ) && ! empty( $cursor_post->post_date ) ) {
$orderby = $query->get( 'orderby' );
if ( ! empty( $orderby ) && is_array( $orderby ) ) {
foreach ( $orderby as $by => $order ) {
$order_compare = ( 'ASC' === $order ) ? '>' : '<';
$value = $cursor_post->{$by};
if ( ! empty( $by ) && ! empty( $value ) ) {
$where .= $wpdb->prepare( " AND {$wpdb->posts}.{$by} {$order_compare} %s", $value );
}
}
} else {
$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_date {$compare}= %s AND {$wpdb->posts}.ID != %d", esc_sql( $cursor_post->post_date ), absint( $cursor_offset ) );
}
} else {
$where .= $wpdb->prepare( " AND {$wpdb->posts}.ID {$compare} %d", $cursor_offset );
}
}
}
return $where;
}
/**
* This filters the term_clauses in the WP_Term_Query to support cursor based pagination, where we can
* move forward or backward from a particular record, instead of typical offset pagination which can be
* much more expensive and less accurate.
*
* @param array $pieces Terms query SQL clauses.
* @param array $taxonomies An array of taxonomies.
* @param array $args An array of terms query arguments.
*
* @return array $pieces
*/
public function graphql_wp_term_query_cursor_pagination_support( array $pieces, $taxonomies, $args ) {
/**
* Access the global $wpdb object
*/
global $wpdb;
if ( defined( 'GRAPHQL_REQUEST' ) && GRAPHQL_REQUEST && ! empty( $args['graphql_cursor_offset'] ) ) {
$cursor_offset = $args['graphql_cursor_offset'];
/**
* Ensure the cursor_offset is a positive integer
*/
if ( is_integer( $cursor_offset ) && 0 < $cursor_offset ) {
$compare = ! empty( $args['graphql_cursor_compare'] ) ? $args['graphql_cursor_compare'] : '>';
$compare = in_array( $compare, [ '>', '<' ], true ) ? $compare : '>';
$order_by = ! empty( $args['orderby'] ) ? $args['orderby'] : 'comment_date';
$order = ! empty( $args['order'] ) ? $args['order'] : 'DESC';
$order_compare = ( 'ASC' === $order ) ? '>' : '<';
// Get the $cursor_post
$cursor_term = get_term( $cursor_offset );
if ( ! empty( $cursor_term ) && ! empty( $cursor_term->name ) ) {
$pieces['where'] .= $wpdb->prepare( " AND t.{$order_by} {$order_compare} %s", $cursor_term->{$order_by} );
} else {
$pieces['where'] .= $wpdb->prepare( ' AND t.term_id %1$s %2$d', $compare, $cursor_offset );
}
}
}
return $pieces;
}
/**
* This returns a modified version of the $pieces of the comment query clauses if the request
* is a GRAPHQL_REQUEST and the query has a graphql_cursor_offset defined
*
* @param array $pieces A compacted array of comment query clauses.
* @param \WP_Comment_Query $query Current instance of WP_Comment_Query, passed by reference.
*
* @return array $pieces
*/
public function graphql_wp_comments_query_cursor_pagination_support( array $pieces, \WP_Comment_Query $query ) {
/**
* Access the global $wpdb object
*/
global $wpdb;
if (
defined( 'GRAPHQL_REQUEST' ) && GRAPHQL_REQUEST &&
( is_array( $query->query_vars ) && array_key_exists( 'graphql_cursor_offset', $query->query_vars ) )
) {
$cursor_offset = $query->query_vars['graphql_cursor_offset'];
/**
* Ensure the cursor_offset is a positive integer
*/
if ( is_integer( $cursor_offset ) && 0 < $cursor_offset ) {
$compare = ! empty( $query->get( 'graphql_cursor_compare' ) ) ? $query->get( 'graphql_cursor_compare' ) : '>';
$compare = in_array( $compare, [ '>', '<' ], true ) ? $compare : '>';
$order_by = ! empty( $query->query_vars['order_by'] ) ? $query->query_vars['order_by'] : 'comment_date';
$order = ! empty( $query->query_vars['order'] ) ? $query->query_vars['order'] : 'DESC';
$order_compare = ( 'ASC' === $order ) ? '>' : '<';
// Get the $cursor_post
$cursor_comment = get_comment( $cursor_offset );
if ( ! empty( $cursor_comment ) ) {
$pieces['where'] .= $wpdb->prepare( " AND {$order_by} {$order_compare} %s", $cursor_comment->{$order_by} );
} else {
$pieces['where'] .= $wpdb->prepare( ' AND comment_ID %1$s %2$d', $compare, $cursor_offset );
}
}
}
return $pieces;
}
}

View File

@@ -0,0 +1,319 @@
<?php
namespace WPGraphQL\Data;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Connection\ArrayConnection;
use WPGraphQL\AppContext;
/**
* Class Connections
*
* This class is meant to be extended by ConnectionResolvers
*
* @package WPGraphQL\Data
*/
abstract class ConnectionResolver implements ConnectionResolverInterface {
/**
* Runs the query for comments
*
* @param mixed $source Data returned from the query
* @param array $args Args for the query
* @param AppContext $context AppContext object for the query
* @param ResolveInfo $info ResolveInfo object
*
* @return array
* @since 0.5.0
* @throws \Exception
* @access public
*/
public static function resolve( $source, $args, AppContext $context, ResolveInfo $info ) {
$query_args = static::get_query_args( $source, $args, $context, $info );
$query = static::get_query( $query_args );
$array_slice = self::get_array_slice( $query, $args );
$connection = static::get_connection( $query, $array_slice, $source, $args, $context, $info );
/**
* Filter the connection, and provide heaps of info to make it easy to filter very specific cases
*
* @param array $connection The connection to return
* @param array $array_slice The Array to create the connection from
* @param mixed $query The query that was run to retrieve the items
* @param array $query_args The args that were used by the query
* @param mixed $source The source being passed down the GraphQL tree
* @param array $args The args that were input for the specific GraphQL query
* @param AppContext object $context The AppContext that gets passed down the GraphQL tree
* @param ResolveInfo object $info The ResolveInfo object that gets passed down the GraphQL tree
*
*/
$connection = apply_filters(
'graphql_connection_resolver_resolve',
$connection,
$array_slice,
$query,
$query_args,
$source,
$args,
$context,
$info
);
return $connection;
}
/**
* Take an array return a connection
*
* @param mixed $query The query being performed
* @param array $array The array of connection items
* @param mixed $source The source being passed down the resolve tree
* @param array $args The args for the field being resolved
* @param AppContext $context The context being passed down the Resolve tree
* @param ResolveInfo $info The ResolveInfo for the field being resolved
*
* @return array
*/
public static function get_connection( $query, array $array, $source, array $args, AppContext $context, ResolveInfo $info ) {
$meta = self::get_array_meta( $query, $args );
$connection = ArrayConnection::connectionFromArraySlice( $array, $args, $meta );
$connection['nodes'] = $array;
return $connection;
}
/**
* This returns a slice of the query results based on the posts retrieved and the $args passed to the query
*
* @param mixed $query The query that was made to fetch the items
* @param array $args array of arguments input in the field as part of the GraphQL query
*
* @return array
*/
public static function get_array_slice( $query, array $args ) {
$info = self::get_query_info( $query );
$items = $info['items'];
$array_slice = [];
if ( ! empty( $items ) && is_array( $items ) ) {
foreach ( $items as $item ) {
if ( true === is_object( $item ) ) {
switch ( true ) {
case $item instanceof \WP_Comment:
$array_slice[ $item->comment_ID ] = $item;
break;
case $item instanceof \WP_Term:
$array_slice[ $item->term_id ] = $item;
break;
case $item instanceof \WP_Post:
$array_slice[ $item->ID ] = $item;
break;
// the \WP_User_Query doesn't have proper filters to allow for true cursor based pagination
case $item instanceof \WP_User:
$array_slice[] = $item;
break;
default:
$array_slice = $items;
}
}
}
}
return $array_slice;
}
/**
* This checks the $args to determine the amount requested, and if
*
* @param array $args array of arguments input in the field as part of the GraphQL query
*
* @return int|null
* @throws \Exception
*/
public static function get_amount_requested( array $args ) {
/**
* Set the default amount
*/
$amount_requested = 10;
/**
* If both first & last are used in the input args, throw an exception as that won't
* work properly
*/
if ( ! empty( $args['first'] ) && ! empty( $args['last'] ) ) {
throw new UserError( esc_html__( 'first and last cannot be used together. For forward pagination, use first & after. For backward pagination, use last & before.', 'wp-graphql' ) );
}
/**
* If first is set, and is a positive integer, use it for the $amount_requested
* but if it's set to anything that isn't a positive integer, throw an exception
*/
if ( ! empty( $args['first'] ) && is_int( $args['first'] ) ) {
if ( 0 > $args['first'] ) {
throw new UserError( esc_html__( 'first must be a positive integer.', 'wp-graphql' ) );
} else {
$amount_requested = $args['first'];
}
}
/**
* If last is set, and is a positive integer, use it for the $amount_requested
* but if it's set to anything that isn't a positive integer, throw an exception
*/
if ( ! empty( $args['last'] ) && is_int( $args['last'] ) ) {
if ( 0 > $args['last'] ) {
throw new UserError( esc_html__( 'last must be a positive integer.', 'wp-graphql' ) );
} else {
$amount_requested = $args['last'];
}
}
return max( 0, $amount_requested );
}
/**
* get_query_amount
*
* Returns the max between what was requested and what is defined as the $max_query_amount to ensure that
* queries don't exceed unwanted limits when querying data.
*
* @param $source
* @param $args
* @param $context
* @param $info
*
* @return mixed
*/
public static function get_query_amount( $source, $args, $context, $info ) {
/**
* Filter the maximum number of posts per page that should be quried. The default is 100 to prevent queries from
* being exceedingly resource intensive, however individual systems can override this for their specific needs.
*
* This filter is intentionally applied AFTER the query_args filter, as
*
* @param array $query_args array of query_args being passed to the
* @param mixed $source source passed down from the resolve tree
* @param array $args array of arguments input in the field as part of the GraphQL query
* @param AppContext $context Object containing app context that gets passed down the resolve tree
* @param ResolveInfo $info Info about fields passed down the resolve tree
*
* @since 0.0.6
*/
$max_query_amount = apply_filters( 'graphql_connection_max_query_amount', 100, $source, $args, $context, $info );
return min( $max_query_amount, absint( self::get_amount_requested( $args ) ) );
}
/**
* This returns a meta array to be used in preparing the connection edges
*
* @param mixed $query The query that was made to fetch the items
* @param array $args array of arguments input in the field as part of the GraphQL query
*
* @return array
*/
public static function get_array_meta( $query, $args ) {
$info = self::get_query_info( $query );
$meta = [
'sliceStart' => max( 0, absint( self::get_offset( $args ) ) ),
'arrayLength' => absint( max( 0, $info['total_items'], count( $info['items'] ) ) ),
];
return $meta;
}
/**
* This returns the offset to be used in the $query_args based on the $args passed to the GraphQL query.
*
* @param $args
*
* @return int|mixed
*/
public static function get_offset( $args ) {
/**
* Defaults
*/
$offset = 0;
/**
* Get the $after offset
*/
if ( ! empty( $args['after'] ) ) {
$offset = ArrayConnection::cursorToOffset( $args['after'] );
} elseif ( ! empty( $args['before'] ) ) {
$offset = ArrayConnection::cursorToOffset( $args['before'] );
}
/**
* Return the higher of the two values
*/
return max( 0, $offset );
}
/**
* WordPress has different queries that return date in different shapes. This normalizes the return
* for re-use.
*
* @param mixed $query The query that was made to fetch the items
*
* @return integer
*/
public static function get_query_info( $query ) {
/**
* Set the default values to return
*/
$query_info = [
'total_items' => 0,
'items' => [],
];
if ( true === is_object( $query ) ) {
switch ( true ) {
case $query instanceof \WP_Query:
$found_posts = $query->posts;
$query_info['total_items'] = ! empty( $found_posts ) ? count( $found_posts ) : null;
$query_info['items'] = $found_posts;
break;
case $query instanceof \WP_Comment_Query:
$found_comments = $query->get_comments();
$query_info['total_items'] = ! empty( $found_comments ) ? count( $found_comments ) : null;
$query_info['items'] = ! empty( $found_comments ) ? $found_comments : [];
break;
case $query instanceof \WP_Term_Query:
$query_info['total_items'] = ! empty( $query->query_vars['taxonomy'] ) ? wp_count_terms( $query->query_vars['taxonomy'][0], $query->query_vars ) : 0;
$query_info['items'] = $query->get_terms();
break;
case $query instanceof \WP_User_Query:
$query_info['total_items'] = ! empty( $query->get_total() ) ? $query->get_total() : count( $query->get_results() );
$query_info['items'] = $query->get_results();
break;
}
}
/**
* Filter the items count after a query has been made
*
* @param int $items_count The number of items matching the query
* @param mixed $query The query that was made to fetch the items
*/
$query_info = apply_filters( 'graphql_connection_query_info', $query_info, $query );
/**
* Return the $count
*/
return $query_info;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WPGraphQL\Data;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
/**
* Class Connections
*
* This class provides some helper methods to make creating connections easier.
*
* @package WPGraphQL\Data
*/
interface ConnectionResolverInterface {
public static function get_query( $query_args );
/**
* Placeholder class that should be implemented by the extending class.
*
* This prepares the $query_args for use in the connection query. This is where default $args are set, where dynamic
* $args from the $source get set, and where mapping the input $args to the actual $query_args occurs.
*
* @param $source
* @param array $args
* @param AppContext $context
* @param ResolveInfo $info
*
* @return array
*/
public static function get_query_args( $source, array $args, AppContext $context, ResolveInfo $info );
/**
* Placeholder class that should be implemented by the extending class.
*
* This validates, sanitizes and maps the input $args and maps them to the appropriate WP Query class
* (WP_Query, WP_Comment_Query, etc) ensuring the args being processed by the query are "safe"
* to process.
*
* @param $args
* @param $source
* @param $all_args
* @param $context
* @param $info
*
* @return array
*/
public static function sanitize_input_fields( array $args, $source, array $all_args, AppContext $context, ResolveInfo $info );
}

View File

@@ -0,0 +1,716 @@
<?php
namespace WPGraphQL\Data;
use GraphQL\Deferred;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionResolver;
use WPGraphQL\Type\Comment\Connection\CommentConnectionResolver;
use WPGraphQL\Type\Plugin\Connection\PluginConnectionResolver;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionResolver;
use WPGraphQL\Type\Theme\Connection\ThemeConnectionResolver;
use WPGraphQL\Type\User\Connection\UserConnectionResolver;
use WPGraphQL\Types;
/**
* Class DataSource
*
* This class serves as a factory for all the resolvers for queries and mutations. This layer of
* abstraction over the actual resolve functions allows easier, granular control over versioning as
* we can change big things behind the scenes if/when needed, and we just need to ensure the
* call to the DataSource method returns the expected data later on. This should make it easy
* down the road to version resolvers if/when changes to the WordPress API are rolled out.
*
* @package WPGraphQL\Data
* @since 0.0.4
*/
class DataSource {
/**
* Stores an array of node definitions
*
* @var array $node_definition
* @since 0.0.4
* @access protected
*/
protected static $node_definition;
/**
* Retrieves a WP_Comment object for the id that gets passed
*
* @param int $id ID of the comment we want to get the object for
*
* @return \WP_Comment object
* @throws UserError
* @since 0.0.5
* @access public
*/
public static function resolve_comment( $id ) {
$comment = \WP_Comment::get_instance( $id );
if ( empty( $comment ) ) {
throw new UserError( sprintf( __( 'No comment was found with ID %d', 'wp-graphql' ), absint( $id ) ) );
}
return $comment;
}
/**
* Retrieves a WP_Comment object for the ID that gets passed
*
* @param string $author_email The ID of the comment the comment author is associated with.
*
* @return array
* @throws
*/
public static function resolve_comment_author( $author_email ) {
global $wpdb;
$comment_author = $wpdb->get_row( $wpdb->prepare( "SELECT comment_author_email, comment_author, comment_author_url, comment_author_email from $wpdb->comments WHERE comment_author_email = %s LIMIT 1", esc_sql( $author_email ) ) );
$comment_author = ! empty( $comment_author ) ? ( array ) $comment_author : [];
$comment_author['is_comment_author'] = true;
return $comment_author;
}
/**
* Wrapper for the CommentsConnectionResolver class
*
* @param WP_Post object $source
* @param array $args Query args to pass to the connection resolver
* @param AppContext $context The context of the query to pass along
* @param ResolveInfo $info The ResolveInfo object
*
* @return mixed
* @since 0.0.5
*/
public static function resolve_comments_connection( $source, array $args, $context, ResolveInfo $info ) {
$resolver = new CommentConnectionResolver();
return $resolver->resolve( $source, $args, $context, $info );
}
/**
* Returns an array of data about the plugin you are requesting
*
* @param string $name Name of the plugin you want info for
*
* @return null|array
* @throws \Exception
* @since 0.0.5
* @access public
*/
public static function resolve_plugin( $name ) {
// Puts input into a url friendly slug format.
$slug = sanitize_title( $name );
$plugin = null;
// The file may have not been loaded yet.
require_once ABSPATH . 'wp-admin/includes/plugin.php';
/**
* NOTE: This is missing must use and drop in plugins.
*/
$plugins = apply_filters( 'all_plugins', get_plugins() );
/**
* Loop through the plugins and find the matching one
*
* @since 0.0.5
*/
foreach ( $plugins as $path => $plugin_data ) {
if ( sanitize_title( $plugin_data['Name'] ) === $slug ) {
$plugin = $plugin_data;
$plugin['path'] = $path;
// Exit early when plugin is found.
break;
}
}
/**
* Return the plugin, or throw an exception
*/
if ( ! empty( $plugin ) ) {
return $plugin;
} else {
throw new UserError( sprintf( __( 'No plugin was found with the name %s', 'wp-graphql' ), $name ) );
}
}
/**
* Wrapper for PluginsConnectionResolver::resolve
*
* @param \WP_Post $source WP_Post object
* @param array $args Array of arguments to pass to reolve method
* @param AppContext $context AppContext object passed down
* @param ResolveInfo $info The ResolveInfo object
*
* @return array
* @since 0.0.5
* @access public
*/
public static function resolve_plugins_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
return PluginConnectionResolver::resolve( $source, $args, $context, $info );
}
/**
* Returns the post object for the ID and post type passed
*
* @param int $id ID of the post you are trying to retrieve
* @param string $post_type Post type the post is attached to
*
* @throws UserError
* @since 0.0.5
* @return \WP_Post
* @access public
*/
public static function resolve_post_object( $id, $post_type ) {
$post_object = \WP_Post::get_instance( $id );
if ( empty( $post_object ) ) {
throw new UserError( sprintf( __( 'No %1$s was found with the ID: %2$s', 'wp-graphql' ), $id, $post_type ) );
}
/**
* Set the resolving post to the global $post. That way any filters that
* might be applied when resolving fields can rely on global post and
* post data being set up.
*/
$GLOBALS['post'] = $post_object;
return $post_object;
}
/**
* Wrapper for PostObjectsConnectionResolver
*
* @param string $post_type Post type of the post we are trying to resolve
* @param $source
* @param array $args Arguments to pass to the resolve method
* @param AppContext $context AppContext object to pass down
* @param ResolveInfo $info The ResolveInfo object
*
* @return mixed
* @since 0.0.5
* @access public
*/
public static function resolve_post_objects_connection( $source, array $args, AppContext $context, ResolveInfo $info, $post_type ) {
$resolver = new PostObjectConnectionResolver( $post_type );
return $resolver->resolve( $source, $args, $context, $info );
}
/**
* Gets the post type object from the post type name
*
* @param string $post_type Name of the post type you want to retrieve the object for
*
* @return \WP_Post_Type object
* @throws UserError
* @since 0.0.5
* @access public
*/
public static function resolve_post_type( $post_type ) {
/**
* Get the allowed_post_types
*/
$allowed_post_types = \WPGraphQL::get_allowed_post_types();
/**
* If the $post_type is one of the allowed_post_types
*/
if ( in_array( $post_type, $allowed_post_types, true ) ) {
return get_post_type_object( $post_type );
} else {
throw new UserError( sprintf( __( 'No post_type was found with the name %s', 'wp-graphql' ), $post_type ) );
}
}
/**
* Retrieves the taxonomy object for the name of the taxonomy passed
*
* @param string $taxonomy Name of the taxonomy you want to retrieve the taxonomy object for
*
* @return \WP_Taxonomy object
* @throws UserError
* @since 0.0.5
* @access public
*/
public static function resolve_taxonomy( $taxonomy ) {
/**
* Get the allowed_taxonomies
*/
$allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies();
/**
* If the $post_type is one of the allowed_post_types
*/
if ( in_array( $taxonomy, $allowed_taxonomies, true ) ) {
return get_taxonomy( $taxonomy );
} else {
throw new UserError( sprintf( __( 'No taxonomy was found with the name %s', 'wp-graphql' ), $taxonomy ) );
}
}
/**
* Get the term object for a term
*
* @param int $id ID of the term you are trying to retrieve the object for
* @param string $taxonomy Name of the taxonomy the term is in
*
* @return mixed
* @throws UserError
* @since 0.0.5
* @access public
*/
public static function resolve_term_object( $id, $taxonomy ) {
$term_object = \WP_Term::get_instance( $id, $taxonomy );
if ( empty( $term_object ) ) {
throw new UserError( sprintf( __( 'No %1$s was found with the ID: %2$s', 'wp-graphql' ), $taxonomy, $id ) );
}
return $term_object;
}
/**
* Wrapper for TermObjectConnectionResolver::resolve
*
* @param $source
* @param array $args Array of args to be passed to the resolve method
* @param AppContext $context The AppContext object to be passed down
* @param ResolveInfo $info The ResolveInfo object
* @param \WP_Taxonomy $taxonomy The WP_Taxonomy object of the taxonomy the term is connected to
*
* @return array
* @since 0.0.5
* @access public
*/
public static function resolve_term_objects_connection( $source, array $args, $context, ResolveInfo $info, $taxonomy ) {
$resolver = new TermObjectConnectionResolver( $taxonomy );
return $resolver->resolve( $source, $args, $context, $info );
}
/**
* Retrieves the theme object for the theme you are looking for
*
* @param string $stylesheet Directory name for the theme.
*
* @return \WP_Theme object
* @throws UserError
* @since 0.0.5
* @access public
*/
public static function resolve_theme( $stylesheet ) {
$theme = wp_get_theme( $stylesheet );
if ( $theme->exists() ) {
return $theme;
} else {
throw new UserError( sprintf( __( 'No theme was found with the stylesheet: %s', 'wp-graphql' ), $stylesheet ) );
}
}
/**
* Wrapper for the ThemesConnectionResolver::resolve method
*
* @param $source
* @param array $args Passes an array of arguments to the resolve method
* @param AppContext $context The AppContext object to be passed down
* @param ResolveInfo $info The ResolveInfo object
*
* @return array
* @since 0.0.5
* @access public
*/
public static function resolve_themes_connection( $source, array $args, $context, ResolveInfo $info ) {
return ThemeConnectionResolver::resolve( $source, $args, $context, $info );
}
/**
* Gets the user object for the user ID specified
*
* @param int $id ID of the user you want the object for
*
* @return Deferred
* @since 0.0.5
* @access public
*/
public static function resolve_user( $id ) {
Loader::addOne( 'user', $id );
$loader = function() use ( $id ) {
Loader::loadBuffered( 'user' );
return Loader::loadOne( 'user', $id );
};
return new Deferred( $loader );
}
/**
* Wrapper for the UsersConnectionResolver::resolve method
*
* @param $source
* @param array $args Array of args to be passed down to the resolve method
* @param AppContext $context The AppContext object to be passed down
* @param ResolveInfo $info The ResolveInfo object
*
* @return array
* @since 0.0.5
* @access public
*/
public static function resolve_users_connection( $source, array $args, $context, ResolveInfo $info ) {
return UserConnectionResolver::resolve( $source, $args, $context, $info );
}
/**
* Get all of the allowed settings by group and return the
* settings group that matches the group param
*
* @access public
* @param string $group
*
* @return array $settings_groups[ $group ]
*/
public static function get_setting_group_fields( $group ) {
/**
* Get all of the settings, sorted by group
*/
$settings_groups = self::get_allowed_settings_by_group();
return ! empty( $settings_groups[ $group ] ) ? $settings_groups[ $group ] : [];
}
/**
* Get all of the allowed settings by group
*
* @access public
* @return array $allowed_settings_by_group
*/
public static function get_allowed_settings_by_group() {
/**
* Get all registered settings
*/
$registered_settings = get_registered_settings();
/**
* Loop through the $registered_settings array and build an array of
* settings for each group ( general, reading, discussion, writing, reading, etc. )
* if the setting is allowed in REST or GraphQL
*/
foreach ( $registered_settings as $key => $setting ) {
if ( ! isset( $setting['show_in_graphql'] ) ) {
if ( isset( $setting['show_in_rest'] ) && false !== $setting['show_in_rest'] ) {
$setting['key'] = $key;
$allowed_settings_by_group[ $setting['group'] ][ $key ] = $setting;
}
} else if ( true === $setting['show_in_graphql'] ) {
$setting['key'] = $key;
$allowed_settings_by_group[ $setting['group'] ][ $key ] = $setting;
}
};
/**
* Set the setting groups that are allowed
*/
$allowed_settings_by_group = ! empty( $allowed_settings_by_group ) && is_array( $allowed_settings_by_group ) ? $allowed_settings_by_group : [];
/**
* Filter the $allowed_settings_by_group to allow enabling or disabling groups in the GraphQL Schema.
*
* @param array $allowed_settings_by_group
*/
$allowed_settings_by_group = apply_filters( 'graphql_allowed_settings_by_group', $allowed_settings_by_group );
return $allowed_settings_by_group;
}
/**
* Get all of the $allowed_settings
*
* @access public
* @return array $allowed_settings
*/
public static function get_allowed_settings() {
/**
* Get all registered settings
*/
$registered_settings = get_registered_settings();
/**
* Loop through the $registered_settings and if the setting is allowed in REST or GraphQL
* add it to the $allowed_settings array
*/
foreach ( $registered_settings as $key => $setting ) {
if ( ! isset( $setting['show_in_graphql'] ) ) {
if ( isset( $setting['show_in_rest'] ) && false !== $setting['show_in_rest'] ) {
$setting['key'] = $key;
$allowed_settings[ $key ] = $setting;
}
} else if ( true === $setting['show_in_graphql'] ) {
$setting['key'] = $key;
$allowed_settings[ $key ] = $setting;
}
};
/**
* Verify that we have the allowed settings
*/
$allowed_settings = ! empty( $allowed_settings ) && is_array( $allowed_settings ) ? $allowed_settings : [];
/**
* Filter the $allowed_settings to allow some to be enabled or disabled from showing in
* the GraphQL Schema.
*
* @param array $allowed_settings
*
* @return array
*/
$allowed_settings = apply_filters( 'graphql_allowed_setting_groups', $allowed_settings );
return $allowed_settings;
}
/**
* We get the node interface and field from the relay library.
*
* The first method is the way we resolve an ID to its object. The second is the way we resolve
* an object that implements node to its type.
*
* @return array
* @throws UserError
* @access public
*/
public static function get_node_definition() {
if ( null === self::$node_definition ) {
$node_definition = Relay::nodeDefinitions(
// The ID fetcher definition
function( $global_id ) {
if ( empty( $global_id ) ) {
throw new UserError( __( 'An ID needs to be provided to resolve a node.', 'wp-graphql' ) );
}
/**
* Convert the encoded ID into an array we can work with
*
* @since 0.0.4
*/
$id_components = Relay::fromGlobalId( $global_id );
/**
* If the $id_components is a proper array with a type and id
*
* @since 0.0.5
*/
if ( is_array( $id_components ) && ! empty( $id_components['id'] ) && ! empty( $id_components['type'] ) ) {
/**
* Get the allowed_post_types and allowed_taxonomies
*
* @since 0.0.5
*/
$allowed_post_types = \WPGraphQL::get_allowed_post_types();
$allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies();
switch ( $id_components['type'] ) {
case in_array( $id_components['type'], $allowed_post_types, true ):
$node = self::resolve_post_object( $id_components['id'], $id_components['type'] );
break;
case in_array( $id_components['type'], $allowed_taxonomies, true ):
$node = self::resolve_term_object( $id_components['id'], $id_components['type'] );
break;
case 'comment':
$node = self::resolve_comment( $id_components['id'] );
break;
case 'commentAuthor':
$node = self::resolve_comment_author( $id_components['id'] );
break;
case 'plugin':
$node = self::resolve_plugin( $id_components['id'] );
break;
case 'postType':
$node = self::resolve_post_type( $id_components['id'] );
break;
case 'taxonomy':
$node = self::resolve_taxonomy( $id_components['id'] );
break;
case 'theme':
$node = self::resolve_theme( $id_components['id'] );
break;
case 'user':
$node = self::resolve_user( $id_components['id'] );
break;
default:
/**
* Add a filter to allow externally registered node types to resolve based on
* the id_components
*
* @param int $id The id of the node, from the global ID
* @param string $type The type of node to resolve, from the global ID
*
* @since 0.0.6
*/
$node = apply_filters( 'graphql_resolve_node', null, $id_components['id'], $id_components['type'] );
break;
}
/**
* If the $node is not properly resolved, throw an exception
*
* @since 0.0.6
*/
if ( null === $node ) {
throw new UserError( sprintf( __( 'No node could be found with global ID: %s', 'wp-graphql' ), $global_id ) );
}
/**
* Return the resolved $node
*
* @since 0.0.5
*/
return $node;
} else {
throw new UserError( sprintf( __( 'The global ID isn\'t recognized ID: %s', 'wp-graphql' ), $global_id ) );
}
},
// Type resolver
function( $node ) {
if ( true === is_object( $node ) ) {
switch ( true ) {
case $node instanceof \WP_Post:
$type = Types::post_object( $node->post_type );
break;
case $node instanceof \WP_Term:
$type = Types::term_object( $node->taxonomy );
break;
case $node instanceof \WP_Comment:
$type = Types::comment();
break;
case $node instanceof \WP_Post_Type:
$type = Types::post_type();
break;
case $node instanceof \WP_Taxonomy:
$type = Types::taxonomy();
break;
case $node instanceof \WP_Theme:
$type = Types::theme();
break;
case $node instanceof \WP_User:
$type = Types::user();
break;
default:
$type = null;
}
// Some nodes might return an array instead of an object
} elseif ( is_array( $node ) ) {
switch ( $node ) {
case array_key_exists( 'PluginURI', $node ):
$type = Types::plugin();
break;
case array_key_exists( 'is_comment_author', $node ):
$type = Types::comment_author();
break;
default:
$type = null;
}
}
/**
* Add a filter to allow externally registered node types to return the proper type
* based on the node_object that's returned
*
* @param mixed|object|array $type The type definition the node should resolve to.
* @param mixed|object|array $node The $node that is being resolved
*
* @since 0.0.6
*/
$type = apply_filters( 'graphql_resolve_node_type', $type, $node );
/**
* If the $type is not properly resolved, throw an exception
*
* @since 0.0.6
*/
if ( null === $type ) {
throw new UserError( __( 'No type was found matching the node', 'wp-graphql' ) );
}
/**
* Return the resolved $type for the $node
*
* @since 0.0.5
*/
return $type;
}
);
self::$node_definition = $node_definition;
}
return self::$node_definition;
}
/**
* Cached version of get_page_by_path so that we're not making unnecessary SQL all the time
*
* This is a modified version of the cached function from WordPress.com VIP MU Plugins here.
*
* @param string $uri
* @param string $output Optional. Output type; OBJECT*, ARRAY_N, or ARRAY_A.
* @param string $post_type Optional. Post type; default is 'post'.
* @return WP_Post|null WP_Post on success or null on failure
* @see https://github.com/Automattic/vip-go-mu-plugins/blob/52549ae9a392fc1343b7ac9dba4ebcdca46e7d55/vip-helpers/vip-caching.php#L186
* @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
*/
public static function get_post_object_by_uri( $uri, $output = OBJECT, $post_type = 'post' ) {
if ( is_array( $post_type ) ) {
$cache_key = sanitize_key( $uri ) . '_' . md5( serialize( $post_type ) );
} else {
$cache_key = $post_type . '_' . sanitize_key( $uri );
}
$post_id = wp_cache_get( $cache_key, 'get_post_object_by_path' );
if ( false === $post_id ) {
$post = get_page_by_path( $uri, $output, $post_type );
$post_id = $post ? $post->ID : 0;
if ( 0 === $post_id ) {
wp_cache_set( $cache_key, $post_id, 'get_post_object_by_path', ( 1 * HOUR_IN_SECONDS + mt_rand( 0, HOUR_IN_SECONDS ) ) ); // We only store the ID to keep our footprint small
} else {
wp_cache_set( $cache_key, $post_id, 'get_post_object_by_path', 0 ); // We only store the ID to keep our footprint small
}
}
if ( $post_id ) {
return get_post( $post_id, $output );
}
return null;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace WPGraphQL\Data;
use GraphQL\Error\UserError;
/**
* Class Loader
*
* This class sets up general patterns for loading data in an optimized way. Used in conjunction with GraphQL Deferred
* resolvers
*
* @package WPGraphQL\Data
*/
class Loader {
/**
* Holds the queue of items to be loaded
*
* @var array $buffer
* @access protected
*/
protected static $buffer = [];
/**
* Holds the collection of items that have already been loaded
*
* @var array $loaded
* @access protected
*/
protected static $loaded = [];
/**
* Add an item to the buffer
*
* @param string $type The type of object to add
* @param integer $id The ID of the item to be loaded
*
* @access public
*/
public static function addOne( $type, $id ) {
if ( empty( self::$buffer[ $type ] ) ) {
self::$buffer[ $type ] = [];
}
if ( ! in_array( $id, self::$buffer[ $type ], true ) ) {
array_push( self::$buffer[ $type ], absint( $id ) );
}
}
/**
* Add many items to the buffer
*
* @param string $type The type of objects to add
* @param array $ids Array of IDs to be added to the buffer
*
* @access public
*/
public static function addMany( $type, array $ids ) {
if ( ! empty( $ids ) && is_array( $ids ) ) {
foreach ( $ids as $id ) {
self::addOne( $type, $id );
}
}
}
/**
* Load an individual item from the loaded items
*
* @param string $type The type of object to load
* @param int $id The ID of the item to load
*
* @return mixed
* @access public
*/
public static function loadOne( $type, $id ) {
$loaded = ! empty( self::$loaded[ $type ][ $id ] ) ? self::$loaded[ $type ][ $id ] : null;
if ( ! empty( $loaded ) ) {
return $loaded;
} else {
throw new UserError( sprintf( __( 'No %1$s was found with the provided ID', 'wp-graphql' ), $type, $id ) );
}
}
/**
* Load many items from the already loaded items
*
* @param string $type The type of objects to load
* @param array $ids Array of items to load
*
* @access public
*/
public static function loadMany( $type, array $ids ) {
$load = [];
if ( ! empty( $ids ) && is_array( $ids ) ) {
foreach ( $ids as $id ) {
$load[ $type ][] = self::$loaded[ $id ];
}
}
}
/**
* Should be implemented by extending loader
*
* @param string $type The type of objects to load
*
* @return array
* @access public
*/
public static function loadBuffered( $type ) {
switch ( $type ) {
case 'user':
return self::load_users();
break;
default:
return [];
}
}
/**
* Loads users from the buffer
*
* @return mixed
* @access protected
*/
protected static function load_users() {
$type = 'user';
if ( ! empty( self::$buffer[ $type ] ) ) {
$query = new \WP_User_Query( [
'include' => self::$buffer[ $type ],
'orderby' => 'include',
'count_total' => false,
'fields' => 'all_with_meta'
] );
if ( ! empty( $query->get_results() ) && is_array( $query->get_results() ) ) {
foreach ( $query->get_results() as $user ) {
if ( $user instanceof \WP_User ) {
self::$loaded[ $type ][ $user->ID ] = $user;
}
}
}
}
self::reset_buffer( $type );
return ! empty( self::$loaded[ $type ] ) ? self::$loaded[ $type ] : [];
}
/**
* Resets the buffer for a given type
*
* @param string $type The buffer type to reset
*
* @access protected
*/
protected static function reset_buffer( $type ) {
self::$buffer[ $type ] = [];
}
}

View File

@@ -0,0 +1,4 @@
#Data
Methods for reading and writing data should live here. The "DataSource" class serves as a factory for methods
that handle the fetching/writing of data.

View File

@@ -0,0 +1,590 @@
<?php
namespace WPGraphQL;
use GraphQL\Error\FormattedError;
use GraphQL\Error\UserError;
/**
* Class Router
* This sets up the /graphql endpoint
*
* @package WPGraphQL
* @since 0.0.1
*/
class Router {
/**
* Sets the route to use as the endpoint
*
* @var string $route
* @access public
*/
public static $route = 'graphql';
/**
* Set the default status code to 200.
*
* @var int
*/
public static $http_status_code = 200;
/**
* Router constructor.
*
* @since 0.0.1
* @access public
*/
public function __construct() {
/**
* Pass the route through a filter in case the endpoint /graphql should need to be changed
*
* @since 0.0.1
* @return string
*/
self::$route = apply_filters( 'graphql_endpoint', 'graphql' );
/**
* Create the rewrite rule for the route
*
* @since 0.0.1
*/
add_action( 'init', [ $this, 'add_rewrite_rule' ], 10 );
/**
* Add the query var for the route
*
* @since 0.0.1
*/
add_filter( 'query_vars', [ $this, 'add_query_var' ], 1, 1 );
/**
* Redirects the route to the graphql processor
*
* @since 0.0.1
*/
add_action( 'parse_request', [ $this, 'resolve_http_request' ], 10 );
}
/**
* Adds rewrite rule for the route endpoint
*
* @uses add_rewrite_rule()
* @since 0.0.1
* @access public
* @return void
*/
public static function add_rewrite_rule() {
add_rewrite_rule(
self::$route . '/?$',
'index.php?' . self::$route . '=true',
'top'
);
}
/**
* Adds the query_var for the route
*
* @param array $query_vars The array of whitelisted query variables
*
* @access public
* @since 0.0.1
* @return array
*/
public static function add_query_var( $query_vars ) {
$query_vars[] = self::$route;
return $query_vars;
}
/**
* This resolves the http request and ensures that WordPress can respond with the appropriate
* JSON response instead of responding with a template from the standard WordPress Template
* Loading process
*
* @since 0.0.1
* @access public
* @return void
*/
public static function resolve_http_request() {
/**
* Access the $wp_query object
*/
global $wp_query;
/**
* Ensure we're on the registered route for graphql route
*/
if ( empty( $GLOBALS['wp']->query_vars ) || ! is_array( $GLOBALS['wp']->query_vars ) || ! array_key_exists( self::$route, $GLOBALS['wp']->query_vars ) ) {
return;
}
/**
* Set is_home to false
*/
$wp_query->is_home = false;
/**
* Whether it's a GraphQL HTTP Request
*
* @since 0.0.5
*/
if ( ! defined( 'GRAPHQL_HTTP_REQUEST' ) ) {
define( 'GRAPHQL_HTTP_REQUEST', true );
}
/**
* Process the GraphQL query Request
*/
self::process_http_request();
return;
}
/**
* Sends an HTTP header.
*
* @since 0.0.5
* @access public
*
* @param string $key Header key.
* @param string $value Header value.
*/
public static function send_header( $key, $value ) {
/**
* Sanitize as per RFC2616 (Section 4.2):
*
* Any LWS that occurs between field-content MAY be replaced with a
* single SP before interpreting the field value or forwarding the
* message downstream.
*/
$value = preg_replace( '/\s+/', ' ', $value );
header( apply_filters( 'graphql_send_header', sprintf( '%s: %s', $key, $value ), $key, $value ) );
}
/**
* Sends an HTTP status code.
*
* @since 0.0.5
* @access protected
*
* @param int $code HTTP status.
*/
protected static function set_status( $code ) {
status_header( $code );
}
/**
* Returns an array of headers to send with the HTTP response
*
* @return array
*/
protected static function get_response_headers() {
/**
* Filtered list of access control headers.
*
* @param array $access_control_headers Array of headers to allow.
*/
$access_control_allow_headers = apply_filters( 'graphql_access_control_allow_headers', [
'Authorization',
'Content-Type'
] );
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => implode( ', ', $access_control_allow_headers ),
'Content-Type' => 'application/json ; charset=' . get_option( 'blog_charset' ),
'X-Robots-Tag' => 'noindex',
'X-Content-Type-Options' => 'nosniff',
'X-hacker' => __( 'If you\'re reading this, you should visit github.com/wp-graphql and contribute!', 'wp-graphql' ),
];
/**
* Send nocache headers on authenticated requests.
*
* @since 0.0.5
*
* @param bool $rest_send_nocache_headers Whether to send no-cache headers.
*/
$send_no_cache_headers = apply_filters( 'graphql_send_nocache_headers', is_user_logged_in() );
if ( $send_no_cache_headers ) {
foreach ( wp_get_nocache_headers() as $no_cache_header_key => $no_cache_header_value ) {
$headers[ $no_cache_header_key ] = $no_cache_header_value;
}
}
/**
* Filter the $headers to send
*/
return apply_filters( 'graphql_response_headers_to_send', $headers );
}
/**
* Set the response headers
*
* @param int $http_status The status code to send as a header
*
* @since 0.0.1
* @access public
* @return void
*/
public static function set_headers( $http_status ) {
if ( false === headers_sent() ) {
/**
* Set the HTTP response status
*/
self::set_status( $http_status );
/**
* Get the response headers
*/
$headers = self::get_response_headers();
/**
* If there are headers, set them for the response
*/
if ( ! empty( $headers ) && is_array( $headers ) ) {
foreach ( $headers as $key => $value ) {
self::send_header( $key, $value );
}
}
/**
* Fire an action when the headers are set
*
* @param array $headers The headers sent in the response
*/
do_action( 'graphql_response_set_headers', $headers );
}
}
/**
* Retrieves the raw request entity (body).
*
* @since 0.0.5
* @access public
* @global string $HTTP_RAW_POST_DATA Raw post data.
* @return string Raw request data.
*/
public static function get_raw_data() {
global $HTTP_RAW_POST_DATA;
/*
* A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
* but we can do it ourself.
*/
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}
return $HTTP_RAW_POST_DATA;
}
/**
* This processes the graphql requests that come into the /graphql endpoint via an HTTP request
*
* @since 0.0.1
* @access public
* @return mixed
*/
public static function process_http_request() {
/**
* This action can be hooked to to enable various debug tools,
* such as enableValidation from the GraphQL Config.
*
* @since 0.0.4
*/
do_action( 'graphql_process_http_request' );
/**
* Start the $response array to return for the response content
*
* @since 0.0.5
*/
$response = [];
$graphql_results = [];
$request = '';
$operation_name = '';
$variables = [];
$user = null;
try {
/**
* Store the global post so it can be reset after GraphQL execution
*
* This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
* without disrupting the flow of the post as the global POST before and after GraphQL execution will be
* the same.
*/
$global_post = ! empty( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;
/**
* Respond to pre-flight requests.
*
* @see: https://apollographql.slack.com/archives/C10HTKHPC/p1507649812000123
* @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
*/
if ( 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
self::$http_status_code = 200;
self::set_headers( self::$http_status_code );
exit;
} else if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'GET' ) {
$data = [
'query' => isset( $_GET['query'] ) ? wp_kses_stripslashes( sanitize_text_field( $_GET['query'] ) ) : '',
'operationName' => isset( $_GET['operationName'] ) ? wp_kses_stripslashes( sanitize_text_field( $_GET['operationName'] ) ) : '',
'variables' => isset( $_GET['variables'] ) ? $_GET['variables'] : '',
];
/**
* Allow the data to be filtered
*
* @param array $data An array containing the pieces of the data of the GraphQL request
*/
$data = apply_filters( 'graphql_request_data', $data );
/**
* If the variables are already formatted as an array use them.
*
* Example:
* ?query=query getPosts($first:Int){posts(first:$first){edges{node{id}}}}&variables[first]=1
*/
if ( is_array( $data['variables'] ) ) {
$sanitized_variables = [];
foreach ( $data['variables'] as $key => $value ) {
$sanitized_variables[ $key ] = sanitize_text_field( $value );
}
$decoded_variables = $sanitized_variables;
/**
* If the variables are not an array, let's attempt to decode them and convert them to an array for
* use in the executor.
*/
} else {
$decoded_variables = json_decode( wp_kses_stripslashes( $data['variables'] ), true );
}
$data['variables'] = ! empty( $decoded_variables ) && is_array( $decoded_variables ) ? $decoded_variables : null;
/**
* Allow the data to be filtered
*
* @param array $data An array containing the pieces of the data of the GraphQL request
*/
$data = apply_filters( 'graphql_request_data', $data );
/**
* Get the pieces of the request from the data
*/
$request = isset( $data['query'] ) ? $data['query'] : '';
$operation_name = isset( $data['operationName'] ) ? $data['operationName'] : '';
$variables = isset( $data['variables'] ) ? $data['variables'] : [];
if ( false === headers_sent() ) {
self::prepare_headers( $response, $graphql_results, $request, $operation_name, $variables, $user );
}
/**
* Process the GraphQL request
*
* @since 0.0.5
*/
$graphql_results = do_graphql_request( $request, $operation_name, $variables );
/**
* Ensure the $graphql_request is returned as a proper, populated array,
* otherwise add an error to the result
*/
if ( ! empty( $graphql_results ) && is_array( $graphql_results ) ) {
$response = $graphql_results;
} else {
$response['errors'] = __( 'The GraphQL request returned an invalid response', 'wp-graphql' );
}
self::after_execute( $response, $operation_name, $request, $variables, $graphql_results );
} else {
/**
* If headers haven't been sent already, let's set the headers and return the JSON response
*/
if ( false === headers_sent() ) {
self::prepare_headers( $response, $graphql_results, $request, $operation_name, $variables, $user );
/**
* Send the JSON response
*/
$server = \WPGraphQL::server();
$response = $server->executeRequest();
self::after_execute( $response, $operation_name, $request, $variables, $graphql_results );
}
}
} catch ( \Exception $error ) {
/**
* If there are errors, set the status to 500
* and format the captured errors to be output properly
*
* @since 0.0.4
*/
self::$http_status_code = 500;
$response['errors'] = [ FormattedError::createFromException( $error ) ];
} // End try().
/**
* Send the response
*/
wp_send_json( $response );
}
/**
* Prepare headers for response
*
* @param array $response The response of the GraphQL Request
* @param array $graphql_results The results of the GraphQL execution
* @param string $request The GraphQL Request
* @param string $operation_name The operation name of the GraphQL Request
* @param array $variables The variables applied to the GraphQL Request
* @param \WP_User $user The current user object
*/
protected static function prepare_headers( $response, $graphql_results, $request, $operation_name, $variables, $user ) {
/**
* Filter the $status_code before setting the headers
*
* @param int $status_code The status code to apply to the headers
* @param array $response The response of the GraphQL Request
* @param array $graphql_results The results of the GraphQL execution
* @param string $request The GraphQL Request
* @param string $operation_name The operation name of the GraphQL Request
* @param array $variables The variables applied to the GraphQL Request
* @param \WP_User $user The current user object
*/
$status_code = apply_filters( 'graphql_response_status_code', self::$http_status_code, $response, $graphql_results, $request, $operation_name, $variables, $user );
/**
* Set the response headers
*/
self::set_headers( $status_code );
}
/**
* Apply filters and do actions after GraphQL Execution
*
* @param array $result The result of your GraphQL request
* @param string $operation_name The name of the operation
* @param string $request The request that GraphQL executed
* @param array|null $variables Variables to passed to your GraphQL query
*/
protected static function after_execute( $result, $operation_name, $request, $variables ) {
/**
* Run an action. This is a good place for debug tools to hook in to log things, etc.
*
* @since 0.0.4
*
* @param array $result The result of your GraphQL request
* @param Schema object $schema The schema object for the root request
* @param string $operation_name The name of the operation
* @param string $request The request that GraphQL executed
* @param array|null $variables Variables to passed to your GraphQL query
*/
do_action( 'graphql_execute', $result, \WPGraphQL::get_schema(), $operation_name, $request, $variables );
/**
* Filter the $result of the GraphQL execution. This allows for the response to be filtered before
* it's returned, allowing granular control over the response at the latest point.
*
* POSSIBLE USAGE EXAMPLES:
* This could be used to ensure that certain fields never make it to the response if they match
* certain criteria, etc. For example, this filter could be used to check if a current user is
* allowed to see certain things, and if they are not, the $result could be filtered to remove
* the data they should not be allowed to see.
*
* Or, perhaps some systems want the result to always include some additional piece of data in
* every response, regardless of the request that was sent to it, this could allow for that
* to be hooked in and included in the $result
*
* @since 0.0.5
*
* @param array $result The result of your GraphQL query
* @param Schema object $schema The schema object for the root query
* @param string $operation_name The name of the operation
* @param string $request The request that GraphQL executed
* @param array|null $variables Variables to passed to your GraphQL request
*/
$filtered_result = apply_filters( 'graphql_request_results', $result, \WPGraphQL::get_schema(), $operation_name, $request, $variables );
/**
* Run an action after the result has been filtered, as the response is being returned.
* This is a good place for debug tools to hook in to log things, etc.
*
* @param array $filtered_result The filtered_result of the GraphQL request
* @param array $result The result of your GraphQL request
* @param WPSchema $schema The schema object for the root request
* @param string $operation_name The name of the operation
* @param string $request The request that GraphQL executed
* @param array|null $variables Variables to passed to your GraphQL query
*/
do_action( 'graphql_return_response', $filtered_result, $result, \WPGraphQL::get_schema(), $operation_name, $request, $variables );
/**
* Reset the global post after execution
*
* This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
* without disrupting the flow of the post as the global POST before and after GraphQL execution will be
* the same.
*/
if ( ! empty( $global_post ) ) {
$GLOBALS['post'] = $global_post;
}
/**
* Run an action after the HTTP Response is ready to be sent back. This might be a good place for tools
* to hook in to track metrics, such as how long the process took from `graphql_process_http_request`
* to here, etc.
*
* @param array $result The result of the GraphQL Query
* @param array $filtered_result
* @param string $operation_name The name of the operation
* @param string $request The request that GraphQL executed
* @param array $variables Variables to passed to your GraphQL query
*
* @since 0.0.5
*/
do_action( 'graphql_process_http_request_response', $filtered_result, $result, $operation_name, $request, $variables );
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace WPGraphQL\Type\Avatar;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class AvatarType
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class AvatarType extends WPObjectType {
/**
* Holds the type name
*
* @var string $type_name
*/
private static $type_name;
/**
* This holds the field definitions
*
* @var array $fields
* @since 0.0.5
*/
private static $fields;
/**
* WPObjectType constructor.
*
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
*
* @since 0.0.5
*/
self::$type_name = 'Avatar';
$config = [
'name' => self::$type_name,
'fields' => self::fields(),
'description' => __( 'Avatars are profile images for users. WordPress by default uses the Gravatar service to host and fetch avatars from.', 'wp-graphql' ),
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields for the AvatarType. The fields are passed through a filter so the shape of the schema
* can be modified
*
* @return array|\GraphQL\Type\Definition\FieldDefinition[]
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'size' => [
'type' => Types::int(),
'description' => __( 'The size of the avatar in pixels. A value of 96 will match a 96px x 96px gravatar image.', 'wp-graphql' ),
],
'height' => [
'type' => Types::int(),
'description' => __( 'Height of the avatar image.', 'wp-graphql' ),
],
'width' => [
'type' => Types::int(),
'description' => __( 'Width of the avatar image.', 'wp-graphql' ),
],
'default' => [
'type' => Types::string(),
'description' => __( "URL for the default image or a default type. Accepts '404' (return a 404 instead of a default image), 'retro' (8bit), 'monsterid' (monster), 'wavatar' (cartoon face), 'indenticon' (the 'quilt'), 'mystery', 'mm', or 'mysteryman' (The Oyster Man), 'blank' (transparent GIF), or 'gravatar_default' (the Gravatar logo).", 'wp-graphql' ),
],
'forceDefault' => [
'type' => Types::boolean(),
'description' => __( 'Whether to always show the default image, never the Gravatar.', 'wp-graphql' ),
'resolve' => function( $avatar, array $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $avatar['force_default'] ) && true === $avatar['force_default'] ) ? true : false;
},
],
'rating' => [
'type' => Types::string(),
'description' => __( "What rating to display avatars up to. Accepts 'G', 'PG', 'R', 'X', and are judged in that order.", 'wp-graphql' ),
],
'scheme' => [
'type' => Types::string(),
'description' => __( 'Type of url scheme to use. Typically HTTP vs. HTTPS.', 'wp-graphql' ),
],
'extraAttr' => [
'type' => Types::string(),
'description' => __( 'HTML attributes to insert in the IMG element. Is not sanitized.', 'wp-graphql' ),
'resolve' => function( $avatar, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $avatar['extra_attr'] ) ? $avatar['extra_attr'] : null;
},
],
'foundAvatar' => [
'type' => Types::boolean(),
'description' => __( 'Whether the avatar was successfully found.', 'wp-graphql' ),
'resolve' => function( $avatar, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $avatar['found_avatar'] && true === $avatar['found_avatar'] ) ? true : false;
},
],
'url' => [
'type' => Types::string(),
'description' => __( 'URL for the gravatar image source.', 'wp-graphql' ),
],
];
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace WPGraphQL\Type\Comment;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
class CommentQuery {
/**
* root_query
* @return array
*/
public static function root_query() {
return [
'type' => Types::comment(),
'description' => __( 'Returns a Comment', 'wp-graphql' ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_comment( $id_components['id'] );
},
];
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace WPGraphQL\Type\Comment;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\Comment\Connection\CommentConnectionDefinition;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
class CommentType extends WPObjectType {
/**
* Holds the type name
*
* @var string $type_name
*/
private static $type_name;
/**
* Holds the $fields definition for the CommentType
*
* @var $fields
*/
private static $fields;
/**
* CommentType constructor.
*
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
*
* @since 0.0.5
*/
self::$type_name = 'Comment';
$config = [
'name' => self::$type_name,
'description' => __( 'A Comment object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* This defines the fields that make up the CommentType
*
* @return mixed
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The globally unique identifier for the user', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_ID ) ? Relay::toGlobalId( 'comment', $comment->comment_ID ) : null;
},
],
'commentId' => [
'type' => Types::int(),
'description' => __( 'ID for the comment, unique among comments.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_ID ) ? $comment->comment_ID : 0;
},
],
'commentedOn' => [
'type' => Types::post_object_union(),
'description' => __( 'The object the comment was added to', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_post_ID ) ? get_post( $comment->comment_post_ID ) : null;
},
],
'author' => [
'type' => Types::comment_author_union(),
'description' => __( 'The author of the comment', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
/**
* If the comment has a user associated, use it to populate the author, otherwise return
* the $comment and the Union will use that to hydrate the CommentAuthor Type
*/
if ( ! empty( $comment->user_id ) ) {
return DataSource::resolve_user( absint( $comment->user_id ) );
} else {
return DataSource::resolve_comment_author( $comment->comment_author_email );
}
},
],
'authorIp' => [
'type' => Types::string(),
'description' => __( 'IP address for the author. This field is equivalent to WP_Comment->comment_author_IP and the value matching the `comment_author_IP` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_author_IP ) ? $comment->comment_author_IP : '';
},
],
'date' => [
'type' => Types::string(),
'description' => __( 'Date the comment was posted in local time. This field is equivalent to WP_Comment->date and the value matching the `date` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_date ) ? $comment->comment_date : '';
},
],
'dateGmt' => [
'type' => Types::string(),
'description' => __( 'Date the comment was posted in GMT. This field is equivalent to WP_Comment->date_gmt and the value matching the `date_gmt` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_date_gmt ) ? $comment->comment_date_gmt : '';
},
],
'content' => [
'type' => Types::string(),
'description' => __( 'Content of the comment. This field is equivalent to WP_Comment->comment_content and the value matching the `comment_content` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_content ) ? $comment->comment_content : '';
},
],
'karma' => [
'type' => Types::int(),
'description' => __( 'Karma value for the comment. This field is equivalent to WP_Comment->comment_karma and the value matching the `comment_karma` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_karma ) ? $comment->comment_karma : 0;
},
],
'approved' => [
'type' => Types::string(),
'description' => __( 'The approval status of the comment. This field is equivalent to WP_Comment->comment_approved and the value matching the `comment_approved` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_approved ) ? $comment->comment_approved : '';
},
],
'agent' => [
'type' => Types::string(),
'description' => __( 'User agent used to post the comment. This field is equivalent to WP_Comment->comment_agent and the value matching the `comment_agent` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_agent ) ? $comment->comment_agent : '';
},
],
'type' => [
'type' => Types::string(),
'description' => __( 'Type of comment. This field is equivalent to WP_Comment->comment_type and the value matching the `comment_type` column in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment->comment_type ) ? $comment->comment_type : '';
},
],
'parent' => [
'type' => Types::comment(),
'description' => __( 'Parent comment of current comment. This field is equivalent to the WP_Comment instance matching the WP_Comment->comment_parent ID.', 'wp-graphql' ),
'resolve' => function( \WP_Comment $comment, $args, AppContext $context, ResolveInfo $info ) {
return get_comment( $comment->comment_parent );
},
],
];
/**
* Add a comments_connection to display the child comments
*
* @since 0.0.5
*/
$fields['children'] = CommentConnectionDefinition::connection( 'Children' );
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace WPGraphQL\Type\Comment\Connection;
use GraphQL\Type\Definition\EnumType;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class CommentConnectionArgs
*
* This sets up the Query Args for comments connections, which uses WP_Comment_Query, so this defines the allowed
* input fields that will be passed to the WP_Comment_Query
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class CommentConnectionArgs extends WPInputObjectType {
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
public static $fields = [];
/**
* Holds the orderby Enum definition
* @var EnumType
* @since 0.0.5
*/
private static $comments_orderby_enum;
/**
* Holds the order Enum definition
* @var EnumType
*/
private static $comments_order;
/**
* CommentConnectionArgs constructor.
* @param array $config
* @param string $connection
* @since 0.0.5
*/
public function __construct( $config = [], $connection ) {
$config['name'] = ucfirst( $connection ) . 'CommentArgs';
$config['fields'] = self::fields( $connection );
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the CommentConnectionArgs
*
* @return array
* @since 0.0.5
*/
private static function fields( $connection ) {
if ( empty( self::$fields[ $connection ] ) ) {
$fields = [
'authorEmail' => [
'type' => Types::string(),
'description' => __( 'Comment author email address.', 'wp-graphql' ),
],
'authorUrl' => [
'type' => Types::string(),
'description' => __( 'Comment author URL.', 'wp-graphql' ),
],
'authorIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of author IDs to include comments for.', 'wp-graphql' ),
],
'authorNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of author IDs to exclude comments for.', 'wp-graphql' ),
],
'commentIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of comment IDs to include.', 'wp-graphql' ),
],
'commentNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of IDs of users whose unapproved comments will be returned by the
query regardless of status.', 'wp-graphql' ),
],
'includeUnapproved' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of author IDs to include comments for.', 'wp-graphql' ),
],
'karma' => [
'type' => Types::int(),
'description' => __( 'Karma score to retrieve matching comments for.', 'wp-graphql' ),
],
'orderby' => [
'type' => self::comments_orderby_enum(),
'description' => __( 'Field to order the comments by.', 'wp-graphql' ),
],
'order' => [
'type' => self::comment_order(),
],
'parent' => [
'type' => Types::int(),
'description' => __( 'Parent ID of comment to retrieve children of.', 'wp-graphql' ),
],
'parentIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of parent IDs of comments to retrieve children for.', 'wp-graphql' ),
],
'parentNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of parent IDs of comments *not* to retrieve children
for.', 'wp-graphql' ),
],
'contentAuthorIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of author IDs to retrieve comments for.', 'wp-graphql' ),
],
'contentAuthorNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of author IDs *not* to retrieve comments for.', 'wp-graphql' ),
],
'contentId' => [
'type' => Types::int(),
'description' => __( 'Limit results to those affiliated with a given content object
ID.', 'wp-graphql' ),
],
'contentIdIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of content object IDs to include affiliated comments
for.', 'wp-graphql' ),
],
'contentIdNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of content object IDs to exclude affiliated comments
for.', 'wp-graphql' ),
],
'contentAuthor' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Content object author ID to limit results by.', 'wp-graphql' ),
],
'contentStatus' => [
'type' => Types::list_of( Types::post_status_enum() ),
'description' => __( 'Array of content object statuses to retrieve affiliated comments for.
Pass \'any\' to match any value.', 'wp-graphql' ),
],
'contentType' => [
'type' => Types::list_of( Types::post_type_enum() ),
'description' => __( 'Content object type or array of types to retrieve affiliated comments for. Pass \'any\' to match any value.', 'wp-graphql' ),
],
'contentName' => [
'type' => Types::string(),
'description' => __( 'Content object name to retrieve affiliated comments for.', 'wp-graphql' ),
],
'contentParent' => [
'type' => Types::int(),
'description' => __( 'Content Object parent ID to retrieve affiliated comments for.', 'wp-graphql' ),
],
'search' => [
'type' => Types::string(),
'description' => __( 'Search term(s) to retrieve matching comments for.', 'wp-graphql' ),
],
'status' => [
'type' => Types::string(),
'description' => __( 'Comment status to limit results by.', 'wp-graphql' ),
],
'commentType' => [
'type' => Types::string(),
'description' => __( 'Include comments of a given type.', 'wp-graphql' ),
],
'commentTypeIn' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Include comments from a given array of comment types.', 'wp-graphql' ),
],
'commentTypeNotIn' => [
'type' => Types::string(),
'description' => __( 'Exclude comments from a given array of comment types.', 'wp-graphql' ),
],
'userId' => [
'type' => Types::int(),
'description' => __( 'Include comments for a specific user ID.', 'wp-graphql' ),
],
];
self::$fields[ $connection ] = self::prepare_fields( $fields, ucfirst( $connection ) . 'CommentArgs' );
}
return ! empty( self::$fields[ $connection ] ) ? self::$fields[ $connection ] : null;
}
/**
* comments_orderby_enum
*
* Defines the orderby Enum values for ordering a comments query
*
* @return EnumType
* @since 0.0.5
*/
private static function comments_orderby_enum() {
if ( null === self::$comments_orderby_enum ) {
self::$comments_orderby_enum = new WPEnumType( [
'name' => 'CommentsOrderby',
'values' => [
'COMMENT_AGENT' => [
'value' => 'comment_agent',
],
'COMMENT_APPROVED' => [
'value' => 'comment_approved',
],
'COMMENT_AUTHOR' => [
'value' => 'comment_author',
],
'COMMENT_AUTHOR_EMAIL' => [
'value' => 'comment_author_email',
],
'COMMENT_AUTHOR_IP' => [
'value' => 'comment_author_IP',
],
'COMMENT_AUTHOR_URL' => [
'value' => 'comment_author_url',
],
'COMMENT_CONTENT' => [
'value' => 'comment_content',
],
'COMMENT_DATE' => [
'value' => 'comment_date',
],
'COMMENT_DATE_GMT' => [
'value' => 'comment_date_gmt',
],
'COMMENT_ID' => [
'value' => 'comment_ID',
],
'COMMENT_KARMA' => [
'value' => 'comment_karma',
],
'COMMENT_PARENT' => [
'value' => 'comment_parent',
],
'COMMENT_POST_ID' => [
'value' => 'comment_post_ID',
],
'COMMENT_TYPE' => [
'value' => 'comment_type',
],
'USER_ID' => [
'value' => 'user_id',
],
'COMMENT_IN' => [
'value' => 'comment__in',
],
],
] );
}
return self::$comments_orderby_enum;
}
private static function comment_order() {
if ( null === self::$comments_order ) {
self::$comments_order = new WPEnumType( [
'name' => 'CommentsOrder',
'values' => [
'ASC' => [
'value' => 'ASC',
],
'DESC' => [
'value' => 'DESC',
],
],
'defaultValue' => 'DESC',
] );
}
return self::$comments_order;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace WPGraphQL\Type\Comment\Connection;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class CommentConnectionDefinition
*
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class CommentConnectionDefinition {
/**
* @var array connection
* @since 0.0.5
*/
private static $connection = [];
/**
* Holds the input $args for the Connection
*
* @var $args InputObjectType
*/
private static $args = [];
/**
* connection
* This sets up a connection of comments
*
* @param string $from_type
*
* @return mixed
* @since 0.0.5
*/
public static function connection( $from_type = 'Root' ) {
if ( empty( self::$connection[ $from_type ] ) ) {
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::comment(),
'name' => ucfirst( $from_type ) . 'Comments',
'connectionFields' => function() {
return [
'nodes' => [
'type' => Types::list_of( Types::comment() ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the "where" args to the commentConnection
*
* @since 0.0.5
*/
$args[ $from_type ] = [
'where' => [
'name' => 'where',
'type' => self::args( ucfirst( $from_type ) . 'Comments' ),
],
];
self::$connection[ $from_type ] = [
'type' => $connection['connectionType'],
'description' => __( 'A collection of comment objects', 'wp-graphql' ),
'args' => array_merge( Relay::connectionArgs(), $args[ $from_type ] ),
'resolve' => function( $source, $args, AppContext $context, ResolveInfo $info ) {
return DataSource::resolve_comments_connection( $source, $args, $context, $info );
},
];
}
return ! empty( self::$connection[ $from_type ] ) ? self::$connection[ $from_type ] : null;
}
/**
* Return the $args to use for the connection
*
* @param string $connection
*
* @return mixed
* @since 0.0.5
*/
private static function args( $connection ) {
if ( empty( self::$args[ $connection ] ) ) {
self::$args[ $connection ] = new CommentConnectionArgs( [], $connection );
}
return self::$args[ $connection ];
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace WPGraphQL\Type\Comment\Connection;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Connection\ArrayConnection;
use WPGraphQL\AppContext;
use WPGraphQL\Data\ConnectionResolver;
use WPGraphQL\Types;
/**
* Class CommentConnectionResolver - Connects the comments to other objects
*
* @package WPGraphQL\Data\Resolvers
*/
class CommentConnectionResolver extends ConnectionResolver {
/**
* This prepares the $query_args for use in the connection query. This is where default $args are set, where dynamic
* $args from the $source get set, and where mapping the input $args to the actual $query_args occurs.
*
* @param $source
* @param array $args
* @param AppContext $context
* @param ResolveInfo $info
*
* @return mixed
*/
public static function get_query_args( $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Prepare for later use
*/
$last = ! empty( $args['last'] ) ? $args['last'] : null;
$first = ! empty( $args['first'] ) ? $args['first'] : null;
/**
* Don't calculate the total rows, it's not needed and can be expensive
*/
$query_args['no_found_rows'] = true;
/**
* Set the default comment_status for Comment Queries to be "approved"
*/
$query_args['comment_status'] = 'approved';
/**
* Set the default comment_parent for Comment Queries to be "0" to only get top level comments
*/
$query_args['parent'] = 0;
/**
* Set the number, ensuring it doesn't exceed the amount set as the $max_query_amount
*
* @since 0.0.6
*/
$query_args['number'] = min( max( absint( $first ), absint( $last ), 10 ), self::get_query_amount( $source, $args, $context, $info ) ) + 1;
/**
* Set the default order
*/
$query_args['orderby'] = 'comment_date';
/**
* Take any of the $args that were part of the GraphQL query and map their
* GraphQL names to the WP_Term_Query names to be used in the WP_Term_Query
*
* @since 0.0.5
*/
$input_fields = [];
if ( ! empty( $args['where'] ) ) {
$input_fields = self::sanitize_input_fields( $args['where'], $source, $args, $context, $info );
}
/**
* Merge the default $query_args with the $args that were entered
* in the query.
*
* @since 0.0.5
*/
if ( ! empty( $input_fields ) ) {
$query_args = array_merge( $query_args, $input_fields );
}
/**
* Throw an exception if the query is attempted to be queried by
*/
if ( 'comment__in' === $query_args['orderby'] && empty( $query_args['comment__in'] ) ) {
throw new UserError( __( 'In order to sort by comment__in, an array of IDs must be passed as the commentIn argument', 'wp-graphql' ) );
}
/**
* If there's no orderby params in the inputArgs, set order based on the first/last argument
*/
if ( empty( $query_args['order'] ) ) {
$query_args['order'] = ! empty( $last ) ? 'ASC' : 'DESC';
}
/**
* Set the graphql_cursor_offset
*/
$query_args['graphql_cursor_offset'] = self::get_offset( $args );
$query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<';
/**
* Pass the graphql $args to the WP_Query
*/
$query_args['graphql_args'] = $args;
/**
* Handle setting dynamic $query_args based on the source (higher level query)
*/
if ( true === is_object( $source ) ) {
switch ( true ) {
case $source instanceof \WP_Post:
$query_args['post_id'] = absint( $source->ID );
break;
case $source instanceof \WP_User:
$query_args['user_id'] = absint( $source->ID );
break;
case $source instanceof \WP_Comment:
$query_args['parent'] = absint( $source->comment_ID );
break;
default:
break;
}
}
/**
* Filter the query_args that should be applied to the query. This filter is applied AFTER the input args from
* the GraphQL Query have been applied and has the potential to override the GraphQL Query Input Args.
*
* @param array $query_args array of query_args being passed to the
* @param mixed $source source passed down from the resolve tree
* @param array $args array of arguments input in the field as part of the GraphQL query
* @param AppContext $context object passed down zthe resolve tree
* @param ResolveInfo $info info about fields passed down the resolve tree
*
* @since 0.0.6
*/
$query_args = apply_filters( 'graphql_comment_connection_query_args', $query_args, $source, $args, $context, $info );
return $query_args;
}
/**
*
* @param $query_args
*
* @return \WP_Comment_Query
*/
public static function get_query( $query_args ) {
$query = new \WP_Comment_Query;
$query->query( $query_args );
return $query;
}
/**
* @param array $query The query that was processed to retrieve connection data
* @param array $items The array of connected items
* @param mixed $source The source being passed down the resolve tree
* @param array $args The Input args for the field
* @param AppContext $context The AppContext passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the resolve tree
*
* @return array
*/
public static function get_connection( $query, array $items, $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Get the $posts from the query
*/
$items = ! empty( $items ) && is_array( $items ) ? $items : [];
/**
* Set whether there is or is not another page
*/
$has_previous_page = ( ! empty( $args['last'] ) && count( $items ) > self::get_amount_requested( $args ) ) ? true : false;
$has_next_page = ( ! empty( $args['first'] ) && count( $items ) > self::get_amount_requested( $args ) ) ? true : false;
/**
* Slice the array to the amount of items that were requested
*/
$items = array_slice( $items, 0, self::get_amount_requested( $args ) );
/**
* Get the edges from the $items
*/
$edges = self::get_edges( $items, $source, $args, $context, $info );
/**
* Find the first_edge and last_edge
*/
$first_edge = $edges ? $edges[0] : null;
$last_edge = $edges ? $edges[ count( $edges ) - 1 ] : null;
$connection = [
'edges' => $edges,
'pageInfo' => [
'hasPreviousPage' => $has_previous_page,
'hasNextPage' => $has_next_page,
'startCursor' => ! empty( $first_edge['cursor'] ) ? $first_edge['cursor'] : null,
'endCursor' => ! empty( $last_edge['cursor'] ) ? $last_edge['cursor'] : null,
],
'nodes' => $items,
];
return $connection;
}
/**
* Takes an array of items and returns the edges
*
* @param $items
*
* @return array
*/
public static function get_edges( $items, $source, $args, $context, $info ) {
$edges = [];
/**
* If we're doing backward pagination we want to reverse the array before
* returning it to the edges
*/
if ( ! empty( $args['last'] ) ) {
$items = array_reverse( $items );
}
if ( ! empty( $items ) && is_array( $items ) ) {
foreach ( $items as $item ) {
$edges[] = [
'cursor' => ArrayConnection::offsetToCursor( $item->comment_ID ),
'node' => $item,
];
}
}
return $edges;
}
/**
* This sets up the "allowed" args, and translates the GraphQL-friendly keys to
* WP_Comment_Query friendly keys.
*
* There's probably a cleaner/more dynamic way to approach this, but this was quick. I'd be
* down to explore more dynamic ways to map this, but for now this gets the job done.
*
* @param array $args The array of query arguments
* @param mixed $source The query results
* @param array $all_args Array of all of the original arguments (not just the "where"
* args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object for the query
*
* @since 0.0.5
* @access private
* @return array
*/
public static function sanitize_input_fields( array $args, $source, array $all_args, AppContext $context, ResolveInfo $info ) {
$arg_mapping = [
'authorEmail' => 'author_email',
'authorIn' => 'author__in',
'authorNotIn' => 'author__not_in',
'authorUrl' => 'author_url',
'commentIn' => 'comment__in',
'commentNotIn' => 'comment__not_in',
'contentAuthor' => 'post_author',
'contentAuthorIn' => 'post_author__in',
'contentAuthorNotIn' => 'post_author__not_in',
'contentId' => 'post_id',
'contentIdIn' => 'post__in',
'contentIdNotIn' => 'post__not_in',
'contentName' => 'post_name',
'contentParent' => 'post_parent',
'contentStatus' => 'post_status',
'contentType' => 'post_type',
'includeUnapproved' => 'includeUnapproved',
'parentIn' => 'parent__in',
'parentNotIn' => 'parent__not_in',
'userId' => 'user_id',
];
/**
* Map and sanitize the input args to the WP_Comment_Query compatible args
*/
$query_args = Types::map_input( $args, $arg_mapping );
/**
* Filter the input fields
*
* This allows plugins/themes to hook in and alter what $args should be allowed to be passed
* from a GraphQL Query to the get_terms query
*
* @since 0.0.5
*/
$query_args = apply_filters( 'graphql_map_input_fields_to_wp_comment_query', $query_args, $args, $source, $all_args, $context, $info );
return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace WPGraphQL\Type\CommentAuthor;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
class CommentAuthorType extends WPObjectType {
/**
* Holds the type name
*
* @var string $type_name
*/
private static $type_name;
/**
* Holds the $fields definition for the CommentAuthorType
*
* @var $fields
*/
private static $fields;
/**
* CommentAuthorType constructor.
*
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
*
* @since 0.0.5
*/
self::$type_name = 'CommentAuthor';
$config = [
'name' => self::$type_name,
'description' => __( 'A Comment Author object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* This defines the fields that make up the CommentAuthorType
*
* @return mixed
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The globally unique identifier for the Comment Author user', 'wp-graphql' ),
'resolve' => function( array $comment_author, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment_author['comment_author_email'] ) ? Relay::toGlobalId( 'commentAuthor', $comment_author['comment_author_email'] ) : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'The name for the comment author.', 'wp-graphql' ),
'resolve' => function( array $comment_author, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment_author['comment_author'] ) ? $comment_author['comment_author'] : '';
},
],
'email' => [
'type' => Types::string(),
'description' => __( 'The email for the comment author', 'wp-graphql' ),
'resolve' => function( array $comment_author, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment_author['comment_author_email'] ) ? $comment_author['comment_author_email'] : '';
},
],
'url' => [
'type' => Types::string(),
'description' => __( 'The url the comment author.', 'wp-graphql' ),
'resolve' => function( array $comment_author, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $comment_author['comment_author_url'] ) ? $comment_author['comment_author_url'] : '';
},
],
];
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace WPGraphQL\Type\EditLock;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
class EditLockType extends WPObjectType {
/**
* Holds the $fields definition for the PostObjectType
*
* @var $fields
*/
private static $type_name;
/**
* Holds the post_type_object
*
* @var object $post_type_object
*/
private static $fields;
/**
* EditLockType constructor.
*/
public function __construct() {
self::$type_name = 'EditLock';
$config = [
'name' => self::$type_name,
'description' => __( 'Info on whether the object is locked by another user editing it', 'wp-graphql' ),
'fields' => function() {
return self::fields();
},
];
parent::__construct( $config );
}
/**
* Configures the fields for the EditLock type
* @return mixed|null
*/
protected static function fields() {
if ( null === self::$fields ) {
$fields = [
'editTime' => [
'type' => Types::string(),
'description' => __( 'The time when the object was last edited', 'wp-graphql' ),
'resolve' => function( $edit_lock, array $args, AppContext $context, ResolveInfo $info ) {
$time = ( is_array( $edit_lock ) && ! empty( $edit_lock[0] ) ) ? $edit_lock[0] : null;
return ! empty( $time ) ? date( 'Y-m-d H:i:s', $time ) : null;
},
],
'user' => [
'type' => Types::user(),
'description' => __( 'The user that most recently edited the object', 'wp-graphql' ),
'resolve' => function( $edit_lock, array $args, AppContext $context, ResolveInfo $info ) {
$user_id = ( is_array( $edit_lock ) && ! empty( $edit_lock[1] ) ) ? $edit_lock[1] : null;
return ! empty( $user_id ) ? DataSource::resolve_user( $user_id ) : null;
},
],
];
self::$fields = self::prepare_fields( $fields, self::$type_name );
}
return ! empty( self::$fields ) ? self::$fields : null;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
/**
* Class MediaItemStatusEnumType
*
* This defines an EnumType with allowed post stati for attachments in WordPress.
* Attachments do not have the same status capabilities as other post types, see here
* for reference: https://github.com/WordPress/WordPress/blob/master/wp-includes/post.php#L3072
*
* @package WPGraphQL\Type\Enum
*/
class MediaItemStatusEnumType extends WPEnumType {
/**
* This holds the enum values array
*
* @var array $values
*/
private static $values;
public function __construct() {
$config = [
'name' => 'MediaItemStatus',
'description' => __( 'The status of the media item object.', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* values
* Creates a list of post_stati that can be used to query by.
*
* @return array
*/
private static function values() {
/**
* Set the default, if no values are built dynamically
*
*/
self::$values = [
'INHERIT' => [
'value' => 'inherit',
],
];
/**
* Get the dynamic list of post_stati
*/
$post_stati = [
'inherit',
'private',
'trash',
'auto-draft',
];
/**
* If there are $post_stati, create the $values based on them
*/
if ( ! empty( $post_stati ) && is_array( $post_stati ) ) {
/**
* Reset the array
*/
self::$values = [];
/**
* Loop through the post_stati
*/
foreach ( $post_stati as $status ) {
$formatted_status = strtoupper( preg_replace( '/[^A-Za-z0-9]/i', '_', $status ) );
self::$values[ $formatted_status ] = [
'name' => $formatted_status,
'description' => sprintf( __( 'Objects with the %1$s status', 'wp-graphql' ), $status ),
'value' => $status,
];
}
}
/**
* Return the $values
*/
return self::$values;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
/**
* Class MimeTypeEnumType
*
* This defines an EnumType with allowed mime types that are registered to WordPress.
*
* @package WPGraphQL\Type\Enum
* @since 0.0.5
*/
class MimeTypeEnumType extends WPEnumType {
/**
* This holds the enum values array
*
* @var array $values
*/
private static $values;
/**
* MimeTypeEnumType constructor.
*
* @since 0.0.5
*/
public function __construct() {
$config = [
'name' => 'MimeType',
'description' => __( 'The MimeType of the object', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* values
* Returns the values to be used in the Enum
*
* @return array|null
*/
private static function values() {
if ( null === self::$values ) {
/**
* Establish a default MimeType value to ensure we don't
* return null values
*
* @since 0.0.5
*/
self::$values = [
'IMAGE_JPEG' => [
'value' => 'image/jpeg',
],
];
$allowed_mime_types = get_allowed_mime_types();
if ( ! empty( $allowed_mime_types ) ) {
self::$values = [];
foreach ( $allowed_mime_types as $mime_type ) {
$formatted_mime_type = strtoupper( preg_replace( '/[^A-Za-z0-9]/i', '_', $mime_type ) );
self::$values[ $formatted_mime_type ] = [
'value' => $mime_type,
];
}
}
}
return self::$values;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
/**
* Class PostObjectFieldFormatEnumType
*
* This defines an EnumType with allowed formats of post field data.
*
* @package WPGraphQL\Type\Enum
* @since 0.0.18
*/
class PostObjectFieldFormatEnumType extends WPEnumType {
/**
* This holds the enum values array.
*
* @var array $values
*/
private static $values;
public function __construct() {
$config = [
'name' => 'PostObjectFieldFormat',
'description' => __( 'The format of post field data.', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* Creates a list of formats of post field data.
*
* @return array
*/
private static function values() {
if ( null === self::$values ) {
/**
* Post object field formats.
*
* @since 0.0.18
*/
self::$values = [
'RAW' => [
'name' => 'RAW',
'description' => __( 'Provide the field value directly from database', 'wp-graphql' ),
'value' => 'raw',
],
'RENDERED' => [
'name' => 'RENDERED',
'description' => __( 'Apply the default WordPress rendering', 'wp-graphql' ),
'value' => 'rendered',
],
];
}
/**
* Return the $values
*/
return self::$values;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
/**
* Class PostStatusEnumType
*
* This defines an EnumType with allowed post stati that are registered to WordPress.
*
* @package WPGraphQL\Type\Enum
* @since 0.0.5
*/
class PostStatusEnumType extends WPEnumType {
/**
* This holds the enum values array
*
* @var array $values
*/
private static $values;
public function __construct() {
$config = [
'name' => 'PostStatusEnum',
'description' => __( 'The status of the object.', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* values
* Creates a list of post_stati that can be used to query by.
*
* @return array
*/
private static function values() {
/**
* Set the default, if no values are built dynamically
*
* @since 0.0.5
*/
self::$values = [
'name' => 'PUBLISH',
'value' => 'publish',
];
/**
* Get the dynamic list of post_stati
*/
$post_stati = get_post_stati();
/**
* If there are $post_stati, create the $values based on them
*/
if ( ! empty( $post_stati ) && is_array( $post_stati ) ) {
/**
* Reset the array
*/
self::$values = [];
/**
* Loop through the post_stati
*/
foreach ( $post_stati as $status ) {
$formatted_status = strtoupper( preg_replace( '/[^A-Za-z0-9]/i', '_', $status ) );
self::$values[ $formatted_status ] = [
'description' => sprintf( __( 'Objects with the %1$s status', 'wp-graphql' ), $status ),
'value' => $status,
];
}
}
/**
* Return the $values
*/
return self::$values;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WPGraphQL\Type\Enum;
use GraphQL\Type\Definition\EnumType;
/**
* Class PostTypeEnumType
*
* @package WPGraphQL\Type\Enum
*/
class PostTypeEnumType extends EnumType {
/**
* Holds the values to be used for the Enum
* @var array $values
*/
private static $values;
/**
* PostTypeEnumType constructor.
*/
public function __construct() {
$config = [
'name' => 'PostTypeEnum',
'description' => __( 'Allowed Post Types', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* This returns an array of values to be used by the Enum
* @return array|null
*/
private static function values() {
if ( null === self::$values ) {
/**
* Set an empty array
*/
self::$values = [];
/**
* Get the allowed taxonomies
*/
$allowed_post_types = \WPGraphQL::get_allowed_post_types();
/**
* Loop through the taxonomies and create an array
* of values for use in the enum type.
*/
foreach ( $allowed_post_types as $post_type ) {
$formatted_post_type = strtoupper( get_post_type_object( $post_type )->graphql_single_name );
self::$values[ $formatted_post_type ] = [
'value' => $post_type,
];
}
}
/**
* Return the $values
*/
return ! empty( self::$values ) ? self::$values : null;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
/**
* Class RelationEnumType
*
* This defines an EnumType with allowed relations for use in various query args.
*
* @package WPGraphQL\Type\Enum
* @since 0.0.5
*/
class RelationEnumType extends WPEnumType {
/**
* This holds the enum values array
*
* @var array $values
*/
private static $values;
/**
* RelationEnumType constructor.
*
* @since 0.0.5
*/
public function __construct() {
$config = [
'name' => 'RelationEnum',
'description' => __( 'The logical relation between each item in the array when there are more than one.', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* values
* Returns the values to be used in the Enum
*
* @return array|null
*/
private static function values() {
if ( null === self::$values ) {
self::$values = [
'AND' => [
'name' => 'AND',
'value' => 'AND',
],
'OR' => [
'name' => 'OR',
'value' => 'OR',
],
];
}
return self::$values;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace WPGraphQL\Type\Enum;
use WPGraphQL\Type\WPEnumType;
class TaxonomyEnumType extends WPEnumType {
/**
* This holds the enum values array
*
* @var array $values
*/
private static $values;
/**
* TaxonomyEnumType constructor.
*
* @since 0.0.5
*/
public function __construct() {
$config = [
'name' => 'TaxonomyEnum',
'description' => __( 'Allowed taxonomies', 'wp-graphql' ),
'values' => self::values(),
];
parent::__construct( $config );
}
/**
* values
* Returns the values to be used in the Enum
*
* @return array|null
*/
private static function values() {
if ( null === self::$values ) {
/**
* Set an empty array
*/
self::$values = [];
/**
* Get the allowed taxonomies
*/
$allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies();
/**
* Loop through the taxonomies and create an array
* of values for use in the enum type.
*/
foreach ( $allowed_taxonomies as $taxonomy ) {
$formatted_taxonomy = strtoupper( get_taxonomy( $taxonomy )->graphql_single_name );
self::$values[ $formatted_taxonomy ] = [
'value' => $taxonomy,
];
}
}
/**
* Return the $values
*/
return self::$values;
}
}

View File

@@ -0,0 +1,299 @@
<?php
namespace WPGraphQL\Type\MediaItem;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class MediaItemType
*
* This class isn't a full definition of a new Type, instead it's used to customize
* the shape of the mediaItemType (via filter), which is instantiated as a PostObjectType.
*
* @see : wp-graphql.php - add_filter( 'graphql_mediaItem_fields', [ '\WPGraphQL\Type\MediaItem\MediaItemType',
* 'fields' ], 10, 1 );
*
* @package WPGraphQL\Type\MediaItem
*/
class MediaItemType {
/**
* Holds the object definition for media details
*
* @var object $media_details
*/
private static $media_details;
/**
* Holds the object definition for media item meta
*
* @var object $media_item_meta
*/
private static $media_item_meta;
/**
* Holds the object definition for media sizes
*
* @var object $media_sizes
*/
private static $media_sizes;
/**
* This customizes the fields for the mediaItem type ( attachment post_type) as the shape of the mediaItem Schema
* is different than a standard post
*
* @see: wp-graphql.php - add_filter( 'graphql_mediaItem_fields' );add_filter( 'graphql_mediaItem_fields', [
* '\WPGraphQL\Type\MediaItem\MediaItemType', 'fields' ], 10, 1 );
*
* @param array $fields
*
* @return array
*/
public static function fields( $fields ) {
/**
* Deprecate fields for the mediaItem type.
* These fields can still be queried, but are just not preferred for the mediaItem type
*
* @since 0.0.6
*/
$fields['excerpt']['isDeprecated'] = true;
$fields['excerpt']['deprecationReason'] = __( 'Use the caption field instead of excerpt', 'wp-graphql' );
$fields['content']['isDeprecated'] = true;
$fields['content']['deprecationReason'] = __( 'Use the description field instead of content', 'wp-graphql' );
/**
* Add new fields to the mediaItem type
*
* @since 0.0.6
*/
$new_fields = [
'caption' => [
'type' => Types::string(),
'description' => __( 'The caption for the resource', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
$caption = apply_filters( 'the_excerpt', $post->post_excerpt );
return ! empty( $caption ) ? $caption : null;
},
],
'altText' => [
'type' => Types::string(),
'description' => __( 'Alternative text to display when resource is not displayed', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the image (stored as post_content)', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return apply_filters( 'the_content', $post->post_content );
},
],
'mediaType' => [
'type' => Types::string(),
'description' => __( 'Type of resource', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
},
],
'sourceUrl' => [
'type' => Types::string(),
'description' => __( 'Url of the mediaItem', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return wp_get_attachment_url( $post->ID );
},
],
'mimeType' => [
'type' => Types::string(),
'description' => __( 'The mime type of the mediaItem', 'wp-graphql' ),
'resolve' =>function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return ! empty( $post->post_mime_type ) ? $post->post_mime_type : null;
},
],
'mediaDetails' => [
'type' => self::media_details(),
'description' => __( 'Details about the mediaItem', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, $context, ResolveInfo $info ) {
return wp_get_attachment_metadata( $post->ID );
},
],
];
return array_merge( $fields, $new_fields );
}
/**
* This defines the media details object type that can be queried on mediaItems
*
* @return null|WPObjectType
* @since 0.0.6
*/
private static function media_details() {
if ( null === self::$media_details ) {
self::$media_details = new WPObjectType( [
'name' => 'MediaDetails',
'fields' => function() {
$fields = [
'width' => [
'type' => Types::int(),
'description' => __( 'The width of the mediaItem', 'wp-graphql' ),
],
'height' => [
'type' => Types::int(),
'description' => __( 'The height of the mediaItem', 'wp-graphql' ),
],
'file' => [
'type' => Types::string(),
'description' => __( 'The height of the mediaItem', 'wp-graphql' ),
],
'sizes' => [
'type' => Types::list_of( self::media_sizes() ),
'description' => __( 'The available sizes of the mediaItem', 'wp-graphql' ),
'resolve' => function( $media_details, $args, $context, ResolveInfo $info ) {
if ( ! empty( $media_details['sizes'] ) ) {
foreach ( $media_details['sizes'] as $size_name => $size ) {
$size['name'] = $size_name;
$sizes[] = $size;
}
}
return ! empty( $sizes ) ? $sizes : null;
},
],
'meta' => [
'type' => self::media_item_meta(),
'resolve' => function( $media_details, $args, $context, ResolveInfo $info ) {
return ! empty( $media_details['image_meta'] ) ? $media_details['image_meta'] : null;
},
],
];
return WPObjectType::prepare_fields( $fields, 'MediaDetails' );
},
] );
} // End if().
return ! empty( self::$media_details ) ? self::$media_details : null;
}
/**
* This defines the media item meta object type that can be queried on mediaItems
*
* @return null|WPObjectType
* @since 0.0.6
*/
private static function media_item_meta() {
if ( null === self::$media_item_meta ) {
self::$media_item_meta = new WPObjectType( [
'name' => 'MediaItemMeta',
'fields' => [
'aperture' => [
'type' => Types::float(),
],
'credit' => [
'type' => Types::string(),
],
'camera' => [
'type' => Types::string(),
],
'caption' => [
'type' => Types::string(),
],
'createdTimestamp' => [
'type' => Types::int(),
'resolve' => function( $meta, $args, $context, ResolveInfo $info ) {
return ! empty( $meta['created_timestamp'] ) ? $meta['created_timestamp'] : null;
},
],
'copyright' => [
'type' => Types::string(),
],
'focalLength' => [
'type' => Types::int(),
'resolve' => function( $meta, $args, $context, ResolveInfo $info ) {
return ! empty( $meta['focal_length'] ) ? $meta['focal_length'] : null;
},
],
'iso' => [
'type' => Types::int(),
],
'shutterSpeed' => [
'type' => Types::float(),
'resolve' => function( $meta, $args, $context, ResolveInfo $info ) {
return ! empty( $meta['shutter_speed'] ) ? $meta['shutter_speed'] : null;
},
],
'title' => [
'type' => Types::string(),
],
'orientation' => [
'type' => Types::string(),
],
'keywords' => [
'type' => Types::list_of( Types::string() ),
],
],
] );
} // End if().
return ! empty( self::$media_item_meta ) ? self::$media_item_meta : null;
}
/**
* This defines the sizes object type that can be queried on mediaItems within the mediaDetails
*
* @return null|WPObjectType
* @since 0.0.6
*/
private static function media_sizes() {
if ( null === self::$media_sizes ) {
self::$media_sizes = new WPObjectType( [
'name' => 'MediaSizes',
'fields' => [
'name' => [
'type' => Types::string(),
'description' => __( 'The referenced size name', 'wp-graphql' ),
],
'file' => [
'type' => Types::string(),
'description' => __( 'The file of the for the referenced size', 'wp-graphql' ),
],
'width' => [
'type' => Types::string(),
'description' => __( 'The width of the for the referenced size', 'wp-graphql' ),
],
'height' => [
'type' => Types::string(),
'description' => __( 'The height of the for the referenced size', 'wp-graphql' ),
],
'mimeType' => [
'type' => Types::string(),
'description' => __( 'The mime type of the resource', 'wp-graphql' ),
'resolve' => function( $image, $args, $context, ResolveInfo $info ) {
return ! empty( $image['mime-type'] ) ? $image['mime-type'] : null;
},
],
'sourceUrl' => [
'type' => Types::string(),
'description' => __( 'The url of the for the referenced size', 'wp-graphql' ),
'resolve' => function( $image, $args, $context, ResolveInfo $info ) {
return ! empty( $image['file'] ) ? $image['file'] : null;
},
],
],
] );
} // End if().
return ! empty( self::$media_sizes ) ? self::$media_sizes : null;
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace WPGraphQL\Type\MediaItem\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class MediaItemCreate
*
* @package WPGraphQL\Type\MediaItem\Mutation
*/
class MediaItemCreate {
/**
* Holds the mutation field definition
*
* @var array mutation
*/
private static $mutation = [];
/**
* Defines the create mutation for MediaItems
*
* @var \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'CreateMediaItem';
self::$mutation['mediaItem'] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
'description' => __( 'Create mediaItems', 'wp-graphql' ),
'inputFields' => self::input_fields( $post_type_object ),
'outputFields' => [
'mediaItem' => [
'type' => Types::post_object( $post_type_object->name ),
'resolve' => function( $payload ) {
return get_post( $payload['id'] );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
/**
* Stop now if a user isn't allowed to upload a mediaItem
*/
if ( ! current_user_can( 'upload_files' ) ) {
throw new UserError( __( 'Sorry, you are not allowed to upload mediaItems', 'wp-graphql' ) );
}
/**
* Set the file name, whether it's a local file or from a URL.
* Then set the url for the uploaded file
*/
$file_name = basename( $input['filePath'] );
$uploaded_file_url = $input['filePath'];
/**
* Require the file.php file from wp-admin. This file includes the
* download_url and wp_handle_sideload methods
*/
require_once( ABSPATH . 'wp-admin/includes/file.php' );
/**
* If the mediaItem file is from a local server, use wp_upload_bits before saving it to the uploads folder
*/
if ( 'file' === parse_url( $input['filePath'], PHP_URL_SCHEME ) ) {
$uploaded_file = wp_upload_bits( $file_name, null, file_get_contents( $input['filePath'] ) );
$uploaded_file_url = ( empty ( $uploaded_file['error'] ) ? $uploaded_file['url'] : null );
}
/**
* URL data for the mediaItem, timeout value is the default, see:
* https://developer.wordpress.org/reference/functions/download_url/
*/
$timeout_seconds = 300;
$temp_file = download_url( $uploaded_file_url, $timeout_seconds );
/**
* Handle the error from download_url if it occurs
*/
if ( is_wp_error( $temp_file ) ) {
throw new UserError( __( 'Sorry, the URL for this file is invalid, it must be a valid URL', 'wp-graphql' ) );
}
/**
* Build the file data for side loading
*/
$file_data = [
'name' => $file_name,
'type' => ! empty ( $input['fileType'] ) ? $input['fileType'] : wp_check_filetype( $temp_file ),
'tmp_name' => $temp_file,
'error' => 0,
'size' => filesize( $temp_file ),
];
/**
* Tells WordPress to not look for the POST form fields that would normally be present as
* we downloaded the file from a remote server, so there will be no form fields
* The default is true
*/
$overrides = [
'test_form' => false,
];
/**
* Insert the mediaItem and retrieve it's data
*/
$file = wp_handle_sideload( $file_data, $overrides );
/**
* Handle the error from wp_handle_sideload if it occurs
*/
if ( ! empty( $file['error'] ) ) {
throw new UserError( __( 'Sorry, the URL for this file is invalid, it must be a path to the mediaItem file', 'wp-graphql' ) );
}
/**
* Insert the mediaItem object and get the ID
*/
$media_item_args = MediaItemMutation::prepare_media_item( $input, $post_type_object, $mutation_name, $file );
/**
* Get the post parent and if it's not set, set it to false
*/
$attachment_parent_id = ( ! empty( $media_item_args['post_parent'] ) ? $media_item_args['post_parent'] : false );
/**
* Stop now if a user isn't allowed to edit the parent post
*/
$parent = get_post( $attachment_parent_id );
if ( null !== get_post( $attachment_parent_id ) ) {
$post_parent_type = get_post_type_object( $parent->post_type );
if ( ! current_user_can( $post_parent_type->cap->edit_post, $attachment_parent_id ) ) {
throw new UserError( __( 'Sorry, you are not allowed to upload mediaItems to this post', 'wp-graphql' ) );
}
}
/**
* If the mediaItem being created is being assigned to another user that's not the current user, make sure
* the current user has permission to edit others mediaItems
*/
if ( ! empty( $input['authorId'] ) && get_current_user_id() !== $input['authorId'] && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
throw new UserError( __( 'Sorry, you are not allowed to create mediaItems as this user', 'wp-graphql' ) );
}
/**
* Insert the mediaItem
*
* Required Argument defaults are set in the main MediaItemMutation.php if they aren't set
* by the user during input, they are:
* post_title (pulled from file if not entered)
* post_content (empty string if not entered)
* post_status (inherit if not entered)
* post_mime_type (pulled from the file if not entered in the mutation)
*/
$attachment_id = wp_insert_attachment( $media_item_args, $file['file'], $attachment_parent_id );
/**
* Check if the wp_generate_attachment_metadata method exists and include it if not
*/
require_once( ABSPATH . 'wp-admin/includes/image.php' );
/**
* Generate and update the mediaItem's metadata.
* If we make it this far the file and attachment
* have been validated and we will not receive any errors
*/
$attachment_data = wp_generate_attachment_metadata( $attachment_id, $file['file'] );
$attachment_data_update = wp_update_attachment_metadata( $attachment_id, $attachment_data );
/**
* Update alt text postmeta for mediaItem
*/
MediaItemMutation::update_additional_media_item_data( $attachment_id, $input, $post_type_object, $mutation_name, $context, $info );
return [
'id' => $attachment_id,
];
},
] );
return ! empty( self::$mutation['mediaItem'] ) ? self::$mutation['mediaItem'] : null;
}
/**
* Add the filePath as a nonNull field for create mutations as its required
* to create a media item
*
* @param \WP_Post_Type $post_type_object
*
* @return array
*/
private static function input_fields( $post_type_object ) {
/**
* Creating mutations requires a filePath to be passed
*/
return array_merge(
[
'filePath' => [
'type' => Types::non_null( Types::string() ),
'description' => __( 'The URL or file path to the mediaItem', 'wp-graphql' ),
],
],
MediaItemMutation::input_fields( $post_type_object )
);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace WPGraphQL\Type\MediaItem\Mutation;
use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
/**
* Class MediaItemDelete
*
* @package WPGraphQL\Type\mediaItem\Mutation
*/
class MediaItemDelete {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the delete mutation for MediaItems
*
* @param \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
/**
* Set the name of the media item mutation being performed
*/
$mutation_name = 'DeleteMediaItem';
self::$mutation['mediaItem'] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
'description' => __( 'Delete mediaItem objects. By default mediaItem objects will be moved to the trash unless the forceDelete is used', 'wp-graphql' ),
'inputFields' => [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The ID of the mediaItem to delete', 'wp-graphql' ),
],
'forceDelete' => [
'type' => Types::boolean(),
'description' => __( 'Whether the mediaItem should be force deleted instead of being moved to the trash', 'wp-graphql' ),
],
],
'outputFields' => [
'deletedId' => [
'type' => Types::id(),
'description' => __( 'The ID of the deleted mediaItem', 'wp-graphql' ),
'resolve' => function( $payload ) use ( $post_type_object ) {
$deleted = (object) $payload['mediaItemObject'];
return ! empty( $deleted->ID ) ? Relay::toGlobalId( $post_type_object->name, absint( $deleted->ID ) ) : null;
},
],
'mediaItem' => [
'type' => Types::post_object( $post_type_object->name ),
'description' => __( 'The mediaItem before it was deleted', 'wp-graphql' ),
'resolve' => function( $payload ) {
$deleted = (object) $payload['mediaItemObject'];
return ! empty( $deleted ) ? $deleted : null;
},
],
],
'mutateAndGetPayload' => function( $input ) use ( $post_type_object, $mutation_name ) {
/**
* Get the ID from the global ID
*/
$id_parts = Relay::fromGlobalId( $input['id'] );
$existing_media_item = get_post( absint( $id_parts['id'] ) );
/**
* If there's no existing mediaItem, throw an exception
*/
if ( empty( $existing_media_item ) ) {
throw new UserError( __( 'No mediaItem could be found to delete', 'wp-graphql' ) );
}
/**
* Stop now if a user isn't allowed to delete a mediaItem
*/
if ( ! current_user_can( $post_type_object->cap->delete_post, absint( $id_parts['id'] ) ) ) {
throw new UserError( __( 'Sorry, you are not allowed to delete mediaItems', 'wp-graphql' ) );
}
/**
* Check if we should force delete or not
*/
$force_delete = ( ! empty( $input['forceDelete'] ) && true === $input['forceDelete'] ) ? true : false;
/**
* Get the mediaItem object before deleting it
*/
$media_item_before_delete = get_post( absint( $id_parts['id'] ) );
/**
* If the mediaItem isn't of the attachment post type, throw an error
*/
if ( 'attachment' !== $media_item_before_delete->post_type ) {
throw new UserError( sprintf( __( 'Sorry, the item you are trying to delete is a %1%s, not a mediaItem', 'wp-graphql' ), $media_item_before_delete->post_type ) );
}
/**
* If the mediaItem is already in the trash, and the forceDelete input was not passed,
* don't remove from the trash
*/
if ( 'trash' === $media_item_before_delete->post_status ) {
if ( true !== $force_delete ) {
// Translators: the first placeholder is the post_type of the object being deleted and the second placeholder is the unique ID of that object
throw new UserError( sprintf( __( 'The mediaItem with id %1$s is already in the trash. To remove from the trash, use the forceDelete input', 'wp-graphql' ), $input['id'] ) );
}
}
/**
* Delete the mediaItem. This will not throw false thanks to
* all of the above validation
*/
$deleted = wp_delete_attachment( $id_parts['id'], $force_delete );
/**
* If the post was moved to the trash, spoof the object's status before returning it
*/
$media_item_before_delete->post_status = ( false !== $deleted && true !== $force_delete ) ? 'trash' : $media_item_before_delete->post_status;
/**
* Return the deletedId and the mediaItem before it was deleted
*/
return [
'mediaItemObject' => $media_item_before_delete,
];
},
] );
return ! empty( self::$mutation['mediaItem'] ) ? self::$mutation['mediaItem'] : null;
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace WPGraphQL\Type\MediaItem\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class MediaItemMutation
*
* @package WPGraphQL\Type\MediaItem
*/
class MediaItemMutation {
/**
* Holds the input fields configuration
*
* @var array
*/
private static $input_fields = [];
/**
* @param $post_type_object
*
* @return mixed|array|null $input_fields
*/
public static function input_fields( $post_type_object ) {
if ( ! empty( $post_type_object->graphql_single_name ) && empty( self::$input_fields[ $post_type_object->graphql_single_name ] ) ) {
$input_fields = [
'altText' => [
'type' => Types::string(),
'description' => __( 'Alternative text to display when mediaItem is not displayed', 'wp-graphql' ),
],
'authorId' => [
'type' => Types::id(),
'description' => __( 'The userId to assign as the author of the mediaItem', 'wp-graphql' ),
],
'caption' => [
'type' => Types::string(),
'description' => __( 'The caption for the mediaItem', 'wp-graphql' ),
],
'commentStatus' => [
'type' => Types::string(),
'description' => __( 'The comment status for the mediaItem', 'wp-graphql' ),
],
'date' => [
'type' => Types::string(),
'description' => __( 'The date of the mediaItem', 'wp-graphql' ),
],
'dateGmt' => [
'type' => Types::string(),
'description' => __( 'The date (in GMT zone) of the mediaItem', 'wp-graphql' ),
],
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the mediaItem', 'wp-graphql' ),
],
'filePath' => [
'type' => Types::string(),
'description' => __( 'The file name of the mediaItem', 'wp-graphql' ),
],
'fileType' => [
'type' => Types::mime_type_enum(),
'description' => __( 'The file type of the mediaItem', 'wp-graphql' ),
],
'slug' => [
'type' => Types::string(),
'description' => __( 'The slug of the mediaItem', 'wp-graphql' ),
],
'status' => [
'type' => Types::media_item_status_enum(),
'description' => __( 'The status of the mediaItem', 'wp-graphql' ),
],
'title' => [
'type' => Types::string(),
'description' => __( 'The title of the mediaItem', 'wp-graphql' ),
],
'pingStatus' => [
'type' => Types::string(),
'description' => __( 'The ping status for the mediaItem', 'wp-graphql' ),
],
'parentId' => [
'type' => Types::id(),
'description' => __( 'The WordPress post ID or the graphQL postId of the parent object', 'wp-graphql' ),
],
];
/**
* Filters the mutation input fields for the mediaItem
*
* @param array $input_fields The array of input fields
* @param \WP_Post_Type $post_type_object The post_type object for the mediaItem
*/
self::$input_fields[ $post_type_object->graphql_single_name ] = apply_filters( 'graphql_media_item_mutation_input_fields', $input_fields, $post_type_object );
} // End if().
return ! empty( self::$input_fields[ $post_type_object->graphql_single_name ] ) ? self::$input_fields[ $post_type_object->graphql_single_name ] : null;
}
/**
* This prepares the media item for insertion
*
* @param array $input The input for the mutation from the GraphQL request
* @param \WP_Post_Type $post_type_object The post_type_object for the mediaItem (attachment)
* @param string $mutation_name The name of the mutation being performed (create, update, etc.)
* @param mixed $file The mediaItem (attachment) file
*
* @return array $media_item_args
*/
public static function prepare_media_item( $input, $post_type_object, $mutation_name, $file ) {
/**
* Set the post_type (attachment) for the insert
*/
$insert_post_args['post_type'] = $post_type_object->name;
/**
* Prepare the data for inserting the mediaItem
* NOTE: These are organized in the same order as: http://v2.wp-api.org/reference/media/#schema-meta
*/
if ( ! empty( $input['date'] ) && false !== strtotime( $input['date'] ) ) {
$insert_post_args['post_date'] = date( 'Y-m-d H:i:s', strtotime( $input['date'] ) );
}
if ( ! empty( $input['dateGmt'] ) && false !== strtotime( $input['dateGmt'] ) ) {
$insert_post_args['post_date_gmt'] = date( 'Y-m-d H:i:s', strtotime( $input['dateGmt'] ) );
}
if ( ! empty( $input['slug'] ) ) {
$insert_post_args['post_name'] = $input['slug'];
}
if ( ! empty( $input['status'] ) ) {
$insert_post_args['post_status'] = $input['status'];
} else {
$insert_post_args['post_status'] = 'inherit';
}
if ( ! empty( $input['title'] ) ) {
$insert_post_args['post_title'] = $input['title'];
} elseif ( ! empty( $file['file'] ) ) {
$insert_post_args['post_title'] = basename( $file['file'] );
}
$author_id_parts = ! empty( $input['authorId'] ) ? Relay::fromGlobalId( $input['authorId'] ) : null;
if ( is_array( $author_id_parts ) && ! empty( $author_id_parts['id'] ) ) {
$insert_post_args['post_author'] = absint( $author_id_parts['id'] );
}
if ( ! empty( $input['commentStatus'] ) ) {
$insert_post_args['comment_status'] = $input['commentStatus'];
}
if ( ! empty( $input['pingStatus'] ) ) {
$insert_post_args['ping_status'] = $input['pingStatus'];
}
if ( ! empty( $input['caption'] ) ) {
$insert_post_args['post_excerpt'] = $input['caption'];
}
if ( ! empty( $input['description'] ) ) {
$insert_post_args['post_content'] = $input['description'];
} else {
$insert_post_args['post_content'] = '';
}
if ( ! empty( $file['type'] ) ) {
$insert_post_args['post_mime_type'] = $file['type'];
} elseif ( ! empty( $input['fileType'] ) ) {
$insert_post_args['post_mime_type'] = $input['fileType'];
}
if ( ! empty( $input['parentId'] ) ) {
$parent_id_parts = ( ! empty( $input['parentId'] ) ? Relay::fromGlobalId( $input['parentId'] ) : null );
if ( is_array( $parent_id_parts ) && absint( $parent_id_parts['id'] ) ) {
$insert_post_args['post_parent'] = absint( $parent_id_parts['id'] );
} else {
$insert_post_args['post_parent'] = absint( $input['parentId'] );
}
}
/**
* Filter the $insert_post_args
*
* @param array $insert_post_args The array of $input_post_args that will be passed to wp_insert_attachment
* @param array $input The data that was entered as input for the mutation
* @param \WP_Post_Type $post_type_object The post_type_object that the mutation is affecting
* @param string $mutation_type The type of mutation being performed (create, update, delete)
*/
$insert_post_args = apply_filters( 'graphql_media_item_insert_post_args', $insert_post_args, $input, $post_type_object, $mutation_name );
return $insert_post_args;
}
/**
* This updates additional data related to a mediaItem, such as postmeta.
*
* @param int $media_item_id The ID of the media item being mutated
* @param array $input The input on the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the item being mutated
* @param string $mutation_name The name of the mutation
* @param AppContext $context The AppContext that is passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo that is passed down the resolve tree
*/
public static function update_additional_media_item_data( $media_item_id, $input, $post_type_object, $mutation_name, AppContext $context, ResolveInfo $info ) {
/**
* Update alt text postmeta for the mediaItem
*/
if ( ! empty( $input['altText'] ) ) {
update_post_meta( $media_item_id, '_wp_attachment_image_alt', $input['altText'] );
}
/**
* Run an action after the additional data has been updated. This is a great spot to hook into to
* update additional data related to mediaItems, such as updating additional postmeta,
* or sending emails to Kevin. . .whatever you need to do with the mediaItem.
*
* @param int $media_item_id The ID of the mediaItem being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext that is passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo that is passed down the resolve tree
*/
do_action( 'graphql_media_item_mutation_update_additional_data', $media_item_id, $input, $post_type_object, $mutation_name, $context, $info );
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WPGraphQL\Type\MediaItem\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class MediaItemUpdate
*
* @package WPGraphQL\Type\PostObject\Mutation
*/
class MediaItemUpdate {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the update mutation for MediaItems
*
* @param \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'UpdateMediaItem';
self::$mutation['mediaItem'] = Relay::mutationWithClientMutationId([
'name' => esc_html( $mutation_name ),
'description' => __( 'Updates mediaItem objects', 'wp-graphql' ),
'inputFields' => self::input_fields( $post_type_object ),
'outputFields' => [
'mediaItem' => [
'type' => Types::post_object( $post_type_object->name ),
'resolve' => function( $payload ) {
return get_post( $payload['postObjectId'] );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
$id_parts = ! empty( $input['id'] ) ? Relay::fromGlobalId( $input['id'] ) : null;
$existing_media_item = get_post( absint( $id_parts['id'] ) );
/**
* If there's no existing mediaItem, throw an exception
*/
if ( null === $existing_media_item ) {
throw new UserError( __( 'No mediaItem with that ID could be found to update', 'wp-graphql' ) );
} else {
$author_id = $existing_media_item->post_author;
}
/**
* Stop now if the post isn't a mediaItem
*/
if ( $post_type_object->name !== $existing_media_item->post_type ) {
// translators: The placeholder is the ID of the mediaItem being edited
throw new UserError( sprintf( __( 'The id %1$d is not of the type mediaItem', 'wp-graphql' ), $id_parts['id'] ) );
}
/**
* Stop now if a user isn't allowed to edit mediaItems
*/
if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) {
throw new UserError( __( 'Sorry, you are not allowed to update mediaItems', 'wp-graphql' ) );
}
/**
* If the mutation is setting the author to be someone other than the user making the request
* make sure they have permission to edit others posts
**/
if ( ! empty( $input['authorId'] ) ) {
$author_id_parts = Relay::fromGlobalId( $input['authorId'] );
$author_id = $author_id_parts['id'];
}
/**
* Check to see if the existing_media_item author matches the current user,
* if not they need to be able to edit others posts to proceed
*/
if ( get_current_user_id() !== $author_id && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
throw new UserError( __( 'Sorry, you are not allowed to update mediaItems as this user.', 'wp-graphql' ) );
}
/**
* insert the post object and get the ID
*/
$post_args = MediaItemMutation::prepare_media_item( $input, $post_type_object, $mutation_name, false );
$post_args['ID'] = absint( $id_parts['id'] );
$post_args['post_author'] = $author_id;
/**
* Insert the post and retrieve the ID
*
* This will not fail as long as we have an ID in $post_args
* Thanks to the validation above we will always have the ID
*/
$post_id = wp_update_post( wp_slash( (array) $post_args ), true );
/**
* This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
*
* The input for the postObjectMutation will be passed, along with the $new_post_id for the
* postObject that was updated so that relations can be set, meta can be updated, etc.
*/
MediaItemMutation::update_additional_media_item_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info );
/**
* Return the payload
*/
return [
'postObjectId' => $post_id,
];
},
]);
return ! empty( self::$mutation[ $post_type_object->graphql_single_name ] ) ? self::$mutation[ $post_type_object->graphql_single_name ] : null;
}
/**
* Add the id as a nonNull field for update mutations
*
* @param \WP_Post_Type $post_type_object
*
* @return array
*/
private static function input_fields( $post_type_object ) {
/**
* Update mutations require an ID to be passed
*/
return array_merge(
[
'id' => [
'type' => Types::non_null( Types::id() ),
// translators: the placeholder is the name of the type of post object being updated
'description' => sprintf( __( 'The ID of the %1$s object', 'wp-graphql' ), $post_type_object->graphql_single_name ),
],
],
MediaItemMutation::input_fields( $post_type_object )
);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WPGraphQL\Type\Plugin\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class CommentConnectionDefinition
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class PluginConnectionDefinition {
/**
* @var array connection
* @since 0.0.5
*/
private static $connection = [];
/**
* connection
* This sets up a connection of plugins
* @return mixed
* @since 0.0.5
*/
public static function connection( $from_type = 'Root' ) {
if ( empty( self::$connection[ $from_type ] ) ) {
/**
* Setup the connectionDefinition
*
* @since 0.0.5
*/
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::plugin(),
'name' => 'Plugins',
'connectionFields' => function() {
return [
'nodes' => [
'type' => Types::list_of( Types::plugin() ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the connection to the post_objects_connection object
*
* @since 0.0.5
*/
self::$connection[ $from_type ] = [
'type' => $connection['connectionType'],
'description' => __( 'A collection of plugins', 'wp-graphql' ),
'args' => Relay::connectionArgs(),
'resolve' => function( $source, $args, AppContext $context, ResolveInfo $info ) {
return DataSource::resolve_plugins_connection( $source, $args, $context, $info );
},
];
}
return ! empty( self::$connection[ $from_type ] ) ? self::$connection[ $from_type ] : null;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace WPGraphQL\Type\Plugin\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
/**
* Class PluginConnectionResolver - Connects plugins to other objects
*
* @package WPGraphQL\Data\Resolvers
* @since 0.0.5
*/
class PluginConnectionResolver {
/**
* Creates the connection for plugins
*
* @param mixed $source The query results
* @param array $args The query arguments
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.5.0
* @return array
* @access public
*/
public static function resolve( $source, array $args, AppContext $context, ResolveInfo $info ) {
// File has not loaded.
require_once ABSPATH . 'wp-admin/includes/plugin.php';
// This is missing must use and drop in plugins.
$plugins = apply_filters( 'all_plugins', get_plugins() );
$plugins_array = [];
if ( ! empty( $plugins ) && is_array( $plugins ) ) {
foreach ( $plugins as $plugin ) {
$plugins_array[] = $plugin;
}
}
$connection = Relay::connectionFromArray( $plugins_array, $args );
$nodes = [];
if ( ! empty( $connection['edges'] ) && is_array( $connection['edges'] ) ) {
foreach ( $connection['edges'] as $edge ) {
$nodes[] = ! empty( $edge['node'] ) ? $edge['node'] : null;
}
}
$connection['nodes'] = ! empty( $nodes ) ? $nodes : null;
return ! empty( $plugins_array ) ? $connection : null;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace WPGraphQL\Type\Plugin;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class PluginQuery
* @package WPGraphQL\Type\Plugin
* @Since 0.0.5
*/
class PluginQuery {
/**
* Holds the root_query field definition
* @var array $root_query
* @since 0.0.5
*/
private static $root_query;
/**
* Method that returns the root query field definition for the plugin type
*
* @return array
* @since 0.0.5
*/
public static function root_query() {
if ( null === self::$root_query ) {
self::$root_query = [
'type' => Types::plugin(),
'description' => __( 'A WordPress plugin', 'wp-graphql' ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_plugin( $id_components['id'] );
},
];
}
return self::$root_query;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace WPGraphQL\Type\Plugin;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class PluginType
*
* This sets up the PluginType schema.
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class PluginType extends WPObjectType {
/**
* Holds the type name
* @var string $type_name
*/
private static $type_name;
/**
* Holds the $fields definition for the PluginType
* @var $fields
*/
private static $fields;
/**
* PluginType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
* @since 0.0.5
*/
self::$type_name = 'Plugin';
$config = [
'name' => self::$type_name,
'description' => __( 'An plugin object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* This defines the fields for the PluginType. The fields are passed through a filter so the shape of the schema
* can be modified, for example to add entry points to Types that are unique to certain plugins.
*
* @return mixed
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $plugin ) && ! empty( $plugin['Name'] ) ) ? Relay::toGlobalId( 'plugin', $plugin['Name'] ) : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'Display name of the plugin.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['Name'] ) ? $plugin['Name'] : '';
},
],
'pluginUri' => [
'type' => Types::string(),
'description' => __( 'URI for the plugin website. This is useful for directing users for support requests etc.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['PluginURI'] ) ? $plugin['PluginURI'] : '';
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the plugin.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['Description'] ) ? $plugin['Description'] : '';
},
],
'author' => [
'type' => Types::string(),
'description' => __( 'Name of the plugin author(s), may also be a company name.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['Author'] ) ? $plugin['Author'] : '';
},
],
'authorUri' => [
'type' => Types::string(),
'description' => __( 'URI for the related author(s)/company website.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['AuthorURI'] ) ? $plugin['AuthorURI'] : '';
},
],
'version' => [
'type' => Types::string(),
'description' => __( 'Current version of the plugin.', 'wp-graphql' ),
'resolve' => function( array $plugin, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $plugin['Version'] ) ? $plugin['Version'] : '';
},
],
];
/**
* Pass the fields through a filter to allow for hooking in and adjusting the shape
* of the type's schema
*
* @since 0.0.5
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,368 @@
<?php
namespace WPGraphQL\Type\PostObject\Connection;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectConnectionArgs
*
* This sets up the Query Args for post_object connections, which uses WP_Query, so this defines the allowed
* input fields that will be passed to the WP_Query
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class PostObjectConnectionArgs extends WPInputObjectType {
/**
* Stores the date query object
*
* @var PostObjectConnectionArgsDateQuery obj $date_query
* @since 0.5.0
* @access private
*/
private static $date_query;
/**
* This holds the field definitions
*
* @var array $fields
* @since 0.0.5
*/
public static $fields = [];
/**
* This holds the orderby_field input object type
*
* @var array $orderby_field
*/
private static $orderby_field;
/**
* This holds the orderby EnumType definition
*
* @var EnumType
*/
private static $orderby_enum;
/**
* PostObjectConnectionArgs constructor.
* @param array $config Array of Config data for the Input Type
* @param string $connection The name of the connection the args belong to
* @since 0.0.5
*/
public function __construct( $config = [], $connection ) {
$config['name'] = ucfirst( $connection ) . 'QueryArgs';
$config['queryClass'] = 'WP_Query';
$config['fields'] = self::fields( $connection );
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the PostObjectConnectionArgs
*
* @param string $connection The name of the connection the fields belong to
* @return array
* @since 0.0.5
*/
private static function fields( $connection ) {
if ( empty( self::$fields[ $connection ] ) ) {
$fields = [
/**
* Author $args
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Author_Parameters
* @since 0.0.5
*/
'author' => [
'type' => Types::int(),
'description' => __( 'The user that\'s connected as the author of the object. Use the
userId for the author object.', 'wp-graphql' ),
],
'authorName' => [
'type' => Types::string(),
'description' => __( 'Find objects connected to the author by the author\'s nicename', 'wp-graphql' ),
],
'authorIn' => [
'type' => Types::list_of( Types::id() ),
'description' => __( 'Find objects connected to author(s) in the array of author\'s userIds', 'wp-graphql' ),
],
'authorNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Find objects NOT connected to author(s) in the array of author\'s
userIds', 'wp-graphql' ),
],
/**
* Category $args
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Category_Parameters
* @since 0.0.5
*/
'categoryId' => [
'type' => Types::int(),
'description' => __( 'Category ID', 'wp-graphql' ),
],
'categoryName' => [
'type' => Types::string(),
'description' => __( 'Use Category Slug', 'wp-graphql' ),
],
'categoryIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of category IDs, used to display objects from one
category OR another', 'wp-graphql' ),
],
/**
* Tag $args
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Tag_Parameters
* @since 0.0.5
*/
'tag' => [
'type' => Types::string(),
'description' => __( 'Tag Slug', 'wp-graphql' ),
],
'tagId' => [
'type' => Types::string(),
'description' => __( 'Use Tag ID', 'wp-graphql' ),
],
'tagIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of tag IDs, used to display objects from one tag OR
another', 'wp-graphql' ),
],
'tagSlugAnd' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Array of tag slugs, used to display objects from one tag OR
another', 'wp-graphql' ),
],
'tagSlugIn' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Array of tag slugs, used to exclude objects in specified
tags', 'wp-graphql' ),
],
/**
* Search Parameter
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Search_Parameter
* @since 0.0.5
*/
'search' => [
'name' => 'search',
'type' => Types::string(),
'description' => __( 'Show Posts based on a keyword search', 'wp-graphql' ),
],
/**
* Post & Page Parameters
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Post_.26_Page_Parameters
* @since 0.0.5
*/
'id' => [
'type' => Types::int(),
'description' => __( 'Specific ID of the object', 'wp-graphql' ),
],
'name' => [
'type' => Types::string(),
'description' => __( 'Slug / post_name of the object', 'wp-graphql' ),
],
'title' => [
'type' => Types::string(),
'description' => __( 'Title of the object', 'wp-graphql' ),
],
'parent' => [
'type' => Types::string(),
'description' => __( 'Use ID to return only children. Use 0 to return only top-level
items', 'wp-graphql' ),
],
'parentIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Specify objects whose parent is in an array', 'wp-graphql' ),
],
'parentNotIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Specify posts whose parent is not in an array', 'wp-graphql' ),
],
'in' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of IDs for the objects to retrieve', 'wp-graphql' ),
],
'notIn' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Specify IDs NOT to retrieve. If this is used in the same query as "in",
it will be ignored', 'wp-graphql' ),
],
'nameIn' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Specify objects to retrieve. Use slugs', 'wp-graphql' ),
],
/**
* Password parameters
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Password_Parameters
* @since 0.0.2
*/
'hasPassword' => [
'type' => Types::boolean(),
'description' => __( 'True for objects with passwords; False for objects without passwords;
null for all objects with or without passwords', 'wp-graphql' ),
],
'password' => [
'type' => Types::string(),
'description' => __( 'Show posts with a specific password.', 'wp-graphql' ),
],
/**
* post_type
* NOTE: post_type is intentionally not supported as it's the post_type is the entity entry
* point for the queries
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Type_Parameters
* @since 0.0.2
*/
/**
* Status parameters
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Status_Parameters
* @since 0.0.2
*/
'status' => [
'type' => Types::post_status_enum(),
],
/**
* List of post status parameters
*/
'stati' => [
'type' => Types::list_of( Types::post_status_enum() ),
],
/**
* Order & Orderby parameters
*
* @see : https://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters
* @since 0.0.2
*/
'orderby' => [
'type' => Types::list_of( self::orderby_field() ),
'description' => __( 'What paramater to use to order the objects by.', 'wp-graphql' ),
],
'dateQuery' => self::date_query(),
'mimeType' => [
'type' => Types::mime_type_enum(),
'description' => __( 'Get objects with a specific mimeType property', 'wp-graphql' ),
],
];
self::$fields[ $connection ] = self::prepare_fields( $fields, ucfirst( $connection ) . 'QueryArgs' );
}
return ! empty( self::$fields[ $connection ] ) ? self::$fields[ $connection ] : null;
}
/**
* This returns the definition for the PostObjectConnectionArgsDateQuery
*
* @return PostObjectConnectionArgsDateQuery object
* @since 0.0.5
* @access public
*/
public static function date_query() {
return self::$date_query ? : ( self::$date_query = new PostObjectConnectionArgsDateQuery() );
}
/**
* This returns the orderby field which accepts a field (enum) and an order (enum, ASC/DESC)
*
* @return InputObjectType object
* @access private
*/
private static function orderby_field() {
if ( null === self::$orderby_field ) {
self::$orderby_field = new WPInputObjectType( [
'name' => 'OrderByOptions',
'fields' => self::prepare_fields( [
'field' => Types::non_null( self::orderby_enum() ),
'order' => new WPEnumType( [
'name' => 'Order',
'values' => [
'ASC' => [ 'value' => 'ASC' ],
'DESC' => [ 'value' => 'DESC' ],
],
] ),
], 'OrderByOptions' ),
] );
}
return ! empty( self::$orderby_field ) ? self::$orderby_field : null;
}
/**
* orderby_enum
* This returns the orderby enum type for the PostObjectQueryArgs
*
* @return EnumType
* @since 0.0.5
*/
private static function orderby_enum() {
if ( null === self::$orderby_enum ) {
self::$orderby_enum = new WPEnumType( [
'name' => 'OrderBy',
'values' => [
'AUTHOR' => [
'value' => 'post_author',
'description' => __( 'Order by author', 'wp-graphql' ),
],
'TITLE' => [
'value' => 'post_title',
'description' => __( 'Order by title', 'wp-graphql' ),
],
'SLUG' => [
'value' => 'post_name',
'description' => __( 'Order by slug', 'wp-graphql' ),
],
'MODIFIED' => [
'value' => 'post_modified',
'description' => __( 'Order by last modified date', 'wp-graphql' ),
],
'DATE' => [
'value' => 'post_date',
'description' => __( 'Order by publish date', 'wp-graphql' ),
],
'PARENT' => [
'value' => 'post_parent',
'description' => __( 'Order by parent ID', 'wp-graphql' ),
],
'IN' => [
'value' => 'post__in',
'description' => __( 'Preserve the ID order given in the IN array', 'wp-graphql' ),
],
'NAME_IN' => [
'value' => 'post_name__in',
'description' => __( 'Preserve slug order given in the NAME_IN array', 'wp-graphql' ),
],
'MENU_ORDER' => [
'value' => 'menu_order',
'description' => __( 'Order by the menu order value', 'wp-graphql' ),
],
],
] );
}
return self::$orderby_enum;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace WPGraphQL\Type\PostObject\Connection;
use GraphQL\Type\Definition\EnumType;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectConnectionArgsDateQuery
*
* This defines the input fields for date queries
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class PostObjectConnectionArgsDateQuery extends WPInputObjectType {
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
public static $fields;
/**
* Holds the date_after input object definition
* @var WPInputObjectType
* @since 0.0.5
*/
private static $date_after;
/**
* Holds the date_before input object definition
* @var WPInputObjectType
* @since 0.0.5
*/
private static $date_before;
/**
* Holds the column_enum EnumType definition
* @var EnumType
* @since 0.0.5
*/
private static $column_enum;
/**
* DateQueryType constructor.
* @since 0.0.5
*/
public function __construct( $config = [] ) {
$config['name'] = 'DateQuery';
$config['fields'] = self::fields();
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the DateQueryType
*
* @return array|null
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = [
'year' => [
'type' => Types::int(),
'description' => __( '4 digit year (e.g. 2017)', 'wp-graphql' ),
],
'month' => [
'type' => Types::int(),
'description' => __( 'Month number (from 1 to 12)', 'wp-graphql' ),
],
'week' => [
'type' => Types::int(),
'description' => __( 'Week of the year (from 0 to 53)', 'wp-graphql' ),
],
'day' => [
'type' => Types::int(),
'description' => __( 'Day of the month (from 1 to 31)', 'wp-graphql' ),
],
'hour' => [
'type' => Types::int(),
'description' => __( 'Hour (from 0 to 23)', 'wp-graphql' ),
],
'minute' => [
'type' => Types::int(),
'description' => __( 'Minute (from 0 to 59)', 'wp-graphql' ),
],
'second' => [
'type' => Types::int(),
'description' => __( 'Second (0 to 59)', 'wp-graphql' ),
],
'after' => [
'type' => self::date_after(),
],
'before' => [
'type' => self::date_before(),
],
'inclusive' => [
'type' => Types::boolean(),
'description' => __( 'For after/before, whether exact value should be matched or not', 'wp-graphql' ),
],
'compare' => [
'type' => Types::string(),
'description' => __( 'For after/before, whether exact value should be matched or not', 'wp-graphql' ),
],
'column' => [
'type' => self::column_enum(),
'description' => __( 'Column to query against', 'wp-graphql' ),
],
'relation' => [
'type' => Types::relation_enum(),
'description' => __( 'OR or AND, how the sub-arrays should be compared', 'wp-graphql' ),
],
];
}
return self::prepare_fields( self::$fields, 'DateQuery' );
}
/**
* column_enum
* Creates an Enum type with the columns that can be queried against for the DateQuery
* @return EnumType|null
* @since 0.0.5
*/
private static function column_enum() {
if ( null === self::$column_enum ) {
self::$column_enum = new WPEnumType( [
'name' => 'DateColumn',
'values' => [
'DATE' => [
'value' => 'post_date',
],
'MODIFIED' => [
'value' => 'post_modified',
],
],
] );
}
return self::$column_enum;
}
/**
* date_after
* Creates the date_after input field that allows "after" paramaters
* to be entered
* @return WPInputObjectType|null
* @since 0.0.5
*/
private static function date_after() {
if ( null === self::$date_after ) {
self::$date_after = new WPInputObjectType( [
'name' => 'DateAfter',
'fields' => self::prepare_fields( [
'year' => [
'type' => Types::int(),
'description' => __( '4 digit year (e.g. 2017)', 'wp-graphql' ),
],
'month' => [
'type' => Types::int(),
'description' => __( 'Month number (from 1 to 12)', 'wp-graphql' ),
],
'day' => [
'type' => Types::int(),
'description' => __( 'Day of the month (from 1 to 31)', 'wp-graphql' ),
],
], 'DateAfter' ),
] );
}
return self::$date_after;
}
/**
* date_before
* Creates the date_before input field that allows "before" paramaters
* to be entered
* @return WPInputObjectType|null
* @since 0.0.5
*/
private static function date_before() {
if ( null === self::$date_before ) {
self::$date_before = new WPInputObjectType( [
'name' => 'DateBefore',
'fields' => self::prepare_fields( [
'year' => [
'type' => Types::int(),
'description' => __( '4 digit year (e.g. 2017)', 'wp-graphql' ),
],
'month' => [
'type' => Types::int(),
'description' => __( 'Month number (from 1 to 12)', 'wp-graphql' ),
],
'day' => [
'type' => Types::int(),
'description' => __( 'Day of the month (from 1 to 31)', 'wp-graphql' ),
],
], 'DateBefore' ),
] );
}
return self::$date_before;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace WPGraphQL\Type\PostObject\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class PostObjectConnectionDefinition
*
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class PostObjectConnectionDefinition {
/**
* Stores some date for the Relay connection for post objects
*
* @var array $connection
* @since 0.0.5
* @access private
*/
private static $connection = [];
/**
* Stores the added_args for the connection (in addition to the standard Relay args)
*
* @var array $added_args
*/
protected static $added_args = [];
/**
* Method that sets up the relay connection for post objects
*
* @param object $post_type_object The post type object for the connection is registered for
* @param string $from_type The name of the type the connection is coming from
*
* @return mixed
* @since 0.0.5
*
* @return mixed
*/
public static function connection( $post_type_object, $from_type = 'Root' ) {
if ( empty( self::$connection[ $from_type ][ $post_type_object->name ] ) ) {
/**
* Setup the connectionDefinition
*
* @since 0.0.5
*/
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::post_object( $post_type_object->name ),
'name' => ucfirst( $from_type ) . ucfirst( $post_type_object->graphql_plural_name ),
'connectionFields' => function() use ( $post_type_object ) {
return [
'postTypeInfo' => [
'type' => Types::post_type(),
'description' => __( 'Information about the type of content being queried', 'wp-graphql' ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {
return $post_type_object;
},
],
'nodes' => [
'type' => Types::list_of( Types::post_object( $post_type_object->name ) ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the connection to the post_objects_connection object
*
* @since 0.0.5
*/
$connection_name = ucfirst( $from_type ) . ucfirst( $post_type_object->graphql_plural_name );
self::$connection[ $from_type ][ $post_type_object->name ] = [
'type' => $connection['connectionType'],
// Translators: the placeholder is the name of the post_type
'description' => sprintf( __( 'A collection of %s objects', 'wp-graphql' ), $post_type_object->graphql_plural_name ),
'args' => array_merge( Relay::connectionArgs(), self::added_args( $connection_name ) ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {
return DataSource::resolve_post_objects_connection( $source, $args, $context, $info, $post_type_object->name );
},
];
}
/**
* Return the connection from the post_objects_connection object
*
* @since 0.0.5
*/
return self::$connection[ $from_type ][ $post_type_object->name ];
}
/**
* Returns the $args that should be added to the connection args
*
* @param string $connection The name of the connection the args belong to
*
* @return array
*/
protected static function added_args( $connection ) {
if ( empty( self::$added_args[ $connection ] ) ) {
self::$added_args[ $connection ] = [
'where' => [
'name' => 'where',
'description' => __( '', 'wp-graphql' ),
'type' => Types::post_object_query_args( $connection ),
],
];
}
return ! empty( self::$added_args[ $connection ] ) ? self::$added_args[ $connection ] : null;
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace WPGraphQL\Type\PostObject\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Connection\ArrayConnection;
use WPGraphQL\AppContext;
use WPGraphQL\Data\ConnectionResolver;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class PostObjectConnection - connects posts to other types
*
* @package WPGraphQL\Data\Resolvers
* @since 0.0.5
*/
class PostObjectConnectionResolver extends ConnectionResolver {
/**
* Stores the name of the $post_type being resolved
*
* @var $post_type
*/
public static $post_type;
/**
* Holds the maximum number of items that can be queried per request
*
* @var int $max_query_amount
*/
public static $max_query_amount = 100;
/**
* PostObjectConnectionResolver constructor.
*
* @param $post_type
*/
public function __construct( $post_type ) {
self::$post_type = $post_type;
}
/**
* This returns the $query_args that should be used when querying for posts in the postObjectConnectionResolver.
* This checks what input $args are part of the query, combines them with various filters, etc and returns an
* array of $query_args to be used in the \WP_Query call
*
* @param mixed $source The query source being passed down to the resolver
* @param array $args The arguments that were provided to the query
* @param AppContext $context Object containing app context that gets passed down the resolve tree
* @param ResolveInfo $info Info about fields passed down the resolve tree
*
* @return array
*/
public static function get_query_args( $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Prepare for later use
*/
$last = ! empty( $args['last'] ) ? $args['last'] : null;
$first = ! empty( $args['first'] ) ? $args['first'] : null;
/**
* Set the post_type for the query based on the type of post being queried
*/
$query_args['post_type'] = ! empty( self::$post_type ) ? self::$post_type : 'post';
/**
* Don't calculate the total rows, it's not needed and can be expensive
*/
$query_args['no_found_rows'] = true;
/**
* Set the post_status to "publish" by default
*/
$query_args['post_status'] = 'publish';
/**
* Set posts_per_page the highest value of $first and $last, with a (filterable) max of 100
*/
$query_args['posts_per_page'] = min( max( absint( $first ), absint( $last ), 10 ), self::get_query_amount( $source, $args, $context, $info ) ) + 1;
/**
* Set the default to only query posts with no post_parent set
*/
$query_args['post_parent'] = 0;
/**
* Set the graphql_cursor_offset which is used by Config::graphql_wp_query_cursor_pagination_support
* to filter the WP_Query to support cursor pagination
*/
$query_args['graphql_cursor_offset'] = self::get_offset( $args );
$query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<';
/**
* Pass the graphql $args to the WP_Query
*/
$query_args['graphql_args'] = $args;
/**
* Collect the input_fields and sanitize them to prepare them for sending to the WP_Query
*/
$input_fields = [];
if ( ! empty( $args['where'] ) ) {
$input_fields = self::sanitize_input_fields( $args['where'], $source, $args, $context, $info );
}
/**
* If the post_type is "attachment" set the default "post_status" $query_arg to "inherit"
*/
if ( 'attachment' === self::$post_type ) {
$query_args['post_status'] = 'inherit';
/**
* Unset the "post_parent" for attachments, as we don't really care if they
* have a post_parent set by default
*/
unset( $query_args['post_parent'] );
}
/**
* Determine where we're at in the Graph and adjust the query context appropriately.
*
* For example, if we're querying for posts as a field of termObject query, this will automatically
* set the query to pull posts that belong to that term.
*/
if ( true === is_object( $source ) ) {
switch ( true ) {
case $source instanceof \WP_Post:
$query_args['post_parent'] = $source->ID;
break;
case $source instanceof \WP_Post_Type:
$query_args['post_type'] = $source->name;
break;
case $source instanceof \WP_Term:
$query_args['tax_query'] = [
[
'taxonomy' => $source->taxonomy,
'terms' => [ $source->term_id ],
'field' => 'term_id',
],
];
break;
case $source instanceof \WP_User:
$query_args['author'] = $source->ID;
break;
}
}
/**
* Merge the input_fields with the default query_args
*/
if ( ! empty( $input_fields ) ) {
$query_args = array_merge( $query_args, $input_fields );
}
/**
* Map the orderby inputArgs to the WP_Query
*/
if ( ! empty( $args['where']['orderby'] ) && is_array( $args['where']['orderby'] ) ) {
$query_args['orderby'] = [];
foreach ( $args['where']['orderby'] as $orderby_input ) {
/**
* These orderby options should not include the order parameter.
*/
if ( in_array( $orderby_input['field'], [ 'post__in', 'post_name__in', 'post_parent__in' ], true ) ) {
$query_args['orderby'] = esc_sql( $orderby_input['field'] );
} else if ( ! empty( $orderby_input['field'] ) ) {
$query_args['orderby'] = [
esc_sql( $orderby_input['field'] ) => esc_sql( $orderby_input['order'] ),
];
}
}
}
/**
* If there's no orderby params in the inputArgs, set order based on the first/last argument
*/
if ( empty( $query_args['orderby'] ) ) {
$query_args['order'] = ! empty( $last ) ? 'ASC' : 'DESC';
}
/**
* Filter the $query args to allow folks to customize queries programmatically
*
* @param array $query_args The args that will be passed to the WP_Query
* @param mixed $source The source that's passed down the GraphQL queries
* @param array $args The inputArgs on the field
* @param AppContext $context The AppContext passed down the GraphQL tree
* @param ResolveInfo $info The ResolveInfo passed down the GraphQL tree
*/
$query_args = apply_filters( 'graphql_post_object_connection_query_args', $query_args, $source, $args, $context, $info );
return $query_args;
}
/**
* This runs the query and returns the response
*
* @param $query_args
*
* @return \WP_Query
*/
public static function get_query( $query_args ) {
$query = new \WP_Query( $query_args );
return $query;
}
/**
* This takes an array of items, the $args and the $query and returns the connection including
* the edges and page info
*
* @param mixed $query The Query that was processed to get the connection data
* @param array $items The array of items being connected
* @param array $args The $args that were passed to the query
* @param mixed $source The source being passed down the resolve tree
* @param AppContext $context The AppContext being passed down the resolve tree
* @param ResolveInfo $info the ResolveInfo passed down the resolve tree
*
* @return array
*/
public static function get_connection( $query, array $items, $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Get the $posts from the query
*/
$items = ! empty( $items ) && is_array( $items ) ? $items : [];
$info = self::get_query_info( $query );
/**
* Set whether there is or is not another page
*/
$has_previous_page = ( ! empty( $args['last'] ) && ( $info['total_items'] >= self::get_query_amount( $source, $args, $context, $info ) ) ) ? true : false;
$has_next_page = ( ! empty( $args['first'] ) && ( $info['total_items'] >= self::get_query_amount( $source, $args, $context, $info ) ) ) ? true : false;
/**
* Slice the array to the amount of items that were requested
*/
$items = array_slice( $items, 0, self::get_query_amount( $source, $args, $query, $info ) );
/**
* Get the edges from the $items
*/
$edges = self::get_edges( $items, $source, $args, $context, $info );
/**
* Find the first_edge and last_edge
*/
$first_edge = $edges ? $edges[0] : null;
$last_edge = $edges ? $edges[ count( $edges ) - 1 ] : null;
$edges_to_return = $edges;
/**
* Create the connection to return
*/
$connection = [
'edges' => $edges_to_return,
'pageInfo' => [
'hasPreviousPage' => $has_previous_page,
'hasNextPage' => $has_next_page,
'startCursor' => ! empty( $first_edge['cursor'] ) ? $first_edge['cursor'] : null,
'endCursor' => ! empty( $last_edge['cursor'] ) ? $last_edge['cursor'] : null,
],
'nodes' => $items,
];
return $connection;
}
/**
* Takes an array of items and returns the edges
*
* @param $items
*
* @return array
*/
public static function get_edges( $items, $source, $args, $context, $info ) {
$edges = [];
/**
* If we're doing backward pagination we want to reverse the array before
* returning it to the edges
*/
if ( ! empty( $args['last'] ) ) {
$items = array_reverse( $items );
}
if ( ! empty( $items ) && is_array( $items ) ) {
foreach ( $items as $item ) {
$edges[] = [
'cursor' => ArrayConnection::offsetToCursor( $item->ID ),
'node' => DataSource::resolve_post_object( $item->ID, $item->post_type ),
];
}
}
return $edges;
}
/**
* This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_Query
* friendly keys. There's probably a cleaner/more dynamic way to approach this, but
* this was quick. I'd be down to explore more dynamic ways to map this, but for
* now this gets the job done.
*
* @param array $args Query "where" args
* @param mixed $source The query results for a query calling this
* @param array $all_args All of the arguments for the query (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @access public
* @return array
*/
public static function sanitize_input_fields( array $args, $source, array $all_args, AppContext $context, ResolveInfo $info ) {
$arg_mapping = [
'authorName' => 'author_name',
'authorIn' => 'author__in',
'authorNotIn' => 'author__not_in',
'categoryId' => 'cat',
'categoryName' => 'category_name',
'categoryIn' => 'category__in',
'tagId' => 'tag_id',
'tagIds' => 'tag__and',
'tagSlugAnd' => 'tag_slug__and',
'tagSlugIn' => 'tag_slug__in',
'search' => 's',
'id' => 'p',
'parent' => 'post_parent',
'parentIn' => 'post_parent__in',
'parentNotIn' => 'post_parent__not_in',
'in' => 'post__in',
'notIn' => 'post__not_in',
'nameIn' => 'post_name__in',
'hasPassword' => 'has_password',
'password' => 'post_password',
'status' => 'post_status',
'stati' => 'post_status',
'dateQuery' => 'date_query',
];
/**
* Map and sanitize the input args to the WP_Query compatible args
*/
$query_args = Types::map_input( $args, $arg_mapping );
/**
* Filter the input fields
* This allows plugins/themes to hook in and alter what $args should be allowed to be passed
* from a GraphQL Query to the WP_Query
*
* @param array $query_args The mapped query arguments
* @param array $args Query "where" args
* @param string $post_type The post type for the query
* @param mixed $source The query results for a query calling this
* @param array $all_args All of the arguments for the query (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @return array
*/
$query_args = apply_filters( 'graphql_map_input_fields_to_wp_query', $query_args, $args, $source, $all_args, $context, $info );
/**
* Return the Query Args
*/
return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace WPGraphQL\Type\PostObject\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectCreate
*
* @package WPGraphQL\Type\PostObject\Mutation
*/
class PostObjectCreate {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the create mutation for PostTypeObjects
*
* @param \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
if (
! empty( $post_type_object->graphql_single_name ) &&
empty( self::$mutation[ $post_type_object->graphql_single_name ] )
) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'Create' . ucwords( $post_type_object->graphql_single_name );
self::$mutation[ $post_type_object->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => $mutation_name,
// translators: The placeholder is the name of the object type
'description' => sprintf( __( 'Create %1$s objects', 'wp-graphql' ), $post_type_object->graphql_single_name ),
'inputFields' => WPInputObjectType::prepare_fields( PostObjectMutation::input_fields( $post_type_object ), $mutation_name ),
'outputFields' => [
$post_type_object->graphql_single_name => [
'type' => Types::post_object( $post_type_object->name ),
'resolve' => function( $payload ) {
return get_post( $payload['id'] );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
/**
* Throw an exception if there's no input
*/
if ( ( empty( $post_type_object->name ) ) || ( empty( $input ) || ! is_array( $input ) ) ) {
throw new UserError( __( 'Mutation not processed. There was no input for the mutation or the post_type_object was invalid', 'wp-graphql' ) );
}
/**
* Stop now if a user isn't allowed to create a post
*/
if ( ! current_user_can( $post_type_object->cap->create_posts ) ) {
// translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to create %1$s', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
}
/**
* If the post being created is being assigned to another user that's not the current user, make sure
* the current user has permission to edit others posts for this post_type
*/
if ( ! empty( $input['authorId'] ) && get_current_user_id() !== $input['authorId'] && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
// translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to create %1$s as this user', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
}
/**
* @todo: When we support assigning terms and setting posts as "sticky" we need to check permissions
* @see :https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L504-L506
* @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L496-L498
*/
/**
* insert the post object and get the ID
*/
$post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );
/**
* Filter the default post status to use when the post is initially created. Pass through a filter to
* allow other plugins to override the default (for example, Edit Flow, which provides control over
* customizing stati or various E-commerce plugins that make heavy use of custom stati)
*
* @param string $default_status The default status to be used when the post is initially inserted
* @param \WP_Post_Type $post_type_object The Post Type that is being inserted
* @param string $mutation_name The name of the mutation currently in progress
*/
$default_post_status = apply_filters( 'graphql_post_object_create_default_post_status', 'draft', $post_type_object, $mutation_name );
/**
* We want to cache the "post_status" and set the status later. We will set the initial status
* of the inserted post as the default status for the site, allow side effects to process with the
* inserted post (set term object connections, set meta input, sideload images if necessary, etc)
* Then will follow up with setting the status as what it was declared to be later
*/
$intended_post_status = ! empty( $post_args['post_status'] ) ? $post_args['post_status'] : $default_post_status;
/**
* Set the post_status as the default for the initial insert. The intended $post_status will be set after
* side effects are complete.
*/
$post_args['post_status'] = $default_post_status;
/**
* Insert the post and retrieve the ID
*/
$post_id = wp_insert_post( wp_slash( (array) $post_args ), true );
/**
* Throw an exception if the post failed to create
*/
if ( is_wp_error( $post_id ) ) {
$error_message = $post_id->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
throw new UserError( __( 'The object failed to create but no error was provided', 'wp-graphql' ) );
}
}
/**
* If the $post_id is empty, we should throw an exception
*/
if ( empty( $post_id ) ) {
throw new UserError( __( 'The object failed to create', 'wp-graphql' ) );
}
/**
* This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
*
* The input for the postObjectMutation will be passed, along with the $new_post_id for the
* postObject that was created so that relations can be set, meta can be updated, etc.
*/
PostObjectMutation::update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );
/**
* Determine whether the intended status should be set or not.
*
* By filtering to false, the $intended_post_status will not be set at the completion of the mutation.
*
* This allows for side-effect actions to set the status later. For example, if a post
* was being created via a GraphQL Mutation, the post had additional required assets, such as images
* that needed to be sideloaded or some other semi-time-consuming side effect, those actions could
* be deferred (cron or whatever), and when those actions complete they could come back and set
* the $intended_status.
*
* @param boolean $should_set_intended_status Whether to set the intended post_status or not. Default true.
* @param \WP_Post_Type $post_type_object The Post Type Object for the post being mutated
* @param string $mutation_name The name of the mutation currently in progress
* @param AppContext $context The AppContext passed down to all resolvers
* @param ResolveInfo $info The ResolveInfo passed down to all resolvers
* @param string $intended_post_status The intended post_status the post should have according to the mutation input
* @param string $default_post_status The default status posts should use if an intended status wasn't set
*/
$should_set_intended_status = apply_filters( 'graphql_post_object_create_should_set_intended_post_status', true, $post_type_object, $mutation_name, $context, $info, $intended_post_status, $default_post_status );
/**
* If the intended post status and the default post status are not the same,
* update the post with the intended status now that side effects are complete.
*/
if ( $intended_post_status !== $default_post_status && true === $should_set_intended_status ) {
/**
* If the post was deleted by a side effect action before getting here,
* don't proceed.
*/
if ( ! $new_post = get_post( $post_id ) ) {
throw new UserError( sprintf( __( 'The status of the post could not be set', 'wp-graphql' ) ) );
}
/**
* If the $intended_post_status is different than the current status of the post
* proceed and update the status.
*/
if ( $intended_post_status !== $new_post->post_status ) {
$update_args = [
'ID' => $post_id,
'post_status' => $intended_post_status,
// Prevent the post_date from being reset if the date was included in the create post $args
// see: https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/post.php#L3637
'edit_date' => ! empty( $post_args['post_date'] ) ? $post_args['post_date'] : false,
];
wp_update_post( $update_args );
}
}
/**
* Return the post object
*/
return [
'id' => $post_id,
];
},
] );
}
return ! empty( self::$mutation[ $post_type_object->graphql_single_name ] ) ? self::$mutation[ $post_type_object->graphql_single_name ] : null;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WPGraphQL\Type\PostObject\Mutation;
use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
/**
* Class PostObjectDelete
*
* @package WPGraphQL\Type\PostObject\Mutation
*/
class PostObjectDelete {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the delete mutation for PostTypeObjects
*
* @param \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
if (
! empty( $post_type_object->graphql_single_name ) &&
empty( self::$mutation[ $post_type_object->graphql_single_name ] )
) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'Delete' . ucwords( $post_type_object->graphql_single_name );
self::$mutation[ $post_type_object->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
// translators: The placeholder is the name of the object type
'description' => sprintf( __( 'Delete %1$s objects. By default %1$s objects will be moved to the trash unless the forceDelete is used', 'wp-graphql' ), $post_type_object->graphql_single_name ),
'inputFields' => [
'id' => [
'type' => Types::non_null( Types::id() ),
// translators: The placeholder is the name of the post's post_type being deleted
'description' => sprintf( __( 'The ID of the %1$s to delete', 'wp-graphql' ), $post_type_object->graphql_single_name ),
],
'forceDelete' => [
'type' => Types::boolean(),
'description' => __( 'Whether the object should be force deleted instead of being moved to the trash', 'wp-graphql' ),
],
],
'outputFields' => [
'deletedId' => [
'type' => Types::id(),
'description' => __( 'The ID of the deleted object', 'wp-graphql' ),
'resolve' => function( $payload ) use ( $post_type_object ) {
$deleted = (object) $payload['postObject'];
return ! empty( $deleted->ID ) ? Relay::toGlobalId( $post_type_object->name, absint( $deleted->ID ) ) : null;
},
],
$post_type_object->graphql_single_name => [
'type' => Types::post_object( $post_type_object->name ),
'description' => __( 'The object before it was deleted', 'wp-graphql' ),
'resolve' => function( $payload ) {
$deleted = (object) $payload['postObject'];
return ! empty( $deleted ) ? $deleted : null;
},
],
],
'mutateAndGetPayload' => function( $input ) use ( $post_type_object, $mutation_name ) {
/**
* Get the ID from the global ID
*/
$id_parts = Relay::fromGlobalId( $input['id'] );
/**
* Stop now if a user isn't allowed to delete a post
*/
if ( ! current_user_can( $post_type_object->cap->delete_post, absint( $id_parts['id'] ) ) ) {
// translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to delete %1$s', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
}
/**
* Check if we should force delete or not
*/
$force_delete = ( ! empty( $input['forceDelete'] ) && true === $input['forceDelete'] ) ? true : false;
/**
* Get the post object before deleting it
*/
$post_before_delete = get_post( absint( $id_parts['id'] ) );
/**
* If the post is already in the trash, and the forceDelete input was not passed,
* don't remove from the trash
*/
if ( 'trash' === $post_before_delete->post_status ) {
if ( true !== $force_delete ) {
// Translators: the first placeholder is the post_type of the object being deleted and the second placeholder is the unique ID of that object
throw new UserError( sprintf( __( 'The %1$s with id %2$s is already in the trash. To remove from the trash, use the forceDelete input', 'wp-graphql' ), $post_type_object->graphql_single_name, $input['id'] ) );
}
}
/**
* Delete the post
*/
$deleted = wp_delete_post( $id_parts['id'], $force_delete );
/**
* If the post was moved to the trash, spoof the object's status before returning it
*/
$post_before_delete->post_status = ( false !== $deleted && true !== $force_delete ) ? 'trash' : $post_before_delete->post_status;
/**
* Return the deletedId and the object before it was deleted
*/
return [
'postObject' => $post_before_delete,
];
},
] );
}
return ! empty( self::$mutation[ $post_type_object->graphql_single_name ] ) ? self::$mutation[ $post_type_object->graphql_single_name ] : null;
}
}

View File

@@ -0,0 +1,640 @@
<?php
namespace WPGraphQL\Type\PostObject\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectMutation
*
* @package WPGraphQL\Type\PostObject
*/
class PostObjectMutation {
/**
* Holds the input_fields configuration
*
* @var array
*/
private static $input_fields = [];
/**
* @param $post_type_object
*
* @return mixed|array|null $input_fields
*/
public static function input_fields( $post_type_object ) {
if ( ! empty( $post_type_object->graphql_single_name ) && empty( self::$input_fields[ $post_type_object->graphql_single_name ] ) ) {
$input_fields = [
'authorId' => [
'type' => Types::id(),
'description' => __( 'The userId to assign as the author of the post', 'wp-graphql' ),
],
'commentCount' => [
'type' => Types::int(),
'description' => __( 'The number of comments. Even though WPGraphQL denotes this field as an integer, in WordPress this field should be saved as a numeric string for compatability.', 'wp-graphql' ),
],
'commentStatus' => [
'type' => Types::string(),
'description' => __( 'The comment status for the object', 'wp-graphql' ),
],
'content' => [
'type' => Types::string(),
'description' => __( 'The content of the object', 'wp-graphql' ),
],
'date' => [
'type' => Types::string(),
'description' => __( 'The date of the object. Preferable to enter as year/month/day (e.g. 01/31/2017) as it will rearrange date as fit if it is not specified. Incomplete dates may have unintended results for example, "2017" as the input will use current date with timestamp 20:17 ', 'wp-graphql' ),
],
'excerpt' => [
'type' => Types::string(),
'description' => __( 'The excerpt of the object', 'wp-graphql' ),
],
'menuOrder' => [
'type' => Types::int(),
'description' => __( 'A field used for ordering posts. This is typically used with nav menu items or for special ordering of hierarchical content types.', 'wp-graphql' ),
],
'mimeType' => [
'type' => Types::mime_type_enum(),
'description' => __( 'If the post is an attachment or a media file, this field will carry the corresponding MIME type. This field is equivalent to the value of WP_Post->post_mime_type and the post_mime_type column in the `post_objects` database table.', 'wp-graphql' ),
],
'parentId' => [
'type' => Types::id(),
'description' => __( 'The ID of the parent object', 'wp-graphql' ),
],
'password' => [
'type' => Types::string(),
'description' => __( 'The password used to protect the content of the object', 'wp-graphql' ),
],
'pinged' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'URLs that have been pinged.', 'wp-graphql' ),
],
'pingStatus' => [
'type' => Types::string(),
'description' => __( 'The ping status for the object', 'wp-graphql' ),
],
'slug' => [
'type' => Types::string(),
'description' => __( 'The slug of the object', 'wp-graphql' ),
],
'status' => [
'type' => Types::post_status_enum(),
'description' => __( 'The status of the object', 'wp-graphql' ),
],
'title' => [
'type' => Types::string(),
'description' => __( 'The title of the post', 'wp-graphql' ),
],
'toPing' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'URLs queued to be pinged.', 'wp-graphql' ),
],
];
/**
* Add inputs for connected taxonomies
*/
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
// If the taxonomy is in the array of taxonomies registered to the post_type
if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) {
$tax_object = get_taxonomy( $taxonomy );
$node_input = new WPInputObjectType( [
'name' => $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'Nodes',
'description' => sprintf( __( 'List of %1$s to connect the %2$s to. If an ID is set, it will be used to create the connection. If not, it will look for a slug. If neither are valid existing terms, and the site is configured to allow terms to be created during post mutations, a term will be created using the Name if it exists in the input, then fallback to the slug if it exists.', 'wp-graphql' ), $tax_object->graphql_plural_name, $post_type_object->graphql_single_name ),
'fields' => [
'id' => [
'type' => Types::id(),
'description' => sprintf( __( 'The ID of the %1$s. If present, this will be used to connect to the %2$s. If no existing %1$s exists with this ID, no connection will be made.', 'wp-graphql' ), $tax_object->graphql_single_name, $post_type_object->graphql_single_name ),
],
'slug' => [
'type' => Types::string(),
'description' => sprintf( __( 'The slug of the %1$s. If no ID is present, this field will be used to make a connection. If no existing term exists with this slug, this field will be used as a fallback to the Name field when creating a new term to connect to, if term creation is enabled as a nested mutation.', 'wp-graphql' ), $tax_object->graphql_single_name ),
],
'description' => [
'type' => Types::string(),
'description' => sprintf( __( 'The description of the %1$s. This field is used to set a description of the %1$s if a new one is created during the mutation.', 'wp-graphql' ), $tax_object->graphql_single_name ),
],
'name' => [
'type' => Types::string(),
'description' => sprintf( __( 'The name of the %1$s. This field is used to create a new term, if term creation is enabled in nested mutations, and if one does not already exist with the provided slug or ID or if a slug or ID is not provided. If no name is included and a term is created, the creation will fallback to the slug field.', 'wp-graphql' ), $tax_object->graphql_single_name ),
],
],
] );
$input_fields[ $tax_object->graphql_plural_name ] = [
'description' => sprintf( __( 'Set connections between the %1$s and %2$s', 'wp-graphql' ), $post_type_object->graphql_single_name, $tax_object->graphql_plural_name ),
'type' => new WPInputObjectType( [
'name' => ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ),
'description' => sprintf( __( 'Set relationships between the %1$s to %2$s', 'wp-graphql' ), $post_type_object->graphql_single_name, $tax_object->graphql_plural_name ),
'fields' => [
'append' => [
'type' => Types::boolean(),
'description' => sprintf( __( 'If true, this will append the %1$s to existing related %2$s. If false, this will replace existing relationships. Default true.', 'wp-graphql' ), $tax_object->graphql_single_name, $tax_object->graphql_plural_name ),
],
'nodes' => [
'type' => Types::list_of( $node_input ),
],
],
] ),
];
}
}
}
/**
* Filters the mutation input fields for the object type
*
* @param array $input_fields The array of input fields
* @param \WP_Post_Type $post_type_object The post_type object for the type of Post being mutated
*/
self::$input_fields[ $post_type_object->graphql_single_name ] = apply_filters( 'graphql_post_object_mutation_input_fields', $input_fields, $post_type_object );
} // End if().
return ! empty( self::$input_fields[ $post_type_object->graphql_single_name ] ) ? self::$input_fields[ $post_type_object->graphql_single_name ] : null;
}
/**
* This handles inserting the post object
*
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The post_type_object for the type of post being mutated
* @param string $mutation_name The name of the mutation being performed
*
* @return array $insert_post_args
* @throws \Exception
*/
public static function prepare_post_object( $input, $post_type_object, $mutation_name ) {
/**
* Set the post_type for the insert
*/
$insert_post_args['post_type'] = $post_type_object->name;
/**
* Prepare the data for inserting the post
* NOTE: These are organized in the same order as: https://developer.wordpress.org/reference/functions/wp_insert_post/
*/
$author_id_parts = ! empty( $input['authorId'] ) ? Relay::fromGlobalId( $input['authorId'] ) : null;
if ( is_array( $author_id_parts ) && ! empty( $author_id_parts['id'] ) && is_int( $author_id_parts['id'] ) ) {
$insert_post_args['post_author'] = absint( $author_id_parts['id'] );
}
if ( ! empty( $input['date'] ) && false !== strtotime( $input['date'] ) ) {
$insert_post_args['post_date'] = date( 'Y-m-d H:i:s', strtotime( $input['date'] ) );
}
if ( ! empty( $input['content'] ) ) {
$insert_post_args['post_content'] = $input['content'];
}
if ( ! empty( $input['title'] ) ) {
$insert_post_args['post_title'] = $input['title'];
}
if ( ! empty( $input['excerpt'] ) ) {
$insert_post_args['post_excerpt'] = $input['excerpt'];
}
if ( ! empty( $input['status'] ) ) {
$insert_post_args['post_status'] = $input['status'];
}
if ( ! empty( $input['commentStatus'] ) ) {
$insert_post_args['comment_status'] = $input['commentStatus'];
}
if ( ! empty( $input['pingStatus'] ) ) {
$insert_post_args['ping_status'] = $input['pingStatus'];
}
if ( ! empty( $input['password'] ) ) {
$insert_post_args['post_password'] = $input['password'];
}
if ( ! empty( $input['slug'] ) ) {
$insert_post_args['post_name'] = $input['slug'];
}
if ( ! empty( $input['toPing'] ) ) {
$insert_post_args['to_ping'] = $input['toPing'];
}
if ( ! empty( $input['pinged'] ) ) {
$insert_post_args['pinged'] = $input['pinged'];
}
$parent_id_parts = ! empty( $input['parentId'] ) ? Relay::fromGlobalId( $input['parentId'] ) : null;
if ( is_array( $parent_id_parts ) && ! empty( $parent_id_parts['id'] ) && is_int( $parent_id_parts['id'] ) ) {
$insert_post_args['post_parent'] = absint( $parent_id_parts['id'] );
}
if ( ! empty( $input['menuOrder'] ) ) {
$insert_post_args['menu_order'] = $input['menuOrder'];
}
if ( ! empty( $input['mimeType'] ) ) {
$insert_post_args['post_mime_type'] = $input['mimeType'];
}
if ( ! empty( $input['commentCount'] ) ) {
$insert_post_args['comment_count'] = $input['commentCount'];
}
/**
* Filter the $insert_post_args
*
* @param array $insert_post_args The array of $input_post_args that will be passed to wp_insert_post
* @param array $input The data that was entered as input for the mutation
* @param \WP_Post_Type $post_type_object The post_type_object that the mutation is affecting
* @param string $mutation_type The type of mutation being performed (create, edit, etc)
*/
$insert_post_args = apply_filters( 'graphql_post_object_insert_post_args', $insert_post_args, $input, $post_type_object, $mutation_name );
/**
* Return the $args
*/
return $insert_post_args;
}
/**
* This updates additional data related to a post object, such as postmeta, term relationships, etc.
*
* @param int $post_id $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext passed down to all resolvers
* @param ResolveInfo $info The ResolveInfo passed down to all resolvers
* @param string $intended_post_status The intended post_status the post should have according to the
* mutation input
* @param string $default_post_status The default status posts should use if an intended status wasn't set
*/
public static function update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, AppContext $context, ResolveInfo $info, $default_post_status = null, $intended_post_status = null ) {
/**
* Sets the post lock
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext passed down to all resolvers
* @param ResolveInfo $info The ResolveInfo passed down to all resolvers
* @param string $intended_post_status The intended post_status the post should have according to the mutation input
* @param string $default_post_status The default status posts should use if an intended status wasn't set
*
* @return bool
*/
if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) {
/**
* Set the post_lock for the $new_post_id
*/
self::set_edit_lock( $post_id );
}
/**
* Update the _edit_last field
*/
update_post_meta( $post_id, '_edit_last', get_current_user_id() );
/**
* Update the postmeta fields
*/
if ( ! empty( $input['desiredSlug'] ) ) {
update_post_meta( $post_id, '_wp_desired_post_slug', $input['desiredSlug'] );
}
/**
* Set the object terms
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
*/
self::set_object_terms( $post_id, $input, $post_type_object, $mutation_name );
/**
* Run an action after the additional data has been updated. This is a great spot to hook into to
* update additional data related to postObjects, such as setting relationships, updating additional postmeta,
* or sending emails to Kevin. . .whatever you need to do with the postObject.
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext passed down to all resolvers
* @param ResolveInfo $info The ResolveInfo passed down to all resolvers
* @param string $intended_post_status The intended post_status the post should have according to the mutation input
* @param string $default_post_status The default status posts should use if an intended status wasn't set
*/
do_action( 'graphql_post_object_mutation_update_additional_data', $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );
/**
* Sets the post lock
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext passed down to all resolvers
* @param ResolveInfo $info The ResolveInfo passed down to all resolvers
* @param string $intended_post_status The intended post_status the post should have according to the mutation input
* @param string $default_post_status The default status posts should use if an intended status wasn't set
*
* @return bool
*/
if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) {
/**
* Set the post_lock for the $new_post_id
*/
self::remove_edit_lock( $post_id );
}
}
/**
* Given a $post_id and $input from the mutation, check to see if any term associations are being made, and
* properly set the relationships
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
*/
protected static function set_object_terms( $post_id, $input, $post_type_object, $mutation_name ) {
/**
* Fire an action before setting object terms during a GraphQL Post Object Mutation.
*
* One example use for this hook would be to create terms from the input that may not exist yet, so that they can be set as a relation below.
*
* @param int $post_id The ID of the postObject being mutated
* @param array $input The input for the mutation
* @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
*/
do_action( 'graphql_post_object_mutation_set_object_terms', $post_id, $input, $post_type_object, $mutation_name );
/**
* Get the allowed taxonomies and iterate through them to find the term inputs to use for setting relationships
*/
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
/**
* If the taxonomy is in the array of taxonomies registered to the post_type
*/
if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) {
/**
* Get the tax object
*/
$tax_object = get_taxonomy( $taxonomy );
/**
* If there is input for the taxonomy, process it
*/
if ( ! empty( $tax_object->graphql_plural_name ) && ! empty( $input[ $tax_object->graphql_plural_name ] ) ) {
$term_input = $input[ $tax_object->graphql_plural_name ];
/**
* Default append to true, but allow input to set it to false.
*/
$append = isset( $term_input['append'] ) && false === $term_input['append'] ? false : true;
/**
* Start an array of terms to connect
*/
$terms_to_connect = [];
/**
* Filter whether to allow terms to be created during a post mutation.
*
* If a post mutation includes term input for a term that does not already exist,
* this will allow terms to be created in order to connect the term to the post object,
* but if filtered to false, this will prevent the term that doesn't already exist
* from being created during the mutation of the post.
*
* @param bool $allow_term_creation Whether new terms should be created during the post object mutation
* @param \WP_Taxonomy $tax_object The Taxonomy object for the term being added to the Post Object
*/
$allow_term_creation = apply_filters( 'graphql_post_object_mutations_allow_term_creation', true, $tax_object );
/**
* If there are nodes in the term_input
*/
if ( ! empty( $term_input['nodes'] ) && is_array( $term_input['nodes'] ) ) {
foreach ( $term_input['nodes'] as $node ) {
$term_exists = false;
/**
* Handle the input for ID first.
*/
if ( ! empty( $node['id'] ) ) {
if ( ! absint( $node['id'] ) ) {
$id_parts = Relay::fromGlobalId( $node['id'] );
if ( $id_parts['type'] !== $tax_object->name ) {
return;
}
if ( ! empty( $id_parts['id'] ) ) {
$term_exists = get_term_by( 'id', absint( $id_parts['id'] ), $tax_object->name );
if ( $term_exists ) {
$terms_to_connect[] = $term_exists->term_id;
}
}
} else {
$term_exists = get_term_by( 'id', absint( $node['id'] ), $tax_object->name );
if ( $term_exists ) {
$terms_to_connect[] = $term_exists->term_id;
}
}
/**
* Next, handle the input for slug if there wasn't an ID input
*/
} elseif ( ! empty( $node['slug'] ) ) {
$sanitized_slug = sanitize_text_field( $node['slug'] );
$term_exists = get_term_by( 'slug', $sanitized_slug, $tax_object->name );
if ( $term_exists ) {
$terms_to_connect[] = $term_exists->term_id;
}
/**
* If the input for the term isn't an existing term, check to make sure
* we're allowed to create new terms during a Post Object mutation
*/
}
/**
* If no term exists so far, and terms are set to be allowed to be created
* during a post object mutation, create the term to connect based on the
* input
*/
if ( ! $term_exists && true === $allow_term_creation ) {
/**
* If the current user cannot edit terms, don't create terms to connect
*/
if ( ! current_user_can( $tax_object->cap->edit_terms ) ) {
return;
}
$created_term = self::create_term_to_connect( $node, $tax_object->name );
if ( ! empty( $created_term ) ) {
$terms_to_connect[] = $created_term;
}
}
}
}
/**
* If there are terms to connect, set the connection
*/
if ( ! empty( $terms_to_connect ) && is_array( $terms_to_connect ) ) {
/**
* If the current user cannot edit terms, don't create terms to connect
*/
if ( ! current_user_can( $tax_object->cap->assign_terms ) ) {
return;
}
wp_set_object_terms( $post_id, $terms_to_connect, $tax_object->name, $append );
}
}
}
}
}
}
/**
* Given an array of Term properties (slug, name, description, etc), create the term and return a term_id
*
* @param array $node The node input for the term
* @param string $taxonomy The taxonomy the term input is for
*
* @return int $term_id The ID of the created term. 0 if no term was created.
*/
protected static function create_term_to_connect( $node, $taxonomy ) {
$created_term = [];
$term_to_create = [];
$term_args = [];
if ( ! empty( $node['name'] ) ) {
$term_to_create['name'] = sanitize_text_field( $node['name'] );
} elseif ( ! empty( $node['slug'] ) ) {
$term_to_create['name'] = sanitize_text_field( $node['slug'] );
}
if ( ! empty( $node['slug'] ) ) {
$term_args['slug'] = sanitize_text_field( $node['slug'] );
}
if ( ! empty( $node['description'] ) ) {
$term_args['description'] = sanitize_text_field( $node['description'] );
}
/**
* @todo: consider supporting "parent" input in $term_args
*/
if ( ! empty( $term_to_create['name'] ) ) {
$created_term = wp_insert_term( $term_to_create['name'], $taxonomy, $term_args );
}
if ( is_wp_error( $created_term ) && isset( $created_term->error_data['term_exists'] ) ) {
return $created_term->error_data['term_exists'];
}
/**
* Return the created term, or 0
*/
return ! empty( $created_term['term_id'] ) ? $created_term['term_id'] : 0;
}
/**
* This is a copy of the wp_set_post_lock function that exists in WordPress core, but is not
* accessible because that part of WordPress is never loaded for WPGraphQL executions
*
* Mark the post as currently being edited by the current user
*
* @param int $post_id ID of the post being edited.
*
* @return array|false Array of the lock time and user ID. False if the post does not exist, or
* there is no current user.
*/
public static function set_edit_lock( $post_id ) {
$post = get_post( $post_id );
$user_id = get_current_user_id();
if ( empty( $post ) ) {
return false;
}
if ( 0 === $user_id ) {
return false;
}
$now = time();
$lock = "$now:$user_id";
update_post_meta( $post->ID, '_edit_lock', $lock );
return [ $now, $user_id ];
}
/**
* Remove the edit lock for a post
*
* @param int $post_id ID of the post to delete the lock for
*
* @return bool
*/
public static function remove_edit_lock( $post_id ) {
$post = get_post( $post_id );
if ( ! is_a( $post, 'WP_Post' ) ) {
return false;
}
return delete_post_meta( $post->ID, '_edit_lock' );
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace WPGraphQL\Type\PostObject\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectUpdate
*
* @package WPGraphQL\Type\PostObject\Mutation
*/
class PostObjectUpdate {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the Update mutation for PostTypeObjects
*
* @param \WP_Post_Type $post_type_object
*
* @return array|mixed
*/
public static function mutate( \WP_Post_Type $post_type_object ) {
if (
! empty( $post_type_object->graphql_single_name ) &&
empty( self::$mutation[ $post_type_object->graphql_single_name ] )
) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'Update' . ucwords( $post_type_object->graphql_single_name );
self::$mutation[ $post_type_object->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => $mutation_name,
// translators: The placeholder is the name of the post type being updated
'description' => sprintf( __( 'Updates %1$s objects', 'wp-graphql' ), $post_type_object->graphql_single_name ),
'inputFields' => WPInputObjectType::prepare_fields( self::input_fields( $post_type_object ), $mutation_name ),
'outputFields' => [
$post_type_object->graphql_single_name => [
'type' => Types::post_object( $post_type_object->name ),
'resolve' => function( $payload ) {
return get_post( $payload['postObjectId'] );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
$id_parts = ! empty( $input['id'] ) ? Relay::fromGlobalId( $input['id'] ) : null;
$existing_post = get_post( absint( $id_parts['id'] ) );
/**
* If there's no existing post, throw an exception
*/
if ( empty( $id_parts['id'] ) || false === $existing_post || $id_parts['type'] !== $post_type_object->name ) {
// translators: the placeholder is the name of the type of post being updated
throw new UserError( sprintf( __( 'No %1$s could be found to update', 'wp-graphql' ), $post_type_object->graphql_single_name ) );
}
if ( $post_type_object->name !== $existing_post->post_type ) {
// translators: The first placeholder is an ID and the second placeholder is the name of the post type being edited
throw new UserError( sprintf( __( 'The id %1$d is not of the type "%2$s"', 'wp-graphql' ), $id_parts['id'], $post_type_object->name ) );
}
/**
* Stop now if a user isn't allowed to edit posts
*/
if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) {
// translators: the $post_type_object->graphql_single_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to update a %1$s', 'wp-graphql' ), $post_type_object->graphql_single_name ) );
}
/**
* If the mutation is setting the author to be someone other than the user making the request
* make sure they have permission to edit others posts
*/
$author_id_parts = ! empty( $input['authorId'] ) ? Relay::fromGlobalId( $input['authorId'] ) : null;
if ( ! empty( $author_id_parts['id'] ) && get_current_user_id() !== $author_id_parts['id'] && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) {
// translators: the $post_type_object->graphql_single_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to update %1$s as this user.', 'wp-graphql' ), $post_type_object->graphql_plural_name ) );
}
/**
* @todo: when we add support for making posts sticky, we should check permissions to make sure users can make posts sticky
* @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L640-L642
*/
/**
* @todo: when we add support for assigning terms to posts, we should check permissions to make sure they can assign terms
* @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L644-L646
*/
/**
* insert the post object and get the ID
*/
$post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );
$post_args['ID'] = absint( $id_parts['id'] );
/**
* Insert the post and retrieve the ID
*/
$post_id = wp_update_post( wp_slash( (array) $post_args ), true );
/**
* Throw an exception if the post failed to update
*/
if ( is_wp_error( $post_id ) ) {
throw new UserError( __( 'The object failed to update but no error was provided', 'wp-graphql' ) );
}
/**
* Fires after a single term is created or updated via a GraphQL mutation
*
* The dynamic portion of the hook name, `$taxonomy->name` refers to the taxonomy of the term being mutated
*
* @param int $post_id Inserted post ID
* @param array $args The args used to insert the term
* @param string $mutation_name The name of the mutation being performed
*/
do_action( "graphql_insert_{$post_type_object->name}", $post_id, $post_args, $mutation_name );
/**
* This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
*
* The input for the postObjectMutation will be passed, along with the $new_post_id for the
* postObject that was updated so that relations can be set, meta can be updated, etc.
*/
PostObjectMutation::update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info );
/**
* Return the payload
*/
return [
'postObjectId' => $post_id,
];
},
] );
}
return ! empty( self::$mutation[ $post_type_object->graphql_single_name ] ) ? self::$mutation[ $post_type_object->graphql_single_name ] : null;
}
/**
* Add the id as a nonNull field for update mutations
*
* @param \WP_Post_Type $post_type_object
*
* @return array
*/
private static function input_fields( $post_type_object ) {
/**
* Update mutations require an ID to be passed
*/
return array_merge(
[
'id' => [
'type' => Types::non_null( Types::id() ),
// translators: the placeholder is the name of the type of post object being updated
'description' => sprintf( __( 'The ID of the %1$s object', 'wp-graphql' ), $post_type_object->graphql_single_name ),
],
],
PostObjectMutation::input_fields( $post_type_object )
);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WPGraphQL\Type\PostObject;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectQuery
* @package WPGraphQL\Type\PostObject
* @Since 0.0.5
*/
class PostObjectQuery {
/**
* Holds the root_query field definition
* @var array $root_query
* @since 0.0.5
*/
private static $root_query = [];
/**
* Holds the definition of the $post_object_by field
* @var array $post_object_by
*/
private static $post_object_by = [];
/**
* Holds the definition for the args that can be input on the $post_object_by field
* @var array $post_object_by_args
*/
private static $post_object_by_args = [];
/**
* Method that returns the root query field definition for the post object type
*
* @param object $post_type_object
* @return array
* @since 0.0.5
*/
public static function root_query( $post_type_object ) {
if ( ! empty( $post_type_object->name ) && empty( self::$root_query[ $post_type_object->name ] ) ) {
self::$root_query[ $post_type_object->name ] = [
'type' => Types::post_object( $post_type_object->name ),
'description' => sprintf( __( 'A % object', 'wp-graphql' ), $post_type_object->graphql_single_name ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_post_object( $id_components['id'], $post_type_object->name );
},
];
}
return ! empty( self::$root_query[ $post_type_object->name ] ) ? self::$root_query[ $post_type_object->name ] : null;
}
/**
* Method that returns the "post_object_by" field definition to get a post object by id, postId or slug.
*
* @param \WP_Post_Type $post_type_object
* @return array
*/
public static function post_object_by( \WP_Post_Type $post_type_object ) {
if ( ! empty( $post_type_object->name ) && empty( self::$post_object_by[ $post_type_object->name ] ) ) {
self::$post_object_by[ $post_type_object->name ] = [
'type' => Types::post_object( $post_type_object->name ),
'description' => sprintf( __( 'A %s object', 'wp-graphql' ), $post_type_object->graphql_single_name ),
'args' => self::post_object_by_args( $post_type_object ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) {
$post_object = null;
if ( ! empty( $args['id'] ) ) {
$id_components = Relay::fromGlobalId( $args['id'] );
if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) {
throw new UserError( __( 'The "id" is invalid', 'wp-graphql' ) );
}
$post_object = DataSource::resolve_post_object( absint( $id_components['id'] ), $post_type_object->name );
} elseif ( ! empty( $args[ $post_type_object->graphql_single_name . 'Id' ] ) ) {
$id = $args[ $post_type_object->graphql_single_name . 'Id' ];
$post_object = DataSource::resolve_post_object( $id, $post_type_object->name );
} elseif ( ! empty( $args['uri'] ) ) {
$uri = esc_html( $args['uri'] );
$post_object = DataSource::get_post_object_by_uri( $uri, 'OBJECT', $post_type_object->name );
} elseif ( ! empty( $args['slug'] ) ) {
$slug = esc_html( $args['slug'] );
$post_object = DataSource::get_post_object_by_uri( $slug, 'OBJECT', $post_type_object->name );
}
if ( empty( $post_object ) || is_wp_error( $post_object ) ) {
throw new UserError( __( 'No resource could be found', 'wp-graphql' ) );
}
if ( ! $post_object instanceof \WP_Post ) {
throw new UserError( __( 'The queried resource is not valid', 'wp-graphql' ) );
}
if ( $post_type_object->name !== $post_object->post_type ) {
throw new UserError( __( 'The queried resource is not the correct type', 'wp-graphql' ) );
}
return $post_object;
},
];
}
return ! empty( self::$post_object_by[ $post_type_object->name ] ) ? self::$post_object_by[ $post_type_object->name ] : null;
}
/**
* Define the args to be used by the $postObject.By field
* @param \WP_Post_Type $post_type_object
*
* @return mixed
*/
public static function post_object_by_args( \WP_Post_Type $post_type_object ) {
if ( empty( self::$post_object_by_args[ ucfirst( $post_type_object->name ) . 'ByArgs' ] ) ) {
$args = [
'id' => [
'type' => Types::string(),
'description' => sprintf( __( 'Get the object by it\'s global ID', 'wp-graphql' ), $post_type_object->graphql_single_name ),
],
$post_type_object->graphql_single_name . 'Id' => [
'type' => Types::int(),
'description' => sprintf( __( 'Get the %s by it\'s database ID', 'wp-graphql' ), $post_type_object->graphql_single_name ),
],
'uri' => [
'type' => Types::string(),
'description' => sprintf( __( 'Get the %s by it\'s uri', 'wp-graphql' ), $post_type_object->graphql_single_name ),
]
];
if ( false === $post_type_object->hierarchical ) {
$args['slug'] = [
'type' => Types::string(),
'description' => sprintf( __( 'Get the %s by it\'s slug (only available for non-hierarchical types)', 'wp-graphql' ), $post_type_object->graphql_single_name ),
];
}
self::$post_object_by_args[ $post_type_object->name . 'ByArgs' ] = WPInputObjectType::prepare_fields( $args, ucfirst( $post_type_object->name . 'ByArgs' ) );
}
return self::$post_object_by_args[ $post_type_object->name . 'ByArgs' ];
}
}

View File

@@ -0,0 +1,576 @@
<?php
namespace WPGraphQL\Type\PostObject;
use GraphQL\Deferred;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Data\Loader;
use WPGraphQL\Type\Comment\Connection\CommentConnectionDefinition;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionDefinition;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionDefinition;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class PostObjectType
*
* This sets up the base PostObjectType. Custom Post Types that are set to "show_in_graphql" automatically
* use the PostObjectType and inherit the fields that are defined here. The fields get passed through a
* filter unique to each type, so each post_type can modify it's type schema via field filters.
*
* NOTE: In some cases the shape of a Custom Post Type's schema is so drastically different from the standard
* PostObjectType shape it might make more sense for the custom post type to register a different type
* altogether instead of utilizing the PostObjectType.
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class PostObjectType extends WPObjectType {
/**
* Holds the $fields definition for the PostObjectType
*
* @var $fields
*/
private static $fields = [];
/**
* Holds the post_type_object
*
* @var object $post_type_object
*/
private static $post_type_object;
/**
* PostObjectType constructor.
*
* @param string $post_type The post_type name
*
* @since 0.0.5
*/
public function __construct( $post_type ) {
/**
* Get the post_type_object from the post_type and store it
* for later use
*
* @since 0.0.5
*/
self::$post_type_object = get_post_type_object( $post_type );
$config = [
'name' => ucfirst( self::$post_type_object->graphql_single_name ),
// translators: the placeholder is the post_type of the object
'description' => sprintf( __( 'The %s object type', 'wp-graphql' ), self::$post_type_object->graphql_single_name ),
'fields' => self::fields( self::$post_type_object ),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
* This defines the fields for PostObjectTypes
*
* @param $post_type_object
*
* @return \GraphQL\Type\Definition\FieldDefinition|mixed|null
* @since 0.0.5
*/
private static function fields( $post_type_object ) {
/**
* Get the $single_name out of the post_type_object
*
* @since 0.0.5
*/
$single_name = self::$post_type_object->graphql_single_name;
/**
* If the $fields haven't already been defined for this type,
* define the fields
*
* @since 0.0.5
*/
if ( empty( self::$fields[ $single_name ] ) ) {
/**
* Get the taxonomies that are allowed in WPGraphQL
*
* @since 0.0.5
*/
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
/**
* Define the fields for the post_type
*
* @return mixed
* @since 0.0.5
*/
self::$fields[ $single_name ] = function() use ( $single_name, $post_type_object, $allowed_taxonomies ) {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The globally unique ID for the object', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $post->post_type ) && ! empty( $post->ID ) ) ? Relay::toGlobalId( $post->post_type, $post->ID ) : null;
},
],
$single_name . 'Id' => [
'type' => Types::non_null( Types::int() ),
'description' => __( 'The id field matches the WP_Post->ID field.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return absint( $post->ID );
},
],
'ancestors' => [
'type' => Types::list_of( Types::post_object_union() ),
'description' => esc_html__( 'Ancestors of the object', 'wp-graphql' ),
'args' => [
'types' => [
'type' => Types::list_of( Types::post_type_enum() ),
'description' => __( 'The types of ancestors to check for. Defaults to the same type as the current object', 'wp-graphql' ),
],
],
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$ancestors = [];
$types = ! empty( $args['types'] ) ? $args['types'] : [ $post->post_type ];
$ancestor_ids = get_ancestors( $post->ID, $post->post_type );
if ( ! empty( $ancestor_ids ) ) {
foreach ( $ancestor_ids as $ancestor_id ) {
$ancestor_obj = get_post( $ancestor_id );
if ( in_array( $ancestor_obj->post_type, $types, true ) ) {
$ancestors[] = $ancestor_obj;
}
}
}
return ! empty( $ancestors ) ? $ancestors : null;
},
],
'author' => [
'type' => Types::user(),
'description' => __( "The author field will return a queryable User type matching the post's author.", 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return DataSource::resolve_user( $post->post_author );
},
],
'date' => [
'type' => Types::string(),
'description' => __( 'Post publishing date.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_date ) ? $post->post_date : null;
},
],
'dateGmt' => [
'type' => Types::string(),
'description' => __( 'The publishing date set in GMT.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_date_gmt ) ? Types::prepare_date_response( $post->post_date_gmt ) : null;
},
],
'content' => [
'type' => Types::string(),
'description' => __( 'The content of the post.', 'wp-graphql' ),
'args' => [
'format' => self::post_object_format_arg(),
],
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$content = ! empty( $post->post_content ) ? $post->post_content : null;
// If the raw format is requested, don't apply any filters.
if ( isset( $args['format'] ) && 'raw' === $args['format'] ) {
return $content;
}
return apply_filters( 'the_content', $content );
},
],
'title' => [
'type' => Types::string(),
'description' => __( 'The title of the post. This is currently just the raw title. An amendment to support rendered title needs to be made.', 'wp-graphql' ),
'args' => [
'format' => self::post_object_format_arg(),
],
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$title = ! empty( $post->post_title ) ? $post->post_title : null;
// If the raw format is requested, don't apply any filters.
if ( isset( $args['format'] ) && 'raw' === $args['format'] ) {
return $title;
}
return apply_filters( 'the_title', $title );
},
],
'excerpt' => [
'type' => Types::string(),
'description' => __( 'The excerpt of the post. This is currently just the raw excerpt. An amendment to support rendered excerpts needs to be made.', 'wp-graphql' ),
'args' => [
'format' => self::post_object_format_arg(),
],
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$excerpt = ! empty( $post->post_excerpt ) ? $post->post_excerpt : null;
// If the raw format is requested, don't apply any filters.
if ( isset( $args['format'] ) && 'raw' === $args['format'] ) {
return $excerpt;
}
$excerpt = apply_filters( 'get_the_excerpt', $excerpt, $post );
return apply_filters( 'the_excerpt', $excerpt );
},
],
'status' => [
'type' => Types::string(),
'description' => __( 'The current status of the object', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_status ) ? $post->post_status : null;
},
],
'commentStatus' => array(
'type' => Types::string(),
'description' => __( 'Whether the comments are open or closed for this particular post.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->comment_status ) ? $post->comment_status : null;
},
),
'pingStatus' => [
'type' => Types::string(),
'description' => __( 'Whether the pings are open or closed for this particular post.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->ping_status ) ? $post->ping_status : null;
},
],
'slug' => [
'type' => Types::string(),
'description' => __( 'The uri slug for the post. This is equivalent to the WP_Post->post_name field and the post_name column in the database for the `post_objects` table.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_name ) ? $post->post_name : null;
},
],
'toPing' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'URLs queued to be pinged.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->to_ping ) ? implode( ',', $post->to_ping ) : null;
},
],
'pinged' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'URLs that have been pinged.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->pinged ) ? implode( ',', $post->pinged ) : null;
},
],
'modified' => [
'type' => Types::string(),
'description' => __( 'The local modified time for a post. If a post was recently updated the modified field will change to match the corresponding time.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_modified ) ? $post->post_modified : null;
},
],
'modifiedGmt' => [
'type' => Types::string(),
'description' => __( 'The GMT modified time for a post. If a post was recently updated the modified field will change to match the corresponding time in GMT.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_modified_gmt ) ? Types::prepare_date_response( $post->post_modified_gmt ) : null;
},
],
'parent' => [
'type' => Types::post_object_union(),
'description' => __( 'The parent of the object. The parent object can be of various types', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->post_parent ) ? get_post( $post->post_parent ) : null;
},
],
'editLast' => [
'type' => Types::user(),
'description' => __( 'The user that most recently edited the object', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, array $args, AppContext $context, ResolveInfo $info ) {
$edit_last = get_post_meta( $post->ID, '_edit_last', true );
return ! empty( $edit_last ) ? DataSource::resolve_user( absint( $edit_last ) ) : null;
},
],
'editLock' => [
'type' => Types::edit_lock(),
'description' => __( 'If a user has edited the object within the past 15 seconds, this will return the user and the time they last edited. Null if the edit lock doesn\'t exist or is greater than 15 seconds', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, array $args, AppContext $context, ResolveInfo $info ) {
$edit_lock = get_post_meta( $post->ID, '_edit_lock', true );
$edit_lock_parts = explode( ':', $edit_lock );
return ! empty( $edit_lock_parts ) ? $edit_lock_parts : null;
},
],
'enclosure' => [
'type' => Types::string(),
'description' => __( 'The RSS enclosure for the object', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, array $args, AppContext $context, ResolveInfo $info ) {
$enclosure = get_post_meta( $post->ID, 'enclosure', true );
return ! empty( $enclosure ) ? $enclosure : null;
},
],
'guid' => [
'type' => Types::string(),
'description' => __( 'The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the `post_objects` database table.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->guid ) ? $post->guid : null;
},
],
'menuOrder' => [
'type' => Types::int(),
'description' => __( 'A field used for ordering posts. This is typically used with nav menu items or for special ordering of hierarchical content types.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->menu_order ) ? absint( $post->menu_order ) : null;
},
],
'desiredSlug' => [
'type' => Types::string(),
'description' => __( 'The desired slug of the post', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$desired_slug = get_post_meta( $post->ID, '_wp_desired_post_slug', true );
return ! empty( $desired_slug ) ? $desired_slug : null;
},
],
'link' => [
'type' => Types::string(),
'description' => __( 'The permalink of the post', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$link = get_permalink( $post->ID );
return ! empty( $link ) ? $link : null;
},
],
'uri' => [
'type' => Types::string(),
'description' => __( 'URI path for the resource', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$uri = get_page_uri( $post->ID );
return ! empty( $uri ) ? $uri : null;
},
],
'terms' => [
'type' => Types::list_of( Types::term_object_union() ),
'args' => [
'taxonomies' => [
'type' => Types::list_of( Types::taxonomy_enum() ),
'description' => __( 'Select which taxonomies to limit the results to', 'wp-graphql' ),
],
],
// Translators: placeholder is the name of the post_type
'description' => sprintf( __( 'Terms connected to the %1$s', 'wp-graphql' ), $single_name ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) use ( $allowed_taxonomies ) {
/**
* If the $arg for taxonomies is populated, use it as the $allowed_taxonomies
* otherwise use the default $allowed_taxonomies passed down
*/
$taxonomies = [];
if ( ! empty( $args['taxonomies'] ) && is_array( $args['taxonomies'] ) ) {
$taxonomies = $args['taxonomies'];
} else {
$connected_taxonomies = get_object_taxonomies( $post, 'names' );
foreach( $connected_taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, \WPGraphQL::$allowed_taxonomies ) ) {
$taxonomies[] = $taxonomy;
}
}
}
$tax_terms = [];
if ( ! empty( $taxonomies ) ) {
$term_query = new \WP_Term_Query( [
'taxonomy' => $taxonomies,
'object_ids' => $post->ID,
] );
$tax_terms = $term_query->get_terms();
}
return ! empty( $tax_terms ) && is_array( $tax_terms ) ? $tax_terms : null;
},
],
'termNames' => [
'type' => Types::list_of( Types::string() ),
'args' => [
'taxonomies' => [
'type' => Types::list_of( Types::taxonomy_enum() ),
'description' => __( 'Select which taxonomies to limit the results to', 'wp-graphql' ),
],
],
// Translators: placeholder is the name of the post_type
'description' => sprintf( __( 'Terms connected to the %1$s', 'wp-graphql' ), $single_name ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) use ( $allowed_taxonomies ) {
/**
* If the $arg for taxonomies is populated, use it as the $allowed_taxonomies
* otherwise use the default $allowed_taxonomies passed down
*/
$taxonomies = [];
if ( ! empty( $args['taxonomies'] ) && is_array( $args['taxonomies'] ) ) {
$taxonomies = $args['taxonomies'];
} else {
$connected_taxonomies = get_object_taxonomies( $post, 'names' );
foreach( $connected_taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, \WPGraphQL::$allowed_taxonomies ) ) {
$taxonomies[] = $taxonomy;
}
}
}
$tax_terms = [];
if ( ! empty( $taxonomies ) ) {
$term_query = new \WP_Term_Query( [
'taxonomy' => $taxonomies,
'object_ids' => [ $post->ID ],
] );
$tax_terms = $term_query->get_terms();
}
$term_names = ! empty( $tax_terms ) && is_array( $tax_terms ) ? wp_list_pluck( $tax_terms, 'name' ) : [];
return ! empty( $term_names ) ? $term_names : null;
},
],
'termSlugs' => [
'type' => Types::list_of( Types::string() ),
'args' => [
'taxonomies' => [
'type' => Types::list_of( Types::taxonomy_enum() ),
'description' => __( 'Select which taxonomies to limit the results to', 'wp-graphql' ),
],
],
// Translators: placeholder is the name of the post_type
'description' => sprintf( __( 'Terms connected to the %1$s', 'wp-graphql' ), $single_name ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) use ( $allowed_taxonomies ) {
/**
* If the $arg for taxonomies is populated, use it as the $allowed_taxonomies
* otherwise use the default $allowed_taxonomies passed down
*/
$taxonomies = [];
if ( ! empty( $args['taxonomies'] ) && is_array( $args['taxonomies'] ) ) {
$taxonomies = $args['taxonomies'];
} else {
$connected_taxonomies = get_object_taxonomies( $post, 'names' );
foreach( $connected_taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, \WPGraphQL::$allowed_taxonomies ) ) {
$taxonomies[] = $taxonomy;
}
}
}
$tax_terms = [];
if ( ! empty( $taxonomies ) ) {
$term_query = new \WP_Term_Query( [
'taxonomy' => $taxonomies,
'object_ids' => [ $post->ID ],
] );
$tax_terms = $term_query->get_terms();
}
$term_slugs = ! empty( $tax_terms ) && is_array( $tax_terms ) ? wp_list_pluck( $tax_terms, 'slug' ) : [];
return ! empty( $term_slugs ) ? $term_slugs : null;
},
],
];
/**
* Add comment fields to the schema if the post_type supports "comments"
*
* @since 0.0.5
*/
if ( post_type_supports( $post_type_object->name, 'comments' ) ) {
$fields['comments'] = CommentConnectionDefinition::connection( $post_type_object->graphql_single_name );
$fields['commentCount'] = [
'type' => Types::int(),
'description' => __( 'The number of comments. Even though WPGraphQL denotes this field as an integer, in WordPress this field should be saved as a numeric string for compatability.', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post->comment_count ) ? absint( $post->comment_count ) : null;
},
];
}
/**
* If the post_type is Hierarchical, there should be a children field
*/
if ( true === $post_type_object->hierarchical ) {
$fields[ 'child' . ucfirst( $post_type_object->graphql_plural_name ) ] = PostObjectConnectionDefinition::connection( $post_type_object, 'Children' );
}
/**
* Add term connections based on the allowed taxonomies that are also
* registered to the post_type
*
* @since 0.0.5
*/
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
// If the taxonomy is in the array of taxonomies registered to the post_type
if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) {
$tax_object = get_taxonomy( $taxonomy );
$fields[ $tax_object->graphql_plural_name ] = TermObjectConnectionDefinition::connection( $tax_object, $post_type_object->graphql_single_name );
}
}
}
if ( post_type_supports( $post_type_object->name, 'thumbnail' ) ) {
$fields['featuredImage'] = [
'type' => Types::post_object( 'attachment' ),
'description' => __( 'The featured image for the object', 'wp-graphql' ),
'resolve' => function( \WP_Post $post, $args, AppContext $context, ResolveInfo $info ) {
$thumbnail_id = get_post_thumbnail_id( $post->ID );
return ! empty( $thumbnail_id ) ? get_post( absint( $thumbnail_id ) ) : null;
},
];
}
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*
* @since 0.0.5
*/
return self::prepare_fields( $fields, $single_name );
};
}
return ! empty( self::$fields[ $single_name ] ) ? self::$fields[ $single_name ] : null;
}
/**
* Define the args to be used by post object fields.
*
* @return mixed
*/
public static function post_object_format_arg() {
return [
'type' => Types::post_object_field_format_enum(),
'description' => __( 'Format of the field output', 'wp-graphql' ),
];
}
}

View File

@@ -0,0 +1,497 @@
<?php
namespace WPGraphQL\Type\PostType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class PostTypeType
* @package WPGraphQL\Type
* @since 0.0.5
*/
class PostTypeType extends WPObjectType {
/**
* Holds the type name
* @var string $type_name
*/
private static $type_name;
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
private static $fields;
/**
* Holds the object definition for labels details
*
* @var object $labels_details
*/
private static $labels_details;
/**
* PostTypeType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
* @since 0.0.5
*/
self::$type_name = 'PostType';
$config = [
'name' => self::$type_name,
'description' => __( 'An Post Type object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the PostTypeType
*
* @return array
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
/**
* Get the taxonomies that are allowed in WPGraphQL
*
* @since 0.0.6
*/
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
self::$fields = function() use ( $allowed_taxonomies ) {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'resolve' => function( \WP_Post_Type $post_type, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $post_type->name ) && ! empty( $post_type->name ) ) ? Relay::toGlobalId( 'postType', $post_type->name ) : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'The internal name of the post type. This should not be used for display purposes.', 'wp-graphql' ),
],
'label' => [
'type' => Types::string(),
'description' => __( 'Display name of the content type.', 'wp-graphql' ),
],
'labels' => [
'type' => self::labels_details(),
'description' => __( 'Details about the post type labels.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, $args, $context, ResolveInfo $info ) {
return get_post_type_labels( $post_type );
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the content type.', 'wp-graphql' ),
],
'public' => [
'type' => Types::boolean(),
'description' => __( 'Whether a post type is intended for use publicly either via the admin interface or by front-end users. While the default settings of exclude_from_search, publicly_queryable, show_ui, and show_in_nav_menus are inherited from public, each does not rely on this relationship and controls a very specific intention.', 'wp-graphql' ),
],
'hierarchical' => [
'type' => Types::boolean(),
'description' => __( 'Whether the post type is hierarchical, for example pages.', 'wp-graphql' ),
],
'excludeFromSearch' => [
'type' => Types::boolean(),
'description' => __( 'Whether to exclude posts with this post type from front end search results.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->exclude_from_search ) ? true : false;
},
],
'publiclyQueryable' => [
'type' => Types::boolean(),
'description' => __( 'Whether queries can be performed on the front end for the post type as part of parse_request().', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->publicly_queryable ) ? true : false;
},
],
'showUi' => [
'type' => Types::boolean(),
'description' => __( 'Whether to generate and allow a UI for managing this post type in the admin.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->show_ui ) ? true : false;
},
],
'showInMenu' => [
'type' => Types::boolean(),
'description' => __( 'Where to show the post type in the admin menu. To work, $show_ui must be true. If true, the post type is shown in its own top level menu. If false, no menu is shown. If a string of an existing top level menu (eg. "tools.php" or "edit.php?post_type=page"), the post type will be placed as a sub-menu of that.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->show_in_menu ) ? true : false;
},
],
'showInNavMenus' => [
'type' => Types::boolean(),
'description' => __( 'Makes this post type available for selection in navigation menus.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->show_in_nav_menus ) ? true : false;
},
],
'showInAdminBar' => [
'type' => Types::boolean(),
'description' => __( 'Makes this post type available via the admin bar.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return empty( true === $post_type->show_in_admin_bar ) ? true : false;
},
],
'menuPosition' => [
'type' => Types::int(),
'description' => __( 'The position of this post type in the menu. Only applies if show_in_menu is true.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->menu_position ) ? $post_type->menu_position : null;
},
],
'menuIcon' => [
'type' => Types::string(),
'description' => __( 'The name of the icon file to display as a menu icon.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->menu_icon ) ? $post_type->menu_icon : null;
},
],
'hasArchive' => [
'type' => Types::boolean(),
'description' => __( 'Whether this content type should have archives. Content archives are generated by type and by date.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->has_archive ) ? true : false;
},
],
'canExport' => [
'type' => Types::boolean(),
'description' => __( 'Whether this content type should can be exported.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->can_export ) ? true : false;
},
],
'deleteWithUser' => [
'type' => Types::boolean(),
'description' => __( 'Whether delete this type of content when the author of it is deleted from the system.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->delete_with_user ) ? true : false;
},
],
'showInRest' => [
'type' => Types::boolean(),
'description' => __( 'Whether to add the post type route in the REST API `wp/v2` namespace.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->show_in_rest ) ? true : false;
},
],
'restBase' => [
'type' => Types::string(),
'description' => __( 'Name of content type to diplay in REST API `wp/v2` namespace.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->rest_base ) ? $post_type->rest_base : null;
},
],
'restControllerClass' => [
'type' => Types::string(),
'description' => __( 'The REST Controller class assigned to handling this content type.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : null;
},
],
'showInGraphql' => [
'type' => Types::boolean(),
'description' => __( 'Whether to add the post type to the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $post_type->show_in_graphql ) ? true : false;
},
],
'graphqlSingleName' => [
'type' => Types::string(),
'description' => __( 'The singular name of the post type within the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->graphql_single_name ) ? $post_type->graphql_single_name : null;
},
],
'graphqlPluralName' => [
'type' => Types::string(),
'description' => __( 'The plural name of the post type within the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $post_type->graphql_plural_name ) ? $post_type->graphql_plural_name : null;
},
],
'connectedTaxonomyNames' => [
'type' => Types::list_of( Types::string() ),
'args' => [
'taxonomies' => [
'type' => Types::list_of( Types::taxonomy_enum() ),
'description' => __( 'Select which taxonomies to limit the results to', 'wp-graphql' ),
],
],
'description' => __( 'A list of Taxonomies associated with the post type', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type_object, array $args, $context, ResolveInfo $info ) use ( $allowed_taxonomies ) {
$object_taxonomies = get_object_taxonomies( $post_type_object->name );
$taxonomy_names = [];
/**
* If the $arg for taxonomies is populated, use it as the $allowed_taxonomies
* otherwise use the default $allowed_taxonomies passed down
*/
$allowed_taxonomies = ! empty( $args['taxonomies'] ) && is_array( $args['taxonomies'] ) ? $args['taxonomies'] : $allowed_taxonomies;
if ( ! empty( $object_taxonomies ) && is_array( $object_taxonomies ) ) {
foreach ( $object_taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, $allowed_taxonomies, true ) ) {
$taxonomy_names[] = $taxonomy;
}
}
}
return ! empty( $taxonomy_names ) ? $taxonomy_names : null;
},
],
'connectedTaxonomies' => [
'type' => Types::list_of( Types::taxonomy() ),
'args' => [
'taxonomies' => [
'type' => Types::list_of( Types::taxonomy_enum() ),
'description' => __( 'Select which taxonomies to limit the results to', 'wp-graphql' ),
],
],
'description' => __( 'List of Taxonomies connected to the Post Type', 'wp-graphql' ),
'resolve' => function( \WP_Post_Type $post_type_object, array $args, AppContext $context, ResolveInfo $info ) use ( $allowed_taxonomies ) {
$tax_objects = [];
/**
* If the $arg for taxonomies is populated, use it as the $allowed_taxonomies
* otherwise use the default $allowed_taxonomies passed down
*/
$allowed_taxonomies = ! empty( $args['taxonomies'] ) && is_array( $args['taxonomies'] ) ? $args['taxonomies'] : $allowed_taxonomies;
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) {
$tax_object = get_taxonomy( $taxonomy );
$tax_objects[ $tax_object->graphql_single_name ] = $tax_object;
}
}
}
return ! empty( $tax_objects ) ? $tax_objects : null;
},
],
];
/**
* Pass the fields through a filter to allow for hooking in and adjusting the shape
* of the type's schema
*
* @since 0.0.5
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
/**
* This defines the labels details object type that can be queried on mediaItems
*
* @return null|WPObjectType
* @since 0.0.6
*/
private static function labels_details() {
if ( null === self::$labels_details ) {
self::$labels_details = new WPObjectType( [
'name' => 'LabelsDetails',
'fields' => function() {
$fields = [
'name' => [
'type' => Types::string(),
'description' => __( 'General name for the post type, usually plural.', 'wp-graphql' ),
],
'singularName' => [
'type' => Types::string(),
'description' => __( 'Name for one object of this post type.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->singular_name ) ? $labels->singular_name : null;
},
],
'addNew' => [
'type' => Types::string(),
'description' => __( 'Default is Add New for both hierarchical and non-hierarchical types.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->add_new ) ? $labels->add_new : null;
},
],
'addNewItem' => [
'type' => Types::string(),
'description' => __( 'Label for adding a new singular item.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->add_new_item ) ? $labels->add_new_item : null;
},
],
'editItem' => [
'type' => Types::string(),
'description' => __( 'Label for editing a singular item.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->edit_item ) ? $labels->edit_item : null;
},
],
'newItem' => [
'type' => Types::string(),
'description' => __( 'Label for the new item page title.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->new_item ) ? $labels->new_item : null;
},
],
'viewItem' => [
'type' => Types::string(),
'description' => __( 'Label for viewing a singular item.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->view_item ) ? $labels->view_item : null;
},
],
'viewItems' => [
'type' => Types::string(),
'description' => __( 'Label for viewing post type archives.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->view_items ) ? $labels->view_items : null;
},
],
'searchItems' => [
'type' => Types::string(),
'description' => __( 'Label for searching plural items.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->search_items ) ? $labels->search_items : null;
},
],
'notFound' => [
'type' => Types::string(),
'description' => __( 'Label used when no items are found.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->not_found ) ? $labels->not_found : null;
},
],
'notFoundInTrash' => [
'type' => Types::string(),
'description' => __( 'Label used when no items are in the trash.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->not_found_in_trash ) ? $labels->not_found_in_trash : null;
},
],
'parentItemColon' => [
'type' => Types::string(),
'description' => __( 'Label used to prefix parents of hierarchical items.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->parent_item_colon ) ? $labels->parent_item_colon : null;
},
],
'allItems' => [
'type' => Types::string(),
'description' => __( 'Label to signify all items in a submenu link.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->all_items ) ? $labels->all_items : null;
},
],
'archives' => [
'type' => Types::string(),
'description' => __( 'Label for archives in nav menus', 'wp-graphql' ),
],
'attributes' => [
'type' => Types::string(),
'description' => __( 'Label for the attributes meta box.', 'wp-graphql' ),
],
'insertIntoItem' => [
'type' => Types::string(),
'description' => __( 'Label for the media frame button.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->insert_into_item ) ? $labels->insert_into_item : null;
},
],
'uploadedToThisItem' => [
'type' => Types::string(),
'description' => __( 'Label for the media frame filter.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->uploaded_to_this_item ) ? $labels->uploaded_to_this_item : null;
},
],
'featuredImage' => [
'type' => Types::string(),
'description' => __( 'Label for the Featured Image meta box title.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->featured_image ) ? $labels->featured_image : null;
},
],
'setFeaturedImage' => [
'type' => Types::string(),
'description' => __( 'Label for setting the featured image.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->set_featured_image ) ? $labels->set_featured_image : null;
},
],
'removeFeaturedImage' => [
'type' => Types::string(),
'description' => __( 'Label for removing the featured image.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->remove_featured_image ) ? $labels->remove_featured_image : null;
},
],
'useFeaturedImage' => [
'type' => Types::string(),
'description' => __( 'Label in the media frame for using a featured image.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->use_featured_item ) ? $labels->use_featured_item : null;
},
],
'menuName' => [
'type' => Types::string(),
'description' => __( 'Label for the menu name.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->menu_name ) ? $labels->menu_name : null;
},
],
'filterItemsList' => [
'type' => Types::string(),
'description' => __( 'Label for the table views hidden heading.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->filter_items_list ) ? $labels->filter_items_list : null;
},
],
'itemsListNavigation' => [
'type' => Types::string(),
'description' => __( 'Label for the table pagination hidden heading.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->items_list_navigation ) ? $labels->items_list_navigation : null;
},
],
'itemsList' => [
'type' => Types::string(),
'description' => __( 'Label for the table hidden heading.', 'wp-graphql' ),
'resolve' => function( $labels ) {
return ! empty( $labels->items_list ) ? $labels->items_list : null;
},
],
];
return self::prepare_fields( $fields, 'LabelsDetails' );
},
] );
} // End if().
return ! empty( self::$labels_details ) ? self::$labels_details : null;
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace WPGraphQL\Type;
use WPGraphQL\Type\MediaItem\Mutation\MediaItemCreate;
use WPGraphQL\Type\MediaItem\Mutation\MediaItemUpdate;
use WPGraphQL\Type\MediaItem\Mutation\MediaItemDelete;
use WPGraphQL\Type\PostObject\Mutation\PostObjectCreate;
use WPGraphQL\Type\PostObject\Mutation\PostObjectDelete;
use WPGraphQL\Type\PostObject\Mutation\PostObjectUpdate;
use WPGraphQL\Type\PostObject\Mutation\TermObjectDelete;
use WPGraphQL\Type\Settings\Mutation\SettingsUpdate;
use WPGraphQL\Type\TermObject\Mutation\TermObjectCreate;
use WPGraphQL\Type\TermObject\Mutation\TermObjectUpdate;
use WPGraphQL\Type\User\Mutation\UserCreate;
use WPGraphQL\Type\User\Mutation\UserDelete;
use WPGraphQL\Type\User\Mutation\UserUpdate;
/**
* Class RootMutationType
* The RootMutationType is the primary entry point for Mutations in the GraphQL Schema
*
* @package WPGraphQL\Type
* @since 0.0.8
*/
class RootMutationType extends WPObjectType {
/**
* Holds the $fields definition for the PluginType
*
* @var $fields
*/
private static $fields;
/**
* Holds the type name
*
* @var string $type_name
*/
private static $type_name;
/**
* RootMutationType constructor.
*/
public function __construct() {
self::$type_name = 'rootMutation';
/**
* Configure the rootMutation
*/
$config = [
'name' => self::$type_name,
'description' => __( 'The root mutation', 'wp-graphql' ),
'fields' => self::fields(),
];
/**
* Pass the config to the parent construct
*/
parent::__construct( $config );
}
/**
* This defines the fields for the RootMutationType. The fields are passed through a filter so the shape of the
* schema can be modified, for example to add entry points to Types that are unique to certain plugins.
*
* @return array|\GraphQL\Type\Definition\FieldDefinition[]
*/
private static function fields() {
if ( null === self::$fields ) {
$fields = [];
$allowed_post_types = \WPGraphQL::$allowed_post_types;
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $post_type ) {
/**
* Get the post_type object to pass down to the schema
*
* @since 0.0.5
*/
$post_type_object = get_post_type_object( $post_type );
if ( 'mediaItem' === $post_type_object->graphql_single_name ) {
$fields[ 'create' . ucwords( $post_type_object->graphql_single_name ) ] = MediaItemCreate::mutate( $post_type_object );
$fields[ 'update' . ucwords( $post_type_object->graphql_single_name ) ] = MediaItemUpdate::mutate( $post_type_object );
$fields[ 'delete' . ucwords( $post_type_object->graphql_single_name ) ] = MediaItemDelete::mutate( $post_type_object );
} else {
/**
* Root mutation for single posts (of the specified post_type)
*
* @since 0.0.5
*/
$fields[ 'create' . ucwords( $post_type_object->graphql_single_name ) ] = PostObjectCreate::mutate( $post_type_object );
$fields[ 'update' . ucwords( $post_type_object->graphql_single_name ) ] = PostObjectUpdate::mutate( $post_type_object );
$fields[ 'delete' . ucwords( $post_type_object->graphql_single_name ) ] = PostObjectDelete::mutate( $post_type_object );
}
} // End foreach().
} // End if().
/**
* Root mutation field for updating settings
*/
$fields[ 'updateSettings' ] = SettingsUpdate::mutate();
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
/**
* Get the taxonomy object to pass down to the schema
*/
$taxonomy_object = get_taxonomy( $taxonomy );
/**
* Root mutation for single term objects (of the specified taxonomy)
*/
$fields[ 'create' . ucwords( $taxonomy_object->graphql_single_name ) ] = TermObjectCreate::mutate( $taxonomy_object );
$fields[ 'update' . ucwords( $taxonomy_object->graphql_single_name ) ] = TermObjectUpdate::mutate( $taxonomy_object );
$fields[ 'delete' . ucwords( $taxonomy_object->graphql_single_name ) ] = TermObjectDelete::mutate( $taxonomy_object );
}
} // End if().
/**
* User Mutations
*/
$fields[ 'createUser' ] = UserCreate::mutate();
$fields[ 'updateUser' ] = UserUpdate::mutate();
$fields[ 'deleteUser' ] = UserDelete::mutate();
self::$fields = $fields;
} // End if().
/**
* Pass the fields through a filter to allow for hooking in and adjusting the shape
* of the type's schema
*/
return self::prepare_fields( self::$fields, self::$type_name );
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace WPGraphQL\Type;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\Comment\CommentQuery;
use WPGraphQL\Type\Comment\Connection\CommentConnectionDefinition;
use WPGraphQL\Type\Setting\SettingQuery;
use WPGraphQL\Type\Settings\SettingsQuery;
use WPGraphQL\Type\Plugin\Connection\PluginConnectionDefinition;
use WPGraphQL\Type\Plugin\PluginQuery;
use WPGraphQL\Type\PostObject\PostObjectQuery;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionDefinition;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionDefinition;
use WPGraphQL\Type\TermObject\TermObjectQuery;
use WPGraphQL\Type\Theme\Connection\ThemeConnectionDefinition;
use WPGraphQL\Type\User\Connection\UserConnectionDefinition;
use WPGraphQL\Type\User\UserQuery;
use WPGraphQL\Types;
/**
* Class RootQueryType
* The RootQueryType is the primary entry for Queries in the GraphQL Schema.
* @package WPGraphQL\Type
* @since 0.0.4
*/
class RootQueryType extends WPObjectType {
/**
* RootQueryType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Run an action when the RootQuery is being generated
* @since 0.0.5
*/
do_action( 'graphql_root_query' );
/**
* Configure the RootQuery
* @since 0.0.5
*/
$config = [
'name' => 'rootQuery',
'fields' => self::fields(),
];
/**
* Pass the config to the parent construct
* @since 0.0.5
*/
parent::__construct( $config );
}
public static function fields() {
/**
* Setup data
* @since 0.0.5
*/
$allowed_post_types = \WPGraphQL::$allowed_post_types;
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
$allowed_setting_types = DataSource::get_allowed_settings_by_group();
$node_definition = DataSource::get_node_definition();
/**
* Creates the node root query field which can be used
* to query any node from the system using the globally unique
* ID
* @since 0.0.5
*/
$fields['node'] = $node_definition['nodeField'];
/**
* Creates the comment root query field
* @since 0.0.5
*/
$fields['comment'] = CommentQuery::root_query();
$fields['comments'] = CommentConnectionDefinition::connection();
/**
* Creates the plugin root query field
* @since 0.0.5
*/
$fields['plugin'] = PluginQuery::root_query();
$fields['plugins'] = PluginConnectionDefinition::connection();
/**
* Create the root query fields for any setting type in
* the $allowed_setting_types array.
*/
if ( ! empty( $allowed_setting_types ) && is_array( $allowed_setting_types ) ) {
foreach ( $allowed_setting_types as $group => $setting_type ) {
$setting_type = str_replace('_', '', strtolower( $group ) );
$fields[ $setting_type . 'Settings' ] = SettingQuery::root_query( $group, $setting_type );
}
}
/**
* Creates the all settings root query field
*/
$fields['allSettings'] = SettingsQuery::root_query();
/**
* Creates the theme root query field
* @since 0.0.5
*/
$fields['theme'] = self::theme();
/**
* Creates the theme root query field to query a collection
* of themes
* @since 0.0.5
*/
$fields['themes'] = ThemeConnectionDefinition::connection();
/**
* Creates the user root query field
* @since 0.0.5
*/
$fields['user'] = UserQuery::root_query();
/**
* Creates the users root query field to query a collection
* of users
* @since 0.0.5
*/
$fields['users'] = UserConnectionDefinition::connection();
/**
* Creates the viewer root query field
* @since 0.0.5
*/
$fields['viewer'] = self::viewer();
/**
* Creates the root fields for post objects (of any post_type)
* This registers root fields (single and plural) for any post_type that has been registered as an
* allowed post_type.
* @see \WPGraphQL::$allowed_post_types
* @since 0.0.5
*/
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $post_type ) {
/**
* Get the post_type object to pass down to the schema
* @since 0.0.5
*/
$post_type_object = get_post_type_object( $post_type );
/**
* Root query for single posts (of the specified post_type)
* @since 0.0.5
*/
$fields[ $post_type_object->graphql_single_name ] = PostObjectQuery::root_query( $post_type_object );
$fields[ $post_type_object->graphql_single_name . 'By' ] = PostObjectQuery::post_object_by( $post_type_object );
/**
* Root query for collections of posts (of the specified post_type)
* @since 0.0.5
*/
$fields[ $post_type_object->graphql_plural_name ] = PostObjectConnectionDefinition::connection( $post_type_object );
}
}
/**
* Creates the root fields for terms of each taxonomy
* This registers root fields (single and plural) for terms of any taxonomy that has been registered as an
* allowed taxonomy.
* @see \WPGraphQL::$allowed_taxonomies
* @since 0.0.5
*/
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $taxonomy ) {
/**
* Get the taxonomy object
* @since 0.0.5
*/
$taxonomy_object = get_taxonomy( $taxonomy );
/**
* Root query for single terms (of the specified taxonomy)
* @since 0.0.5
*/
$fields[ $taxonomy_object->graphql_single_name ] = TermObjectQuery::root_query( $taxonomy_object );
/**
* Root query for collections of terms (of the specified taxonomy)
* @since 0.0.5
*/
$fields[ $taxonomy_object->graphql_plural_name ] = TermObjectConnectionDefinition::connection( $taxonomy_object );
}
}
/**
* Pass the root queries through a filter.
* This allows fields to be added or removed.
* NOTE: Use this filter with care. Before removing existing fields seriously consider deprecating the field, as
* that will allow the field to still be used and not break systems that rely on it, but just not be present
* in Schema documentation, etc.
* If the behavior of a field needs to be changed, depending on the change, it might be better to consider adding
* a new field with the new behavior instead of overriding an existing field. This will allow existing fields
* to behave as expected, but will allow introduction of new fields with different behavior at any point.
* @since 0.0.5
*/
$fields = apply_filters( 'graphql_root_queries', $fields );
/**
* Sort the fields alphabetically by keys
* (this makes the schema documentation much nicer to browse)
*/
ksort( $fields );
return $fields;
}
/**
* theme
* This sets up the theme entry point for the root query
* @return array
* @since 0.0.5
*/
public static function theme() {
return [
'type' => Types::theme(),
'description' => __( 'A Theme object', 'wp-graphql' ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, $context, ResolveInfo $info ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_theme( $id_components['id'] );
},
];
}
/**
* viewer
* This sets up the viewer entry point for the root query
* @return array
* @since 0.0.5
*/
public static function viewer() {
return [
'type' => Types::user(),
'description' => __( 'Returns the current user', 'wp-graphql' ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
return ( false !== $context->viewer->ID ) ? DataSource::resolve_user( $context->viewer->ID ) : null;
},
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WPGraphQL\Type\Setting;
use WPGraphQL\Types;
/**
* Class SettingQuery
*
* @package WPGraphQL\Type\Setting
*/
class SettingQuery {
/**
* Holds the root_query field definition
*
* @var array $root_query
* @access private
*/
private static $root_query;
/**
* Method that returns the root query field definition
* for the requested setting type
*
* @access public
* @param string $group
* @param array $setting_type
*
* @return array $root_query
*/
public static function root_query( $group, $setting_type ) {
if ( null === self::$root_query ) {
self::$root_query = [];
}
if ( ! empty( $setting_type ) && empty( self::$root_query[ $group ] ) ) {
self::$root_query = [
'type' => Types::setting( $group ),
'resolve' => function () use ( $setting_type ) {
return $setting_type;
},
];
}
return self::$root_query;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WPGraphQL\Type\Setting;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* class SettingType
*
* This sets up the base setting type for setting queries
*
* @package WPGraphQL\Type\Setting
*/
class SettingType extends WPObjectType {
/**
* Holds the $fields definition
*
* @var array $fields
* @access private
*/
private static $fields;
/**
* Holds the $setting_group definition
*
* @var string $setting_group
* @access private
*/
private static $setting_group;
/**
* SettingType constructor.
*
* @param string $setting_group The setting group name
* @access public
*/
public function __construct( $setting_group ) {
/**
* Set the setting_type so we can use it in $fields
*/
self::$setting_group = $setting_group;
/**
* Retrieve all of the settings that are categorized under the $setting_type
* and set them as the $setting_fields for later use in building fields
*/
$setting_fields = DataSource::get_setting_group_fields( $setting_group );
$config = [
'name' => ucfirst( $setting_group ) . 'Settings',
'description' => sprintf( __( 'The %s setting type', 'wp-graphql' ), $setting_group ),
'fields' => self::fields( $setting_fields ),
];
parent::__construct( $config );
}
/**
* This defines the fields (various settings) for a given setting group
*
* @param $setting_fields
*
* @access private
* @return \GraphQL\Type\Definition\FieldDefinition|mixed|null
*/
private static function fields( $setting_fields ) {
/**
* Set $fields to an empty array so that we aren't storing values
* from another setting_type
*/
$fields = [];
if ( ! empty( $setting_fields ) && is_array( $setting_fields ) ) {
foreach ( $setting_fields as $key => $setting_field ) {
/**
* Determine if the individual setting already has a
* REST API name, if not use the option name.
* Then, sanitize the field name to be camelcase
*/
if ( ! empty( $setting_field['show_in_rest']['name'] ) ) {
$field_key = $setting_field['show_in_rest']['name'];
} else {
$field_key = $key;
}
$field_key = lcfirst( str_replace( '_', '', ucwords( $field_key, '_' ) ) );
if ( ! empty( $key ) && ! empty( $field_key ) ) {
/**
* Dynamically build the individual setting and it's fields
* then add it to the fields array
*/
$fields[ $field_key ] = [
'type' => Types::get_type( $setting_field['type'] ),
'description' => $setting_field['description'],
'resolve' => function( $root, $args, AppContext $context, ResolveInfo $info ) use ( $setting_field, $field_key, $key ) {
/**
* Check to see if the user querying the email field has the 'manage_options' capability
* All other options should be public by default
*/
if ( 'admin_email' === $setting_field['key'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
throw new UserError( __( 'Sorry, you do not have permission to view this setting.', 'wp-graphql' ) );
}
}
$option = ! empty( $setting_field['key'] ) ? get_option( $setting_field['key'] ) : null;
switch ( $setting_field['type'] ) {
case 'integer':
$option = absint( $option );
break;
case 'string':
$option = (string) $option;
break;
case 'boolean':
$option = (boolean) $option;
break;
case 'number':
$option = (float) $option;
break;
}
return ! empty( $option ) ? $option : '';
},
];
}
}
/**
* Pass the fields through a filter to allow for hooking in and adjusting the shape
* of the type's schema
*/
self::$fields = self::prepare_fields( $fields, self::$setting_group );
}
return ! empty( self::$fields ) ? self::$fields : null;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace WPGraphQL\Type\Settings\Mutation;
use WPGraphQL\Types;
use WPGraphQL\Data\DataSource;
/**
* Class SettingsMutation
*
* @package WPGraphQL\Type\Settings
*/
class SettingsMutation {
/**
* Holds the input fields configuration
*/
private static $input_fields;
/**
* The input fields for the settings mutation
*
* @return mixed|array|null $input_fields
*/
public static function input_fields() {
/**
* Retrieve all of the allowed settings
*/
$allowed_settings = DataSource::get_allowed_settings();
$input_fields = [];
if ( ! empty( $allowed_settings ) && empty( self::$input_fields ) ) {
/**
* Loop through the $allowed_settings and build fields
* for the individual settings
*/
foreach ( $allowed_settings as $key => $setting ) {
/**
* Determine if the individual setting already has a
* REST API name, if not use the option name.
* Sanitize the field name to be camelcase
*/
if ( ! empty( $setting['show_in_rest']['name'] ) ) {
$individual_setting_key = lcfirst( $setting['group'] . 'Settings' . str_replace( '_', '', ucwords( $setting['show_in_rest']['name'], '_' ) ) );
} else {
$individual_setting_key = lcfirst( $setting['group'] . 'Settings' . str_replace( '_', '', ucwords( $key, '_' ) ) );
}
/**
* Dynamically build the individual setting,
* then add it to the $input_fields
*/
$input_fields[ $individual_setting_key ] = [
'type' => Types::get_type( $setting['type'] ),
'description' => $setting['description'],
];
}
self::$input_fields = apply_filters( 'graphql_setting_mutation_input_fields', $input_fields );
}
return ( ! empty( self::$input_fields ) ? self::$input_fields : null );
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace WPGraphQL\Type\Settings\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Language\AST\Type;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\Setting\SettingQuery;
use WPGraphQL\Type\Settings\SettingsQuery;
/**
* Class SettingsUpdate
*
* @package WPGraphQL\Type\Settings\Mutation
*/
class SettingsUpdate {
/**
* Stores the UpdateSettings mutation field definition
*
* @var array $mutation
*/
private static $mutation;
/**
* Define the UpdateSettings mutation
*
* @access public
*
* @return array|mixed
*/
public static function mutate() {
if ( empty( self::$mutation ) ) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'UpdateSettings';
self::$mutation = Relay::mutationWithClientMutationId( [
'name' => $mutation_name,
'description' => __( 'Update any of the various settings.', 'wp-graphql' ),
'inputFields' => SettingsMutation::input_fields(),
'outputFields' => self::output_fields(),
'mutateAndGetPayload' => function ( $input ) {
/**
* Check that the user can manage setting options
*/
if ( ! current_user_can( 'manage_options' ) ) {
throw new UserError( __( 'Sorry, you are not allowed to edit settings as this user.', 'wp-graphql' ) );
}
/**
* The $updatable_settings_options will store all of the allowed
* settings in a WP ready format
*/
$updatable_settings_options = [];
$allowed_settings = DataSource::get_allowed_settings();
/**
* Loop through the $allowed_settings and build the insert options array
*/
foreach ( $allowed_settings as $key => $setting ) {
/**
* Determine if the individual setting already has a
* REST API name, if not use the option name.
* Sanitize the field name to be camelcase
*/
if ( ! empty( $setting['show_in_rest']['name'] ) ) {
$individual_setting_key = lcfirst( $setting['group'] . 'Settings' . str_replace( '_', '', ucwords( $setting['show_in_rest']['name'], '_' ) ) );
} else {
$individual_setting_key = lcfirst( $setting['group'] . 'Settings' . str_replace( '_', '', ucwords( $key, '_' ) ) );
}
/**
* Dynamically build the individual setting,
* then add it to $updatable_settings_options
*/
$updatable_settings_options[ $individual_setting_key ] = [
'option' => $key,
'group' => $setting['group'],
];
}
foreach ( $input as $key => $value ) {
/**
* Throw an error if the input field is the site url,
* as we do not want users changing it and breaking all
* the things
*/
if ( 'generalSettingsUrl' === $key ) {
throw new UserError( __( 'Sorry, that is not allowed, speak with your site administrator to change the site URL.', 'wp-graphql' ) );
}
/**
* Check to see that the input field exists in settings, if so grab the option
* name and update the option
*/
if ( array_key_exists( $key, $updatable_settings_options ) ) {
update_option( $updatable_settings_options[ $key ]['option'], $value );
}
}
},
] );
}
return ! empty( self::$mutation ) ? self::$mutation : null;
}
/**
* Build the output of the UpdateSettings mutation.
* This will build a combination of the setting and settings queries
* so that the user can query the returned data by setting group or field
*
* @access protected
*
* @return array $output_fields
*/
protected static function output_fields() {
$output_fields = [];
/**
* Get the allowed setting groups and their fields
*/
$allowed_setting_groups = DataSource::get_allowed_settings_by_group();
if ( ! empty( $allowed_setting_groups ) && is_array( $allowed_setting_groups ) ) {
foreach ( $allowed_setting_groups as $group => $setting_type ) {
$setting_type = str_replace('_', '', strtolower( $group ) );
$output_fields[ $setting_type . 'Settings' ] = SettingQuery::root_query( $group, $setting_type );
}
}
/**
* Get all of the settings, regardless of group
*/
$output_fields['allSettings'] = SettingsQuery::root_query();
return $output_fields;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WPGraphQL\Type\Settings;
use WPGraphQL\Types;
/**
* Class SettingsQuery
*
* @package WPGraphQL\Type\Settings
*/
class SettingsQuery {
/**
* Holds the root_query field definition
*
* @var array $root_query
* @access private
*/
private static $root_query;
/**
* Method that returns the root query field definition
* for all settings
*
* @access public
*
* @return array $root_query
*/
public static function root_query() {
if ( null === self::$root_query ) {
self::$root_query = [];
}
self::$root_query = [
'type' => Types::settings(),
'resolve' => function () {
return true;
},
];
return self::$root_query;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WPGraphQL\Type\Settings;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class SettingsType
*
* This sets up the base settings Type for settings queries and mutations
*
* @package WPGraphQL\Type\Settings
*/
class SettingsType extends WPObjectType {
/**
* Holds the type name
*
* @var string $type_name
*/
private static $type_name;
/**
* Holds the $fields definition for the SettingsType
*
* @var array $fields
* @access private
*/
private static $fields;
/**
* SettingsType constructor.
*
* @access public
*/
public function __construct() {
/**
* Set the type_name
*
* @since 0.0.5
*/
self::$type_name = 'Settings';
/**
* Retrieve all of the allowed settings
*/
$settings_array = DataSource::get_allowed_settings();
$config = [
'name' => self::$type_name,
'fields' => self::fields( $settings_array ),
'description' => __( 'All of the registered settings', 'wp-graphql' ),
];
parent::__construct( $config );
}
/**
* This defines the fields for the settings type
*
* @param $settings_array
*
* @access private
* @return \GraphQL\Type\Definition\FieldDefinition|mixed|null
*/
private static function fields( $settings_array ) {
/**
* Define $fields
*/
$fields = [];
if ( ! empty( $settings_array ) && is_array( $settings_array ) ) {
/**
* Loop through the $settings_array and build the setting with
* proper fields
*/
foreach ( $settings_array as $key => $setting_field ) {
/**
* Determine if the individual setting already has a
* REST API name, if not use the option name.
* Then, sanitize the field name to be camelcase
*/
if ( ! empty( $setting_field['show_in_rest']['name'] ) ) {
$field_key = $setting_field['show_in_rest']['name'];
} else {
$field_key = $key;
}
$field_key = lcfirst( $setting_field['group'] . 'Settings' . str_replace( '_', '', ucwords( $field_key, '_' ) ) );
if ( ! empty( $key ) && ! empty( $field_key ) ) {
/**
* Dynamically build the individual setting and it's fields
* then add it to $fields
*/
$fields[ $field_key ] = [
'type' => Types::get_type( $setting_field['type'] ),
'description' => $setting_field['description'],
'resolve' => function( $root, $args, AppContext $context, ResolveInfo $info ) use ( $setting_field, $field_key, $key ) {
/**
* Check to see if the user querying the email field has the 'manage_options' capability
* All other options should be public by default
*/
if ( 'admin_email' === $key && ! current_user_can( 'manage_options' ) ) {
throw new UserError( __( 'Sorry, you do not have permission to view this setting.', 'wp-graphql' ) );
}
$option = ! empty( $key ) ? get_option( $key ) : null;
switch ( $setting_field['type'] ) {
case 'integer':
$option = absint( $option );
break;
case 'string':
$option = (string) $option;
break;
case 'boolean':
$option = (boolean) $option;
break;
case 'number':
$option = (float) $option;
break;
}
return $option;
},
];
}
}
/**
* Pass the fields through a filter to allow for hooking in and adjusting the shape
* of the type's schema
*/
self::$fields = self::prepare_fields( $fields, self::$type_name );
}
return ! empty( self::$fields ) ? self::$fields : null;
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace WPGraphQL\Type\Taxonomy;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class TaxonomyType
* @package WPGraphQL\Type
* @since 0.0.5
*/
class TaxonomyType extends WPObjectType {
/**
* Holds the type name
* @var string $type_name
*/
private static $type_name;
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
private static $fields;
/**
* TaxonomyType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
* @since 0.0.5
*/
self::$type_name = 'Taxonomy';
$config = [
'name' => self::$type_name,
'description' => __( 'A taxonomy object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields for the TaxonomyType. The fields are passed through a filter so the shape of the schema
* can be modified
*
* @return array|\GraphQL\Type\Definition\FieldDefinition[]
* @since 0.0.5
*/
private function fields() {
if ( null === self::$fields ) {
/**
* Get the post_types that are allowed in WPGraphQL
*
* @since 0.0.6
*/
$allowed_post_types = \WPGraphQL::$allowed_post_types;
self::$fields = function() use ( $allowed_post_types ) {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'resolve' => function( $taxonomy, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $info->parentType ) && ! empty( $taxonomy->name ) ) ? Relay::toGlobalId( 'taxonomy', $taxonomy->name ) : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'The display name of the taxonomy. This field is equivalent to WP_Taxonomy->label', 'wp-graphql' ),
],
'label' => [
'type' => Types::string(),
'description' => __( 'Name of the taxonomy shown in the menu. Usually plural.', 'wp-graphql' ),
],
//@todo: add "labels" field
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the taxonomy. This field is equivalent to WP_Taxonomy->description', 'wp-graphql' ),
],
'public' => [
'type' => Types::boolean(),
'description' => __( 'Whether the taxonomy is publicly queryable', 'wp-graphql' ),
],
'hierarchical' => [
'type' => Types::boolean(),
'description' => __( 'Whether the taxonomy is hierarchical', 'wp-graphql' ),
],
'showUi' => [
'type' => Types::boolean(),
'description' => __( 'Whether to generate and allow a UI for managing terms in this taxonomy in the admin', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_ui ) ? true : false;
},
],
'showInMenu' => [
'type' => Types::boolean(),
'description' => __( 'Whether to show the taxonomy in the admin menu', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_in_menu ) ? true : false;
},
],
'showInNavMenus' => [
'type' => Types::boolean(),
'description' => __( 'Whether the taxonomy is available for selection in navigation menus.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_in_nav_menus ) ? true : false;
},
],
'showCloud' => [
'type' => Types::boolean(),
'description' => __( 'Whether to show the taxonomy as part of a tag cloud widget. This field is equivalent to WP_Taxonomy->show_tagcloud', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_tagcloud ) ? true : false;
},
],
'showInQuickEdit' => [
'type' => Types::boolean(),
'description' => __( 'Whether to show the taxonomy in the quick/bulk edit panel.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_in_quick_edit ) ? true : false;
},
],
'showInAdminColumn' => [
'type' => Types::boolean(),
'description' => __( 'Whether to display a column for the taxonomy on its post type listing screens.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_admin_column ) ? true : false;
},
],
'showInRest' => [
'type' => Types::boolean(),
'description' => __( 'Whether to add the post type route in the REST API `wp/v2` namespace.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_in_rest ) ? true : false;
},
],
'restBase' => [
'type' => Types::string(),
'description' => __( 'Name of content type to diplay in REST API `wp/v2` namespace.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : null;
},
],
'restControllerClass' => [
'type' => Types::string(),
'description' => __( 'The REST Controller class assigned to handling this content type.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : null;
},
],
'showInGraphql' => [
'type' => Types::boolean(),
'description' => __( 'Whether to add the post type to the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ( true === $taxonomy->show_in_graphql ) ? true : false;
},
],
'graphqlSingleName' => [
'type' => Types::string(),
'description' => __( 'The singular name of the post type within the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $taxonomy->graphql_single_name ) ? $taxonomy->graphql_single_name : null;
},
],
'graphqlPluralName' => [
'type' => Types::string(),
'description' => __( 'The plural name of the post type within the GraphQL Schema.', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $taxonomy->graphql_plural_name ) ? $taxonomy->graphql_plural_name : null;
},
],
'connectedPostTypeNames' => [
'type' => Types::list_of( Types::string() ),
'args' => [
'types' => [
'type' => Types::list_of( Types::post_type_enum() ),
'description' => __( 'Select which post types to limit the results to', 'wp-graphql' ),
],
],
'description' => __( 'A list of Post Types associated with the taxonomy', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) use ( $allowed_post_types ) {
$post_type_names = [];
/**
* If the types $arg is populated, use that to filter the $allowed_post_types,
* otherwise use the default $allowed_post_types passed down
*/
$allowed_post_types = ! empty( $args['types'] ) && is_array( $args['types'] ) ? $args['types'] : $allowed_post_types;
$connected_post_types = $taxonomy->object_type;
if ( ! empty( $connected_post_types ) && is_array( $connected_post_types ) ) {
foreach ( $connected_post_types as $post_type ) {
if ( in_array( $post_type, $allowed_post_types, true ) ) {
$post_type_names[] = $post_type;
}
}
}
return ! empty( $post_type_names ) ? $post_type_names : null;
},
],
'connectedPostTypes' => [
'type' => Types::list_of( Types::post_type() ),
'args' => [
'types' => [
'type' => Types::list_of( Types::post_type_enum() ),
'description' => __( 'Select which post types to limit the results to', 'wp-graphql' ),
],
],
'description' => __( 'List of Post Types connected to the Taxonomy', 'wp-graphql' ),
'resolve' => function( \WP_Taxonomy $taxonomy, array $args, AppContext $context, ResolveInfo $info ) use ( $allowed_post_types ) {
$post_type_objects = [];
/**
* If the types $arg is populated, use that to filter the $allowed_post_types,
* otherwise use the default $allowed_post_types passed down
*/
$allowed_post_types = ! empty( $args['types'] ) && is_array( $args['types'] ) ? $args['types'] : $allowed_post_types;
$connected_post_types = ! empty( $taxonomy->object_type ) ? $taxonomy->object_type : [];
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $post_type ) {
if ( in_array( $post_type, $connected_post_types, true ) ) {
$post_type_object = get_post_type_object( $post_type );
$post_type_objects[ $post_type_object->graphql_single_name ] = $post_type_object;
}
}
}
return ! empty( $post_type_objects ) ? $post_type_objects : null;
},
],
];
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace WPGraphQL\Type\TermObject\Connection;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class TermObjectConnectionArgs
*
* This sets up the Query Args for term object connections, which uses get_terms, so this defines the allowed
* input fields that will be passed to get_terms
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class TermObjectConnectionArgs extends WPInputObjectType {
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
public static $fields = [];
/**
* Holds the orderby enum definition
* @var $orderby_enum
*/
protected static $orderby_enum;
/**
* TermObjectConnectionArgs constructor.
*
* @param array $config Array of config details for the Input Object Type
* @param string $connection The name of the connection the args belong to
*
* @since 0.0.5
*/
public function __construct( $config = [], $connection ) {
$config['name'] = ucfirst( $connection ) . 'TermArgs';
$config['queryClass'] = 'WP_Term_Query';
$config['fields'] = self::fields( $connection );
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the TermObjectConnectionArgs
*
* @param string $connection The name of the connection that the fields args belong to
* @return array
* @since 0.0.5
*/
private static function fields( $connection ) {
if ( empty( self::$fields[ $connection ] ) ) {
self::$fields[ $connection ] = [
'objectIds' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of object IDs. Results will be limited to terms associated with these objects.', 'wp-graphql' ),
],
'orderby' => [
'type' => self::orderby_enum(),
'description' => __( 'Field(s) to order terms by. Defaults to \'name\'.', 'wp-graphql' ),
],
'hideEmpty' => [
'type' => Types::boolean(),
'description' => __( 'Whether to hide terms not assigned to any posts. Accepts true or false. Default true', 'wp-graphql' ),
],
'include' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of term ids to include. Default empty array.', 'wp-graphql' ),
],
'exclude' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of term ids to exclude. If $include is non-empty, $exclude is ignored. Default empty array.', 'wp-graphql' ),
],
'excludeTree' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of term ids to exclude along with all of their descendant terms. If $include is non-empty, $exclude_tree is ignored. Default empty array.', 'wp-graphql' ),
],
'name' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Array of names to return term(s) for. Default empty.', 'wp-graphql' ),
],
'slug' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'Array of slugs to return term(s) for. Default empty.', 'wp-graphql' ),
],
'termTaxonomId' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of term taxonomy IDs, to match when querying terms.', 'wp-graphql' ),
],
'hierarchical' => [
'type' => Types::boolean(),
'description' => __( 'Whether to include terms that have non-empty descendants (even if $hide_empty is set to true). Default true.', 'wp-graphql' ),
],
'search' => [
'type' => Types::string(),
'description' => __( 'Search criteria to match terms. Will be SQL-formatted with wildcards before and after. Default empty.', 'wp-graphql' ),
],
'nameLike' => [
'type' => Types::string(),
'description' => __( 'Retrieve terms with criteria by which a term is LIKE `$name__like`. Default empty.', 'wp-graphql' ),
],
'descriptionLike' => [
'type' => Types::string(),
'description' => __( 'Retrieve terms where the description is LIKE `$description__like`. Default empty.', 'wp-graphql' ),
],
'padCounts' => [
'type' => Types::boolean(),
'description' => __( 'Whether to pad the quantity of a term\'s children in the quantity of each term\'s "count" object variable. Default false.', 'wp-graphql' ),
],
'childOf' => [
'type' => Types::int(),
'description' => __( 'Term ID to retrieve child terms of. If multiple taxonomies are passed, $child_of is ignored. Default 0.', 'wp-graphql' ),
],
'parent' => [
'type' => Types::int(),
'description' => __( 'Parent term ID to retrieve direct-child terms of. Default empty.', 'wp-graphql' ),
],
'childless' => [
'type' => Types::boolean(),
'description' => __( 'True to limit results to terms that have no children. This parameter has no effect on non-hierarchical taxonomies. Default false.', 'wp-graphql' ),
],
'cacheDomain' => [
'type' => Types::string(),
'description' => __( 'Unique cache key to be produced when this query is stored in an object cache. Default is \'core\'.', 'wp-graphql' ),
],
'updateTermMetaCache' => [
'type' => Types::boolean(),
'description' => __( 'Whether to prime meta caches for matched terms. Default true.', 'wp-graphql' ),
],
];
/**
* Add these fields to non-root connections
*
* @todo: possibly consider only adding these args to certain connections, like non-Root connections?
*/
self::$fields[ $connection ]['shouldOnlyIncludeConnectedItems'] = [
'type' => Types::boolean(),
'description' => __( 'Default false. If true, only the items connected to the source item will be returned. If false, all items will be returned regardless of connection to the source', 'wp-graphql' ),
];
self::$fields[ $connection ]['shouldOutputInFlatList'] = [
'type' => Types::boolean(),
'description' => __( 'Default false. If true, the connection will be output in a flat list instead of the hierarchical list. So child terms will be output in the same level as the parent terms', 'wp-graphql' ),
];
}
return ! empty( self::$fields[ $connection ] ) ? self::prepare_fields( self::$fields[ $connection ], ucfirst( $connection ) . 'TermArgs' ) : null;
}
/**
* Sets up the definition of the TermsOrderby enum
* @return null|WPEnumType
*/
protected static function orderby_enum() {
if ( null === self::$orderby_enum ) {
self::$orderby_enum = new WPEnumType( [
'name' => 'TermsOrderby',
'values' => [
'NAME' => [
'value' => 'name',
],
'SLUG' => [
'value' => 'slug',
],
'TERM_GROUP' => [
'value' => 'term_group',
],
'TERM_ID' => [
'value' => 'term_id',
],
'TERM_ORDER' => [
'value' => 'term_order',
],
'DESCRIPTION' => [
'value' => 'description',
],
'COUNT' => [
'value' => 'count',
],
],
] );
}
return ! empty( self::$orderby_enum ) ? self::$orderby_enum : null;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace WPGraphQL\Type\TermObject\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class TermObjectConnectionDefinition
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class TermObjectConnectionDefinition {
/**
* Stores some date for the Relay connection for term objects
*
* @var array $connection
* @since 0.0.5
* @access private
*/
private static $connection = [];
/**
* Method that sets up the relay connection for term objects
*
* @param object $taxonomy_object
* @return mixed
* @since 0.0.5
*/
public static function connection( $taxonomy_object, $from_type = 'Root' ) {
if ( empty( self::$connection[ $from_type ][ $taxonomy_object->name ] ) ) {
/**
* Setup the connectionDefinition
*
* @since 0.0.5
*/
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::term_object( $taxonomy_object->name ),
'name' => ucfirst( $from_type ) . ucfirst( $taxonomy_object->graphql_plural_name ),
'connectionFields' => function() use ( $taxonomy_object ) {
return [
'taxonomyInfo' => [
'type' => Types::taxonomy(),
'description' => __( 'Information about the type of content being queried', 'wp-graphql' ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $taxonomy_object ) {
return $taxonomy_object;
},
],
'nodes' => [
'type' => Types::list_of( Types::term_object( $taxonomy_object->name ) ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the "where" args to the termObjectConnections
*
* @since 0.0.5
*/
$args[ $from_type ] = [
'where' => [
'name' => 'where',
'type' => Types::term_object_query_args( ucfirst( $from_type ) . ucfirst( $taxonomy_object->graphql_plural_name ) ),
],
];
/**
* Add the connection to the post_objects_connection object
*
* @since 0.0.5
*/
self::$connection[ $from_type ][ $taxonomy_object->name ] = [
'type' => $connection['connectionType'],
'description' => sprintf( __( 'A collection of %s objects', 'wp-graphql' ), $taxonomy_object->graphql_plural_name ),
'args' => array_merge( Relay::connectionArgs(), $args[ $from_type ] ),
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $taxonomy_object ) {
return DataSource::resolve_term_objects_connection( $source, $args, $context, $info, $taxonomy_object->name );
},
];
}
return self::$connection[ $from_type ][ $taxonomy_object->name ];
}
}

View File

@@ -0,0 +1,336 @@
<?php
namespace WPGraphQL\Type\TermObject\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Connection\ArrayConnection;
use WPGraphQL\AppContext;
use WPGraphQL\Data\ConnectionResolver;
use WPGraphQL\Types;
/**
* Class TermObjectConnectionResolver
*
* @package WPGraphQL\Data\Resolvers
* @since 0.0.5
*/
class TermObjectConnectionResolver extends ConnectionResolver {
/**
* Stores the name of the taxonomy for the connection being resolved
*
* @var string $taxonomy
*/
public static $taxonomy;
/**
* TermObjectConnectionResolver constructor.
*
* @param $taxonomy
*/
public function __construct( $taxonomy ) {
self::$taxonomy = $taxonomy;
}
/**
* Returns an array of query_args to use in the WP_Term_Query to fetch the necessary terms for the connection
*
* @param $source
* @param array $args
* @param AppContext $context
* @param ResolveInfo $info
*
* @return array
*/
public static function get_query_args( $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Set the taxonomy for the $args
*/
$query_args['taxonomy'] = ! empty( self::$taxonomy ) ? self::$taxonomy : 'category';
/**
* Prepare for later use
*/
$last = ! empty( $args['last'] ) ? $args['last'] : null;
$first = ! empty( $args['first'] ) ? $args['first'] : null;
/**
* Set the default parent for TermObject Queries to be "0" to only get top level terms, unless
* includeChildren is set
*/
$query_args['parent'] = 0;
/**
* Set hide_empty as false by default
*/
$query_args['hide_empty'] = false;
/**
* Set the number, ensuring it doesn't exceed the amount set as the $max_query_amount
*/
$query_args['number'] = min( max( absint( $first ), absint( $last ), 10 ), self::get_query_amount( $source, $args, $context, $info ) ) + 1;
/**
* Orderby Name by default
*/
$query_args['orderby'] = 'name';
/**
* Take any of the $args that were part of the GraphQL query and map their
* GraphQL names to the WP_Term_Query names to be used in the WP_Term_Query
*
* @since 0.0.5
*/
$input_fields = [];
if ( ! empty( $args['where'] ) ) {
$input_fields = self::sanitize_input_fields( $args['where'], $source, $args, $context, $info );
}
/**
* Merge the default $query_args with the $args that were entered
* in the query.
*
* @since 0.0.5
*/
if ( ! empty( $input_fields ) ) {
$query_args = array_merge( $query_args, $input_fields );
}
/**
* If there's no orderby params in the inputArgs, set order based on the first/last argument
*/
if ( empty( $query_args['order'] ) ) {
$query_args['order'] = ! empty( $last ) ? 'DESC' : 'ASC';
}
/**
* Set the graphql_cursor_offset
*/
$query_args['graphql_cursor_offset'] = self::get_offset( $args );
$query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<';
/**
* Pass the graphql $args to the WP_Query
*/
$query_args['graphql_args'] = $args;
/**
* If the source of the Query is a Post object, adjust the query args to only query terms
* connected to the post object
*
* @since 0.0.5
*/
global $post;
if ( true === is_object( $source ) ) {
switch ( true ) {
case $source instanceof \WP_Post:
$post = $source;
$post->shouldOnlyIncludeConnectedItems = isset( $input_fields['shouldOnlyIncludeConnectedItems'] ) ? $input_fields['shouldOnlyIncludeConnectedItems'] : true;
$query_args['object_ids'] = $source->ID;
break;
case $source instanceof \WP_Term:
$query_args['object_ids'] = $GLOBALS['post']->ID;
$query_args['parent'] = ! empty( $source->term_id ) ? $source->term_id : 0;
break;
default:
break;
}
}
/**
* IF the connection is set to NOT ONLY include connected items (default behavior), unset the $object_ids arg
*/
if ( isset( $post->shouldOnlyIncludeConnectedItems ) && false === $post->shouldOnlyIncludeConnectedItems ) {
unset( $query_args['object_ids'] );
}
/**
* If the connection is set to output in a flat list, unset the parent
*/
if ( isset( $input_fields['shouldOutputInFlatList'] ) && true === $input_fields['shouldOutputInFlatList'] ){
unset( $query_args['parent'] );
$connected = wp_get_object_terms( $source->ID, self::$taxonomy, ['fields' => 'ids'] );
$query_args['include'] = ! empty( $connected ) ? $connected : [];
}
/**
* Filter the query_args that should be applied to the query. This filter is applied AFTER the input args from
* the GraphQL Query have been applied and has the potential to override the GraphQL Query Input Args.
*
* @param array $query_args array of query_args being passed to the
* @param mixed $source source passed down from the resolve tree
* @param array $args array of arguments input in the field as part of the GraphQL query
* @param AppContext $context object passed down the resolve tree
* @param ResolveInfo $info info about fields passed down the resolve tree
*
* @since 0.0.6
*/
$query_args = apply_filters( 'graphql_term_object_connection_query_args', $query_args, $source, $args, $context, $info );
return $query_args;
}
/**
* This runs the query and returns the response
*
* @param $query_args
*
* @return \WP_Term_Query
*/
public static function get_query( $query_args ) {
$query = new \WP_Term_Query( $query_args );
return $query;
}
/**
* This gets the connection to return
*
* @param array|mixed $query The query that was processed to get data
* @param array $items The array slice that was returned
* @param mixed $source The source being passed down the resolve tress
* @param array $args The input args for the resolving field
* @param AppContext $context The context being passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the resolve tree
*
* @return array
*/
public static function get_connection( $query, array $items, $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Get the $posts from the query
*/
$items = ! empty( $items ) && is_array( $items ) ? $items : [];
/**
* Set whether there is or is not another page
*/
$has_previous_page = ( ! empty( $args['last'] ) && count( $items ) > self::get_amount_requested( $args ) ) ? true : false;
$has_next_page = ( ! empty( $args['first'] ) && count( $items ) > self::get_amount_requested( $args ) ) ? true : false;
/**
* Slice the array to the amount of items that were requested
*/
$items = array_slice( $items, 0, self::get_amount_requested( $args ) );
/**
* Get the edges from the $items
*/
$edges = self::get_edges( $items, $source, $args, $context, $info );
/**
* Find the first_edge and last_edge
*/
$first_edge = $edges ? $edges[0] : null;
$last_edge = $edges ? $edges[ count( $edges ) - 1 ] : null;
/**
* Create the connection to return
*/
$connection = [
'edges' => $edges,
'debug' => [
'queryRequest' => ! empty( $query->request ) ? $query->request : null,
],
'pageInfo' => [
'hasPreviousPage' => $has_previous_page,
'hasNextPage' => $has_next_page,
'startCursor' => ! empty( $first_edge['cursor'] ) ? $first_edge['cursor'] : null,
'endCursor' => ! empty( $last_edge['cursor'] ) ? $last_edge['cursor'] : null,
],
'nodes' => $items,
];
return $connection;
}
/**
* Takes an array of items and returns the edges
*
* @param $items
*
* @return array
*/
public static function get_edges( $items, $source, $args, $context, $info ) {
$edges = [];
/**
* If we're doing backward pagination we want to reverse the array before
* returning it to the edges
*/
if ( ! empty( $args['last'] ) ) {
$items = array_reverse( $items );
}
if ( ! empty( $items ) && is_array( $items ) ) {
foreach ( $items as $item ) {
$edges[] = [
'cursor' => ArrayConnection::offsetToCursor( $item->term_id ),
'node' => $item,
];
}
}
return $edges;
}
/**
* This maps the GraphQL "friendly" args to get_terms $args.
* There's probably a cleaner/more dynamic way to approach this, but this was quick. I'd be down
* to explore more dynamic ways to map this, but for now this gets the job done.
*
* @param array $args Array of query "where" args
* @param mixed $source The query results
* @param array $all_args All of the query arguments (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @return array
* @access public
*/
public static function sanitize_input_fields( array $args, $source, array $all_args, AppContext $context, ResolveInfo $info ) {
$arg_mapping = [
'objectIds' => 'object_ids',
'hideEmpty' => 'hide_empty',
'excludeTree' => 'exclude_tree',
'termTaxonomId' => 'term_taxonomy_id',
'nameLike' => 'name__like',
'descriptionLike' => 'description__like',
'padCounts' => 'pad_counts',
'childOf' => 'child_of',
'cacheDomain' => 'cache_domain',
'updateTermMetaCache' => 'update_term_meta_cache',
];
/**
* Map and sanitize the input args to the WP_Term_Query compatible args
*/
$query_args = Types::map_input( $args, $arg_mapping );
/**
* Filter the input fields
* This allows plugins/themes to hook in and alter what $args should be allowed to be passed
* from a GraphQL Query to the get_terms query
*
* @param array $query_args Array of mapped query args
* @param array $args Array of query "where" args
* @param string $taxonomy The name of the taxonomy
* @param mixed $source The query results
* @param array $all_args All of the query arguments (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @return array
*/
$query_args = apply_filters( 'graphql_map_input_fields_to_get_terms', $query_args, $args, self::$taxonomy, $source, $all_args, $context, $info );
return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace WPGraphQL\Type\TermObject\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
class TermObjectCreate {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the create mutation for TermObjects
*
* @param \WP_Taxonomy $taxonomy
*
* @return array|mixed
*/
public static function mutate( \WP_Taxonomy $taxonomy ) {
if (
! empty( $taxonomy->graphql_single_name ) &&
empty( self::$mutation[ $taxonomy->graphql_single_name ] )
) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'Create' . ucwords( $taxonomy->graphql_single_name );
self::$mutation[ $taxonomy->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
// translators: The placeholder is the name of the object type
'description' => sprintf( __( 'Create %1$s objects', 'wp-graphql' ), $taxonomy->name ),
'inputFields' => self::input_fields( $taxonomy ),
'outputFields' => [
$taxonomy->graphql_single_name => [
'type' => Types::term_object( $taxonomy->name ),
// translators: Placeholder is the name of the taxonomy
'description' => sprintf( __( 'The created %s', 'wp-graphql' ), $taxonomy->name ),
'resolve' => function( $payload ) use ( $taxonomy ) {
return get_term( $payload['id'], $taxonomy->name );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $taxonomy, $mutation_name ) {
/**
* Ensure the user can edit_terms
*/
if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
// translators: the $taxonomy->graphql_plural_name placeholder is the name of the object being mutated
throw new UserError( sprintf( __( 'Sorry, you are not allowed to create %1$s', 'wp-graphql' ), $taxonomy->graphql_plural_name ) );
}
/**
* Prepare the object for insertion
*/
$args = TermObjectMutation::prepare_object( $input, $taxonomy, $mutation_name );
/**
* Ensure a name was provided
*/
if ( empty( $args['name'] ) ) {
// Translators: The placeholder is the name of the taxonomy of the term being mutated
throw new UserError( sprintf( __( 'A name is required to create a %1$s' ), $taxonomy->name ) );
}
/**
* Insert the term
*/
$term = wp_insert_term( wp_slash( $args['name'] ), $taxonomy->name, wp_slash( (array) $args ) );
/**
* If it was an error, return the message as an exception
*/
if ( is_wp_error( $term ) ) {
$error_message = $term->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
throw new UserError( __( 'The object failed to update but no error was provided', 'wp-graphql' ) );
}
}
/**
* If the response to creating the term didn't respond with a term_id, throw an exception
*/
if ( empty( $term['term_id'] ) ) {
throw new UserError( __( 'The object failed to create', 'wp-graphql' ) );
}
/**
* Fires after a single term is created or updated via a GraphQL mutation
*
* The dynamic portion of the hook name, `$taxonomy->name` refers to the taxonomy of the term being mutated
*
* @param int $term_id Inserted term object
* @param array $args The args used to insert the term
* @param string $mutation_name The name of the mutation being performed
* @param AppContext $context The AppContext passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the resolve tree
*/
do_action( "graphql_insert_{$taxonomy->name}", $term['term_id'], $args, $mutation_name, $context, $info );
return [
'id' => $term['term_id'],
];
},
] );
}
return ! empty( self::$mutation[ $taxonomy->graphql_single_name ] ) ? self::$mutation[ $taxonomy->graphql_single_name ] : null;
}
/**
* Add the name as a nonNull field for create mutations
*
* @param \WP_Taxonomy $taxonomy
*
* @return array
*/
private static function input_fields( $taxonomy ) {
/**
* Add name as a non_null field for term creation
*/
return array_merge(
[
'name' => [
'type' => Types::non_null( Types::string() ),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The name of the %1$s object to mutate', 'wp-graphql' ), $taxonomy->name ),
],
],
TermObjectMutation::input_fields( $taxonomy )
);
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace WPGraphQL\Type\PostObject\Mutation;
use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
class TermObjectDelete {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the update mutation for TermObjects
*
* @param \WP_Taxonomy $taxonomy
*
* @return array|mixed
*/
public static function mutate( \WP_Taxonomy $taxonomy ) {
if (
! empty( $taxonomy->graphql_single_name ) &&
empty( self::$mutation[ $taxonomy->graphql_single_name ] )
) {
/**
* Set the name of the mutation being performed
*/
$mutation_name = 'Delete' . ucwords( $taxonomy->graphql_single_name );
self::$mutation[ $taxonomy->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
// Translators: The placeholder is the taxonomy name of the term being deleted
'description' => sprintf( esc_html__( 'Delete %1$s objects', 'wp-graphql' ), $taxonomy->graphql_single_name ),
'inputFields' => [
'id' => [
'type' => Types::non_null( Types::id() ),
// translators: The placeholder is the name of the taxonomy for the term being deleted
'description' => sprintf( __( 'The ID of the %1$s to delete', 'wp-graphql' ), $taxonomy->graphql_single_name ),
],
],
'outputFields' => [
'deletedId' => [
'type' => Types::id(),
'description' => __( 'The ID of the deleted object', 'wp-graphql' ),
'resolve' => function( $payload ) use ( $taxonomy ) {
$deleted = (object) $payload['termObject'];
return ! empty( $deleted->term_id ) ? Relay::toGlobalId( $taxonomy->name, $deleted->term_id ) : null;
},
],
$taxonomy->graphql_single_name => [
'type' => Types::term_object( $taxonomy->name ),
'description' => __( 'The object before it was deleted', 'wp-graphql' ),
'resolve' => function( $payload ) use ( $taxonomy ) {
$deleted = (object) $payload['termObject'];
return ! empty( $deleted ) ? $deleted : null;
},
],
],
'mutateAndGetPayload' => function( $input ) use ( $taxonomy, $mutation_name ) {
$id_parts = Relay::fromGlobalId( $input['id'] );
if ( ! empty( $id_parts['id'] ) && absint( $id_parts['id'] ) ) {
$term_id = absint( $id_parts['id'] );
} else {
// Translators: The placeholder is the name of the taxonomy for the term being deleted
throw new UserError( sprintf( __( 'The ID for the %1$s was not valid', 'wp-graphql' ), $taxonomy->graphql_single_name ) );
}
/**
* Ensure the type for the Global ID matches the type being mutated
*/
if ( empty( $id_parts['type'] ) || $taxonomy->name !== $id_parts['type'] ) {
// Translators: The placeholder is the name of the taxonomy for the term being edited
throw new UserError( sprintf( __( 'The ID passed is not for a %1$s object', 'wp-graphql' ), $taxonomy->graphql_single_name ) );
}
/**
* Get the term before deleting it
*/
$term_object = get_term( $term_id, $taxonomy->name );
/**
* Ensure the user can delete terms of this taxonomy
*/
if ( ! current_user_can( 'delete_term', $term_object->term_id ) ) {
// Translators: The placeholder is the name of the taxonomy for the term being deleted
throw new UserError( sprintf( __( 'You do not have permission to delete %1$s', 'wp-graphql' ), $taxonomy->graphql_plural_name ) );
}
/**
* Delete the term and get the response
*/
$deleted = wp_delete_term( $term_id, $taxonomy->name );
/**
* If there was an error deleting the term, get the error message and return it
*/
if ( is_wp_error( $deleted ) ) {
$error_message = $deleted->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
// Translators: The placeholder is the name of the taxonomy for the term being deleted
throw new UserError( sprintf( __( 'The %1$s failed to delete but no error was provided', 'wp-graphql' ), $taxonomy->name ) );
}
}
/**
* Return the term object that was retrieved prior to deletion
*/
return [
'termObject' => $term_object,
];
},
] );
}
return ! empty( self::$mutation[ $taxonomy->graphql_single_name ] ) ? self::$mutation[ $taxonomy->graphql_single_name ] : null;
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace WPGraphQL\Type\TermObject\Mutation;
use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
class TermObjectMutation {
/**
* Holds the input_fields configuration
*
* @var array
*/
private static $input_fields;
/**
* @param \WP_Taxonomy $taxonomy
*
* @return mixed|null
*/
public static function input_fields( \WP_Taxonomy $taxonomy ) {
if ( ! empty( $taxonomy->name ) && empty( self::$input_fields[ $taxonomy->name ] ) ) {
$input_fields = [
'name' => [
'type' => Types::string(),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The name of the %1$s object to mutate', 'wp-graphql' ), $taxonomy->name ),
],
'aliasOf' => [
'type' => Types::string(),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The slug that the %1$s will be an alias of', 'wp-graphql' ), $taxonomy->name ),
],
'description' => [
'type' => Types::string(),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The description of the %1$s object', 'wp-graphql' ), $taxonomy->name ),
],
'slug' => [
'type' => Types::string(),
'description' => __( 'If this argument exists then the slug will be checked to see if it is not an existing valid term. If that check succeeds (it is not a valid term), then it is added and the term id is given. If it fails, then a check is made to whether the taxonomy is hierarchical and the parent argument is not empty. If the second check succeeds, the term will be inserted and the term id will be given. If the slug argument is empty, then it will be calculated from the term name.' ),
],
];
/**
* Add a parentId field to hierarchical taxonomies to allow parents to be set
*/
if ( true === $taxonomy->hierarchical ) {
$input_fields['parentId'] = [
'type' => Types::id(),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The ID of the %1$s that should be set as the parent', 'wp-graphql' ), $taxonomy->name ),
];
}
/**
* Filter the mutation input fields for the object type
*
* @param array $input_fields The array of input fields
* @param \WP_Taxonomy The taxonomy of the Term object being mutated
*/
self::$input_fields[ $taxonomy->name ] = apply_filters( 'graphql_term_object_mutation_input_fields', $input_fields, $taxonomy );
} // End if().
return ! empty( self::$input_fields[ $taxonomy->name ] ) ? self::$input_fields[ $taxonomy->name ] : null;
}
/**
* This prepares the object to be mutated  ensures data is safe to be saved,
* and mapped from input args to WordPress $args
*
* @param array $input The input from the GraphQL Request
* @param \WP_Taxonomy $taxonomy The Taxonomy object for the type of term being mutated
* @param string $mutation_name The name of the mutation (create, update, etc)
*
* @throws \Exception
*
* @return mixed
*/
public static function prepare_object( $input, \WP_Taxonomy $taxonomy, $mutation_name ) {
/**
* Set the taxonomy for insert
*/
$insert_args['taxonomy'] = $taxonomy->name;
/**
* Prepare the data for inserting the term
*/
if ( ! empty( $input['aliasOf'] ) ) {
$insert_args['alias_of'] = $input['aliasOf'];
}
if ( ! empty( $input['name'] ) ) {
$insert_args['name'] = esc_sql( $input['name'] );
}
if ( ! empty( $input['description'] ) ) {
$insert_args['description'] = esc_sql( $input['description'] );
}
if ( ! empty( $input['slug'] ) ) {
$insert_args['slug'] = esc_sql( $input['slug'] );
}
/**
* If the parentId argument was entered, we need to validate that it's actually a legit term that can
* be set as a parent
*/
if ( ! empty( $input['parentId'] ) ) {
/**
* Convert parent ID to WordPress ID
*/
$parent_id_parts = ! empty( $input['parentId'] ) ? Relay::fromGlobalId( $input['parentId'] ) : null;
/**
* Ensure that the ID passed in is a valid GlobalID
*/
if ( is_array( $parent_id_parts ) && ! empty( $parent_id_parts['id'] ) ) {
/**
* Get the Term ID from the global ID
*/
$parent_id = $parent_id_parts['id'];
/**
* Ensure there's actually a parent term to be associated with
*/
$parent_term = get_term( absint( $parent_id ), $taxonomy->name );
if ( ! $parent_term || is_wp_error( $parent_term ) ) {
throw new UserError( __( 'The parent does not exist', 'wp-graphql' ) );
}
// Otherwise set the parent as the parent term's ID
$insert_args['parent'] = $parent_term->term_id;
} else {
throw new UserError( __( 'The parent ID is not a valid ID', 'wp-graphql' ) );
} // End if().
}
/**
* Filter the $insert_args
*
* @param array $insert_args The array of input args that will be passed to the functions that insert terms
* @param array $input The data that was entered as input for the mutation
* @param \WP_Taxonomy $taxonomy The taxonomy object of the term being mutated
* @param string $mutation_name The name of the mutation being performed (create, edit, etc)
*/
$insert_args = apply_filters( 'graphql_term_object_insert_term_args', $insert_args, $input, $taxonomy, $mutation_name );
/**
* Return the $args
*/
return $insert_args;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace WPGraphQL\Type\TermObject\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
class TermObjectUpdate {
/**
* Holds the mutation field definition
*
* @var array $mutation
*/
private static $mutation = [];
/**
* Defines the update mutation for TermObjects
*
* @param \WP_Taxonomy $taxonomy
*
* @return array|mixed
*/
public static function mutate( \WP_Taxonomy $taxonomy ) {
if (
! empty( $taxonomy->graphql_single_name )
&& empty( self::$mutation[ $taxonomy->graphql_single_name ] )
) {
$mutation_name = 'Update' . ucwords( $taxonomy->graphql_single_name );
self::$mutation[ $taxonomy->graphql_single_name ] = Relay::mutationWithClientMutationId( [
'name' => esc_html( $mutation_name ),
// translators: The placeholder is the name of the post type being updated
'description' => sprintf( esc_html__( 'Updates %1$s objects', 'wp-graphql' ), $taxonomy->graphql_single_name ),
'inputFields' => self::input_fields( $taxonomy ),
'outputFields' => [
$taxonomy->graphql_single_name => [
'type' => Types::term_object( $taxonomy->name ),
'resolve' => function( $payload ) use ( $taxonomy ) {
return get_term( $payload['term_id'], $taxonomy->name );
},
],
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) use ( $taxonomy, $mutation_name ) {
/**
* Get the ID parts
*/
$id_parts = ! empty( $input['id'] ) ? Relay::fromGlobalId( $input['id'] ) : null;
/**
* Ensure the type for the Global ID matches the type being mutated
*/
if ( empty( $id_parts['type'] ) || $taxonomy->name !== $id_parts['type'] ) {
// Translators: The placeholder is the name of the taxonomy for the term being edited
throw new UserError( sprintf( __( 'The ID passed is not for a %1$s object', 'wp-graphql' ), $taxonomy->graphql_single_name ) );
}
/**
* Get the existing term
*/
$existing_term = get_term( absint( $id_parts['id'] ), $taxonomy->name );
/**
* If there was an error getting the existing term, return the error message
*/
if ( is_wp_error( $existing_term ) ) {
$error_message = $existing_term->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
// Translators: The placeholder is the name of the taxonomy for the term being deleted
throw new UserError( sprintf( __( 'The %1$s failed to update', 'wp-graphql' ), $taxonomy->name ) );
}
}
/**
* Ensure the user has permission to edit terms
*/
if ( ! current_user_can( 'edit_term', $existing_term->term_id ) ) {
// Translators: The placeholder is the name of the taxonomy for the term being deleted
throw new UserError( sprintf( __( 'You do not have permission to update %1$s', 'wp-graphql' ), $taxonomy->graphql_plural_name ) );
}
/**
* Prepare the $args for mutation
*/
$args = TermObjectMutation::prepare_object( $input, $taxonomy, $mutation_name );
if ( ! empty( $args ) ) {
/**
* Update the term
*/
$update = wp_update_term( $existing_term->term_id, $taxonomy->name, wp_slash( (array) $args ) );
/**
* Respond with any errors
*/
if ( is_wp_error( $update ) ) {
// Translators: the placeholder is the name of the taxonomy
throw new UserError( sprintf( __( 'The %1$s failed to update', 'wp-graphql' ), $taxonomy->name ) );
}
}
/**
* Fires an action when a term is updated via a GraphQL Mutation
*
* @param int $term_id The ID of the term object that was mutated
* @param array $args The args used to update the term
* @param string $mutation_name The name of the mutation being performed (create, update, delete, etc)
* @param AppContext $context The AppContext passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the resolve tree
*/
do_action( "graphql_update_{$taxonomy->name}", $existing_term->term_id, $args, $mutation_name, $context, $info );
/**
* Return the payload
*/
return [
'term_id' => $existing_term->term_id,
];
},
] );
}
return ! empty( self::$mutation[ $taxonomy->graphql_single_name ] ) ? self::$mutation[ $taxonomy->graphql_single_name ] : null;
}
/**
* Add the id as an optional field for update mutations
*
* @param \WP_Taxonomy $taxonomy
*
* @return array
*/
private static function input_fields( $taxonomy ) {
/**
* Add name as a non_null field for term creation
*/
return array_merge(
[
'name' => [
'type' => Types::string(),
// Translators: The placeholder is the name of the taxonomy for the object being mutated
'description' => sprintf( __( 'The name of the %1$s object to mutate', 'wp-graphql' ), $taxonomy->name ),
],
'id' => [
'type' => Types::non_null( Types::id() ),
// Translators: The placeholder is the taxonomy of the term being updated
'description' => sprintf( __( 'The ID of the %1$s object to update', 'wp-graphql' ), $taxonomy->graphql_single_name ),
],
],
TermObjectMutation::input_fields( $taxonomy )
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace WPGraphQL\Type\TermObject;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class TermObjectQuery
* @package WPGraphQL\Type\TermObject
* @Since 0.0.5
*/
class TermObjectQuery {
/**
* Holds the root_query field definition
* @var array $root_query
* @since 0.0.5
*/
private static $root_query = [];
/**
* Method that returns the root query field definition for the post object type
*
* @param object $taxonomy_object
* @return array
* @since 0.0.5
*/
public static function root_query( $taxonomy_object ) {
if ( ! empty( $taxonomy_object->name ) && empty( self::$root_query[ $taxonomy_object->name ] ) ) {
self::$root_query[ $taxonomy_object->name ] = [
'type' => Types::term_object( $taxonomy_object->name ),
'description' => sprintf( __( 'A % object', 'wp-graphql' ), $taxonomy_object->graphql_single_name ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $taxonomy_object ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_term_object( $id_components['id'], $taxonomy_object->name );
},
];
return self::$root_query[ $taxonomy_object->name ];
}
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace WPGraphQL\Type\TermObject;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionDefinition;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionDefinition;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionResolver;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class TermObjectType
*
* This sets up the base TermObjectType. Custom taxonomies that are set to "show_in_graphql" will automatically
* use the TermObjectType and inherit the fields that are defined here. The fields get passed through a
* filter unique to each taxonomy, so each taxonomy can modify it's term schema via field filters.
*
* NOTE: In some cases, (probably rare, I would guess) the "shape" of a custom taxonomy term might not make sense to
* inherit the fields defined here, so it might make sense for a Taxonomy to register it's own custom defined type
* for it's terms instead of utilizing the TermObjectType.
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class TermObjectType extends WPObjectType {
/**
* Holds the $fields definition for the TermObjectType
*
* @var $fields
*/
private static $fields = [];
/**
* Holds the $taxonomy_object
*
* @var $taxonomy_object
*/
private static $taxonomy_object;
/**
* TermObjectType constructor.
*
* @param string $taxonomy The taxonomy name
*
* @since 0.0.5
*/
public function __construct( $taxonomy ) {
/**
* Get the taxonomy object and store it
*
* @since 0.0.5
*/
self::$taxonomy_object = get_taxonomy( $taxonomy );
$config = [
'name' => ucfirst( self::$taxonomy_object->graphql_single_name ),
'description' => sprintf( __( 'The % object type', 'wp-graphql' ), self::$taxonomy_object->graphql_single_name ),
'fields' => self::fields( self::$taxonomy_object ),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields for TermObjectType
*
* @param \WP_Taxonomy $taxonomy_object
* @return \GraphQL\Type\Definition\FieldDefinition|mixed|null
* @since 0.0.5
*/
private static function fields( $taxonomy_object ) {
/**
* Get the $single_name out of the taxonomy_object
*
* @since 0.0.5
*/
$single_name = self::$taxonomy_object->graphql_single_name;
/**
* If the $fields haven't already been defined for this type,
* define the fields
*
* @since 0.0.5
*/
if ( empty( self::$fields[ $single_name ] ) ) {
/**
* Get the post_types and taxonomies that are allowed
* in WPGraphQL
*
* @since 0.0.5
*/
$allowed_post_types = \WPGraphQL::$allowed_post_types;
/**
* Define the fields for the terms of the specified taxonomy
*
* @return mixed
* @since 0.0.5
*/
self::$fields[ $single_name ] = function() use ( $single_name, $taxonomy_object, $allowed_post_types ) {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
# Placeholder is the name of the taxonomy
'description' => __( 'The global ID for the ' . $taxonomy_object->name, 'wp-graphql' ),
'resolve' => function( \WP_Term $term, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $term->taxonomy ) && ! empty( $term->term_id ) ) ? Relay::toGlobalId( $term->taxonomy, $term->term_id ) : null;
},
],
$single_name . 'Id' => [
'type' => Types::int(),
'description' => __( 'The id field matches the WP_Post->ID field.', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->term_id ) ? absint( $term->term_id ) : null;
},
],
'count' => [
'type' => Types::int(),
'description' => __( 'The number of objects connected to the object', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->count ) ? absint( $term->count ) : null;
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'The description of the object', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->description ) ? $term->description : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'The human friendly name of the object.', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->name ) ? $term->name : null;
},
],
'slug' => [
'type' => Types::string(),
'description' => __( 'An alphanumeric identifier for the object unique to its type.', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->slug ) ? $term->slug : null;
},
],
'termGroupId' => [
'type' => Types::int(),
'description' => __( 'The ID of the term group that this term object belongs to', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->term_group ) ? absint( $term->term_group ) : null;
},
],
'termTaxonomyId' => [
'type' => Types::int(),
'description' => __( 'The taxonomy ID that the object is associated with', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->term_taxonomy_id ) ? absint( $term->term_taxonomy_id ) : null;
},
],
'taxonomy' => [
'type' => Types::taxonomy(),
'description' => __( 'The name of the taxonomy this term belongs to', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, array $args, AppContext $context, ResolveInfo $info ) {
$taxonomy = get_taxonomy( $term->taxonomy );
return ! empty( $term->taxonomy ) && false !== $taxonomy ? $taxonomy : null;
},
],
'link' => [
'type' => Types::string(),
'description' => __( 'The link to the term', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, $args, AppContext $context, ResolveInfo $info ) {
$link = get_term_link( $term->term_id );
return ( ! is_wp_error( $link ) ) ? $link : null;
},
],
];
/**
* For hierarchical taxonomies, provide parent and ancestor fields
*/
if ( true === $taxonomy_object->hierarchical ) {
$fields['parent'] = [
'type' => Types::term_object( $taxonomy_object->name ),
'description' => __( 'The parent object', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $term->parent ) ? DataSource::resolve_term_object( $term->parent, $term->taxonomy ) : null;
},
];
$fields['ancestors'] = [
'type' => Types::list_of( Types::term_object( $taxonomy_object->name ) ),
'description' => esc_html__( 'The ancestors of the object', 'wp-graphql' ),
'resolve' => function( \WP_Term $term, $args, AppContext $context, ResolveInfo $info ) {
$ancestors = [];
$ancestor_ids = get_ancestors( $term->term_id, $term->taxonomy );
if ( ! empty( $ancestor_ids ) ) {
foreach ( $ancestor_ids as $ancestor_id ) {
$ancestors[] = get_term( $ancestor_id );
}
}
return ! empty( $ancestors ) ? $ancestors : null;
},
];
$fields['children'] = TermObjectConnectionDefinition::connection( $taxonomy_object, 'Children' );
}
/**
* Add connections for post_types that are registered to the taxonomy
*
* @since 0.0.5
*/
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $post_type ) {
if ( in_array( $post_type, $taxonomy_object->object_type, true ) ) {
$post_type_object = get_post_type_object( $post_type );
$fields[ $post_type_object->graphql_plural_name ] = PostObjectConnectionDefinition::connection( $post_type_object, $taxonomy_object->graphql_single_name );
}
}
}
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*
* @since 0.0.5
*/
return self::prepare_fields( $fields, $single_name );
};
}
return ! empty( self::$fields[ $single_name ] ) ? self::$fields[ $single_name ] : null;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace WPGraphQL\Type\Theme\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class ThemeConnectionDefinition
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class ThemeConnectionDefinition {
/**
* Stores some date for the Relay connection for term objects
*
* @var array $connection
* @since 0.0.5
* @access private
*/
private static $connection;
/**
* Method that sets up the relay connection for term objects
* @param string $from_type The name of the Type the connection is coming from
* @return mixed
* @since 0.0.5
*/
public static function connection( $from_type = 'Root' ) {
if ( null === self::$connection ) {
self::$connection = [];
}
if ( empty( self::$connection[ $from_type ] ) ) {
/**
* Setup the connectionDefinition
*
* @since 0.0.5
*/
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::theme(),
'name' => ucfirst( $from_type ) . 'Themes',
'connectionFields' => function() {
return [
'nodes' => [
'type' => Types::list_of( Types::theme() ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the connection to the themes_connection object
*
* @since 0.0.5
*/
self::$connection[ $from_type ] = [
'type' => $connection['connectionType'],
'description' => __( 'A collection of theme objects', 'wp-graphql' ),
'args' => Relay::connectionArgs(),
'resolve' => function( $source, $args, AppContext $context, ResolveInfo $info ) {
return DataSource::resolve_themes_connection( $source, $args, $context, $info );
},
];
}
return ! empty( self::$connection[ $from_type ] ) ? self::$connection[ $from_type ] : null;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace WPGraphQL\Type\Theme\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
/**
* Class ThemeConnectionResolver
*
* @package WPGraphQL\Data\Resolvers
* @since 0.5.0
*/
class ThemeConnectionResolver {
/**
* Creates the connection for themes
*
* @param mixed $source The query results of the query calling this relation
* @param array $args Query arguments
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.5.0
* @return array
* @access public
*/
public static function resolve( $source, array $args, AppContext $context, ResolveInfo $info ) {
$themes_array = [];
$themes = wp_get_themes();
if ( is_array( $themes ) && ! empty( $themes ) ) {
foreach ( $themes as $theme ) {
$themes_array[] = $theme;
}
}
$connection = Relay::connectionFromArray( $themes_array, $args );
$nodes = [];
if ( ! empty( $connection['edges'] ) && is_array( $connection['edges'] ) ) {
foreach ( $connection['edges'] as $edge ) {
$nodes[] = ! empty( $edge['node'] ) ? $edge['node'] : null;
}
}
$connection['nodes'] = ! empty( $nodes ) ? $nodes : null;
return ! empty( $themes_array ) ? $connection : null;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace WPGraphQL\Type\Theme;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class ThemeType
* @package WPGraphQL\Type
* @since 0.0.5
*/
class ThemeType extends WPObjectType {
/**
* Holds the type name
* @var string $type_name
*/
private static $type_name;
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
private static $fields;
/**
* ThemeType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
* @since 0.0.5
*/
self::$type_name = 'Theme';
$config = [
'name' => self::$type_name,
'description' => __( 'A theme object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields for the ThemeType. The fields are passed through a filter so the shape of the schema
* can be modified
*
* @return array|\GraphQL\Type\Definition\FieldDefinition[]
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$stylesheet = $theme->get_stylesheet();
return ( ! empty( $info->parentType ) && ! empty( $stylesheet ) ) ? Relay::toGlobalId( 'theme', $stylesheet ) : null;
},
],
'slug' => [
'type' => Types::string(),
'description' => __( 'The theme slug is used to internally match themes. Theme slugs can have subdirectories like: my-theme/sub-theme. This field is equivalent to WP_Theme->get_stylesheet().', 'wp-graphql' ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$stylesheet = $theme->get_stylesheet();
return ! empty( $stylesheet ) ? $stylesheet : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'Display name of the theme. This field is equivalent to WP_Theme->get( "Name" ).', 'wp-graphql' ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$name = $theme->get( 'Name' );
return ! empty( $name ) ? $name : null;
},
],
'screenshot' => [
'type' => Types::string(),
'description' => __( 'The URL of the screenshot for the theme. The screenshot is intended to give an overview of what the theme looks like. This field is equivalent to WP_Theme->get_screenshot().', 'wp-graphql' ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$screenshot = $theme->get_screenshot();
return ! empty( $screenshot ) ? $screenshot : null;
},
],
'themeUri' => [
'type' => Types::string(),
'description' => __( 'A URI if the theme has a website associated with it. The Theme URI is handy for directing users to a theme site for support etc. This field is equivalent to WP_Theme->get( "ThemeURI" ).', 'wp-graphql' ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$theme_uri = $theme->get( 'ThemeURI' );
return ! empty( $theme_uri ) ? $theme_uri : null;
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'The description of the theme. This field is equivalent to WP_Theme->get( "Description" ).', 'wp-graphql' ),
],
'author' => [
'type' => Types::string(),
'description' => __( 'Name of the theme author(s), could also be a company name. This field is equivalent to WP_Theme->get( "Author" ).', 'wp-graphql' ),
],
'authorUri' => [
'type' => Types::string(),
'description' => __( 'URI for the author/company website. This field is equivalent to WP_Theme->get( "AuthorURI" ).', 'wp-graphql' ),
'resolve' => function( \WP_Theme $theme, $args, AppContext $context, ResolveInfo $info ) {
$author_uri = $theme->get( 'AuthorURI' );
return ! empty( $author_uri ) ? $author_uri : null;
},
],
'tags' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'URI for the author/company website. This field is equivalent to WP_Theme->get( "Tags" ).', 'wp-graphql' ),
],
'version' => [
'type' => Types::float(),
'description' => __( 'The current version of the theme. This field is equivalent to WP_Theme->get( "Version" ).', 'wp-graphql' ),
],
];
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace WPGraphQL\Type\Union;
use GraphQL\Type\Definition\UnionType;
use WPGraphQL\Types;
/**
* Class CommentAuthorUnionType
*
* In some situations, the type of term cannot be known until query time. The commentAuthorUnion allows for a
* comment author to be queried and resolved to a number of types. Currently will return a user or commentAuthor.
*
* @package WPGraphQL\Type\Union
*/
class CommentAuthorUnionType extends UnionType {
/**
* This holds an array of the possible types that can be resolved by this union
*
* @var array
*/
private static $possible_types;
/**
* CommentAuthorUnionType constructor.
*/
public function __construct() {
self::getPossibleTypes();
$config = [
'name' => 'CommentAuthorUnion',
'types' => self::$possible_types,
'resolveType' => function( $source ) {
if ( $source instanceof \WP_User ) {
$type = Types::user();
} else {
$type = Types::comment_author();
}
return $type;
},
];
parent::__construct( $config );
}
/**
* This defines the possible types that can be resolved by this union
*
* @return array|null An array of possible types that can be resolved by the union
*/
public function getPossibleTypes() {
if ( null === self::$possible_types ) {
self::$possible_types = [];
}
self::$possible_types = [
'user' => Types::user(),
'commentAuthor' => Types::comment_author(),
];
/**
* Filter the possible_types as it's possible some systems might set things like "parent_id" to a different
* object than a post_type, and might want to be able to hook in and add a non postObject type to the possible
* resolveTypes.
*
* @param array $possible_types An array of possible types that can be resolved for the union
*
* @since 0.0.6
*/
self::$possible_types = apply_filters( 'graphql_comment_author_union_possible_types', self::$possible_types );
return ! empty( self::$possible_types ) ? self::$possible_types : null;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace WPGraphQL\Type\Union;
use GraphQL\Type\Definition\UnionType;
use WPGraphQL\Types;
/**
* Class PostObjectUnionType
*
* In WordPress, relations can be set to a post object by ID, but in many cases there's no strict control over what
* post_type the related post can be, so in many cases it's unknown what "post_type" the related post is until the post
* has been queried and returned. Some examples of such relations are:
*
* - Posts can have a parent_id set, and the parent_id can be of any post_type, so it's unknown what type of postObject
* will be returned until the query has been run.
*
* - Attachments (mediaItems) can be uploaded to any post_type as well, so their "uploadedTo" property can return any
* type of post object.
*
* @package WPGraphQL\Type\Union
* @since 0.0.6
*/
class PostObjectUnionType extends UnionType {
/**
* This holds an array of the possible types that can be resolved by this union
* @var array
* @since 0.0.6
*/
private static $possible_types;
/**
* PostObjectUnionType constructor.
* @since 0.0.6
*/
public function __construct() {
self::getPossibleTypes();
$config = [
'name' => 'PostObjectUnion',
'types' => self::$possible_types,
'resolveType' => function( $value ) {
return ! empty( $value->post_type ) ? Types::post_object( $value->post_type ) : null;
},
];
parent::__construct( $config );
}
/**
* This defines the possible types that can be resolved by this union
*
* @return array An array of possible types that can be resolved by the union
* @since 0.0.5
*/
public function getPossibleTypes() {
if ( null === self::$possible_types ) {
self::$possible_types = [];
}
$allowed_post_types = \WPGraphQL::$allowed_post_types;
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $allowed_post_type ) {
if ( empty( self::$possible_types[ $allowed_post_type ] ) ) {
self::$possible_types[ $allowed_post_type ] = Types::post_object( $allowed_post_type );
}
}
}
/**
* Filter the possible_types as it's possible some systems might set things like "parent_id" to a different
* object than a post_type, and might want to be able to hook in and add a non postObject type to the possible
* resolveTypes.
*
* @param array $possible_types An array of possible types that can be resolved for the union
* @since 0.0.6
*/
self::$possible_types = apply_filters( 'graphql_post_object_union_possible_types', self::$possible_types );
return ! empty( self::$possible_types ) ? self::$possible_types : null;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace WPGraphQL\Type\Union;
use GraphQL\Type\Definition\UnionType;
use WPGraphQL\Types;
/**
* Class TermObjectUnionType
*
* In some situations, the type of term cannot be known until query time. The termObjectUnion allows for connections to
* be queried and resolved to a number of types.
*
* @package WPGraphQL\Type\Union
*/
class TermObjectUnionType extends UnionType {
/**
* This holds an array of the possible types that can be resolved by this union
* @var array
*/
private static $possible_types;
/**
* TermObjectUnionType constructor.
*/
public function __construct() {
self::getPossibleTypes();
$config = [
'name' => 'TermObjectUnion',
'types' => self::$possible_types,
'resolveType' => function( $value ) {
return ! empty( $value->taxonomy ) ? Types::term_object( $value->taxonomy ) : null;
},
];
parent::__construct( $config );
}
/**
* This defines the possible types that can be resolved by this union
*
* @return array|null An array of possible types that can be resolved by the union
*/
public function getPossibleTypes() {
if ( null === self::$possible_types ) {
self::$possible_types = [];
}
$allowed_taxonomies = \WPGraphQL::$allowed_taxonomies;
if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) {
foreach ( $allowed_taxonomies as $allowed_taxonomy ) {
if ( empty( self::$possible_types[ $allowed_taxonomy ] ) ) {
self::$possible_types[ $allowed_taxonomy ] = Types::term_object( $allowed_taxonomy );
}
}
}
/**
* Filter the possible_types as it's possible some systems might set things like "parent_id" to a different
* object than a post_type, and might want to be able to hook in and add a non postObject type to the possible
* resolveTypes.
*
* @param array $possible_types An array of possible types that can be resolved for the union
* @since 0.0.6
*/
self::$possible_types = apply_filters( 'graphql_term_object_union_possible_types', self::$possible_types );
return ! empty( self::$possible_types ) ? self::$possible_types : null;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace WPGraphQL\Type\User\Connection;
use GraphQL\Type\Definition\EnumType;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Types;
/**
* Class UserConnectionArgs
*
* This sets up the Query Args for user connections, which uses WP_User_Query, so this defines the allowed
* input fields that will be passed to the WP_User_Query
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class UserConnectionArgs extends WPInputObjectType {
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
public static $fields;
/**
* This holds the $roles_enum definition
* @var EnumType
* @since 0.0.5
*/
private static $roles_enum;
/**
* This holds the SearchColumnsEnumType
* @var EnumType
* @since 0.0.5
*/
private static $search_columns_enum;
/**
* UserConnectionArgs constructor.
* @param array $config Array of config for the Input Type
* @param string $connection The name of the connection the args belong to
* @since 0.0.5
*/
public function __construct( $config = [], $connection = '' ) {
$config['name'] = ucfirst( $connection ) . 'UserArgs';
$config['queryClass'] = 'WP_User_Query';
$config['fields'] = self::fields( $connection );
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields that make up the UserConnectionArgs
*
* @param string $connection The name of the connection the Args belong to
* @return array
* @since 0.0.5
*/
private static function fields( $connection ) {
if ( null === self::$fields ) {
self::$fields = [];
}
if ( empty( self::$fields ) ) {
$fields = [
'role' => [
'type' => self::roles_enum(),
'description' => __( 'An array of role names that users must match to be included in results. Note that this is an inclusive list: users must match *each* role.', 'wp-graphql' ),
],
'roleIn' => [
'type' => Types::list_of( self::roles_enum() ),
'description' => __( 'An array of role names. Matched users must have at least one of these roles.', 'wp-graphql' ),
],
'roleNotIn' => [
'type' => Types::list_of( self::roles_enum() ),
'description' => __( 'An array of role names to exclude. Users matching one or more of these roles will not be included in results.', 'wp-graphql' ),
],
'include' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of comment IDs to include.', 'wp-graphql' ),
],
'exclude' => [
'type' => Types::list_of( Types::int() ),
'description' => __( 'Array of IDs of users whose unapproved comments will be returned by the query regardless of status.', 'wp-graphql' ),
],
'search' => [
'type' => Types::string(),
'description' => __( 'Search keyword. Searches for possible string matches on columns. When `searchColumns` is left empty, it tries to determine which column to search in based on search string.', 'wp-graphql' ),
],
'searchColumns' => [
'type' => Types::list_of( self::search_columns_enum() ),
'description' => __( 'Array of column names to be searched. Accepts \'ID\', \'login\', \'nicename\', \'email\', \'url\'.', 'wp-graphql' ),
],
'hasPublishedPosts' => [
'type' => Types::list_of( Types::post_type_enum() ),
'description' => __( 'Pass an array of post types to filter results to users who have published posts in those post types.', 'wp-graphql' ),
],
'nicename' => [
'type' => Types::string(),
'description' => __( 'The user nicename.', 'wp-graphql' ),
],
'nicenameIn' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'An array of nicenames to include. Users matching one of these nicenames will be included in results.', 'wp-graphql' ),
],
'nicenameNotIn' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'An array of nicenames to exclude. Users matching one of these nicenames will not be included in results.', 'wp-graphql' ),
],
'login' => [
'type' => Types::string(),
'description' => __( 'The user login.', 'wp-graphql' ),
],
'loginIn' => [
'type' => Types::int(),
'description' => __( 'An array of logins to include. Users matching one of these logins will be included in results.', 'wp-graphql' ),
],
'loginNotIn' => [
'type' => Types::int(),
'description' => __( 'An array of logins to exclude. Users matching one of these logins will not be included in results.', 'wp-graphql' ),
],
];
self::$fields[ $connection ] = self::prepare_fields( $fields, ucfirst( $connection ) . 'UserArgs' );
}
return ! empty( self::$fields[ $connection ] ) ? self::$fields[ $connection ]: null;
}
/**
* search_columns_enum
*
* Returns the searchColumnsEnum type defintion
*
* @return EnumType
* @since 0.0.5
*/
private static function search_columns_enum() {
if ( null === self::$search_columns_enum ) {
self::$search_columns_enum = new WPEnumType( [
'name' => 'SearchColumnsEnum',
'values' => [
'ID' => [
'value' => 'ID',
],
'LOGIN' => [
'value' => 'login',
],
'NICENAME' => [
'value' => 'nicename',
],
'EMAIL' => [
'value' => 'email',
],
'URL' => [
'value' => 'url',
],
],
] );
}
return self::$search_columns_enum;
}
/**
* roles_enum
*
* Returns the userRoleEnum type definition
*
* @return EnumType
* @since 0.0.5
*/
private static function roles_enum() {
if ( null === self::$roles_enum ) {
global $wp_roles;
$all_roles = $wp_roles->roles;
$editable_roles = apply_filters( 'editable_roles', $all_roles );
$roles = [];
if ( ! empty( $editable_roles ) && is_array( $editable_roles ) ) {
foreach ( $editable_roles as $key => $role ) {
$formatted_role = self::format_enum_name( $role['name'] );
$roles[ $formatted_role ] = [
'value' => $key,
];
}
}
if ( ! empty( $roles ) ) {
self::$roles_enum = new WPEnumType( [
'name' => 'UserRoleEnum',
'values' => $roles,
] );
}
}
return self::$roles_enum;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace WPGraphQL\Type\User\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class UserConnectionDefinition
* @package WPGraphQL\Type\Comment\Connection
* @since 0.0.5
*/
class UserConnectionDefinition {
/**
* Stores some date for the Relay connection for term objects
*
* @var array $connection
* @since 0.0.5
* @access private
*/
private static $connection = [];
/**
* Method that sets up the relay connection for term objects
*
* @param string $from_type The name of the type the connection is coming from
* @return mixed
* @since 0.0.5
*/
public static function connection( $from_type = 'Root' ) {
if ( empty( self::$connection[ $from_type ] ) ) {
$connection = Relay::connectionDefinitions( [
'nodeType' => Types::user(),
'name' => ucfirst( $from_type ) . 'Users',
'connectionFields' => function() {
return [
'nodes' => [
'type' => Types::list_of( Types::user() ),
'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ),
'resolve' => function( $source, $args, $context, $info ) {
return ! empty( $source['nodes'] ) ? $source['nodes'] : [];
},
],
];
},
] );
/**
* Add the "where" args to the commentConnection
*
* @since 0.0.5
*/
$args = [
'where' => [
'name' => 'where',
'type' => Types::user_connection_query_args( ucfirst( $from_type ) . 'Users' ),
],
];
self::$connection[ $from_type ] = [
'type' => $connection['connectionType'],
'description' => __( 'A collection of user objects', 'wp-graphql' ),
'args' => array_merge( Relay::connectionArgs(), $args ),
'resolve' => function( $source, $args, AppContext $context, ResolveInfo $info ) {
return DataSource::resolve_users_connection( $source, $args, $context, $info );
},
];
}
return ! empty( self::$connection[ $from_type ] ) ? self::$connection[ $from_type ] : null;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace WPGraphQL\Type\User\Connection;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\ConnectionResolver;
use WPGraphQL\Types;
/**
* Class UserConnectionResolver
*
* @package WPGraphQL\Data\Resolvers
* @since 0.5.0
*/
class UserConnectionResolver extends ConnectionResolver {
/**
* This runs the query and returns the repsonse
*
* @param $query_args
*
* @return \WP_User_Query
*/
public static function get_query( $query_args ) {
$query = new \WP_User_Query( $query_args );
return $query;
}
/**
* This returns the $query_args that should be used when querying for posts in the postObjectConnectionResolver.
* This checks what input $args are part of the query, combines them with various filters, etc and returns an
* array of $query_args to be used in the \WP_Query call
*
* @param mixed $source The query source being passed down to the resolver
* @param array $args The arguments that were provided to the query
* @param AppContext $context Object containing app context that gets passed down the resolve tree
* @param ResolveInfo $info Info about fields passed down the resolve tree
*
* @return array
* @throws \Exception
*/
public static function get_query_args( $source, array $args, AppContext $context, ResolveInfo $info ) {
/**
* Set the $query_args based on various defaults and primary input $args
*/
$query_args['count_total'] = false;
$query_args['offset'] = self::get_offset( $args );
$query_args['order'] = ! empty( $args['last'] ) ? 'ASC' : 'DESC';
/**
* If "pageInfo" is in the fieldSelection, we need to calculate the pagination details, so
* we need to run the query with count_total set to true.
*/
$field_selection = $info->getFieldSelection( 2 );
if ( ! empty( $field_selection['pageInfo'] ) ) {
$query_args['count_total'] = true;
}
/**
* Set the number, ensuring it doesn't exceed the amount set as the $max_query_amount
*/
$query_args['number'] = self::get_query_amount( $source, $args, $context, $info );
/**
* Take any of the input $args (under the "where" input) that were part of the GraphQL query and map and
* sanitize their GraphQL input to apply to the WP_Query
*/
$input_fields = [];
if ( ! empty( $args['where'] ) ) {
$input_fields = self::sanitize_input_fields( $args['where'], $source, $args, $context, $info );
}
/**
* Merge the default $query_args with the $args that were entered in the query.
*
* @since 0.0.5
*/
if ( ! empty( $input_fields ) ) {
$query_args = array_merge( $query_args, $input_fields );
}
/**
* Filter the query_args that should be applied to the query. This filter is applied AFTER the input args from
* the GraphQL Query have been applied and has the potential to override the GraphQL Query Input Args.
*
* @param array $query_args array of query_args being passed to the
* @param mixed $source source passed down from the resolve tree
* @param array $args array of arguments input in the field as part of the GraphQL query
* @param AppContext $context object passed down zthe resolve tree
* @param ResolveInfo $info info about fields passed down the resolve tree
*
* @since 0.0.6
*/
$query_args = apply_filters( 'graphql_user_connection_query_args', $query_args, $source, $args, $context, $info );
return $query_args;
}
/**
* This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_User_Query
* friendly keys.
*
* There's probably a cleaner/more dynamic way to approach this, but this was quick. I'd be
* down to explore more dynamic ways to map this, but for now this gets the job done.
*
* @param array $args The query "where" args
* @param mixed $source The query results of the query calling this relation
* @param array $all_args Array of all the query args (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @return array
* @access private
*/
public static function sanitize_input_fields( array $args, $source, array $all_args, AppContext $context, ResolveInfo $info ) {
$arg_mapping = [
'roleIn' => 'role__in',
'roleNotIn' => 'role__not_in',
'searchColumns' => 'search_columns',
'hasPublishedPosts' => 'has_published_posts',
'nicenameIn' => 'nicename__in',
'nicenameNotIn' => 'nicename__not_in',
'loginIn' => 'login__in',
'loginNotIn' => 'login__not_in',
];
/**
* Map and sanitize the input args to the WP_User_Query compatible args
*/
$query_args = Types::map_input( $args, $arg_mapping );
/**
* Filter the input fields
*
* This allows plugins/themes to hook in and alter what $args should be allowed to be passed
* from a GraphQL Query to the get_terms query
*
* @param array $query_args The mapped query args
* @param array $args The query "where" args
* @param mixed $source The query results of the query calling this relation
* @param array $all_args Array of all the query args (not just the "where" args)
* @param AppContext $context The AppContext object
* @param ResolveInfo $info The ResolveInfo object
*
* @since 0.0.5
* @return array
*/
$query_args = apply_filters( 'graphql_map_input_fields_to_wp_comment_query', $query_args, $args, $source, $all_args, $context, $info );
return ! empty( $query_args ) && is_array( $query_args ) ? $query_args : [];
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace WPGraphQL\Type\User\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class UserCreate
*
* @package WPGraphQL\Type\User\Mutation
*/
class UserCreate {
/**
* Stores the user create mutation
*
* @var array $mutation
* @access private
*/
private static $mutation;
/**
* Process the user creat mutation
*
* @return array|null
* @access public
*/
public static function mutate() {
if ( empty( self::$mutation ) ) {
self::$mutation = Relay::mutationWithClientMutationId( [
'name' => 'CreateUser',
'description' => __( 'Create new user object', 'wp-graphql' ),
'inputFields' => self::input_fields(),
'outputFields' => [
'user' => [
'type' => Types::user(),
'resolve' => function( $payload ) {
return get_user_by( 'ID', $payload['id'] );
}
]
],
'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) {
if ( ! current_user_can( 'create_users' ) ) {
throw new UserError( __( 'Sorry, you are not allowed to create a new user.', 'wp-graphql' ) );
}
/**
* Map all of the args from GQL to WP friendly
*/
$user_args = UserMutation::prepare_user_object( $input, 'userCreate' );
/**
* Create the new user
*/
$user_id = wp_insert_user( $user_args );
/**
* Throw an exception if the post failed to create
*/
if ( is_wp_error( $user_id ) ) {
$error_message = $user_id->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
throw new UserError( __( 'The object failed to create but no error was provided', 'wp-graphql' ) );
}
}
/**
* If the $post_id is empty, we should throw an exception
*/
if ( empty( $user_id ) ) {
throw new UserError( __( 'The object failed to create', 'wp-graphql' ) );
}
/**
* Update additional user data
*/
UserMutation::update_additional_user_object_data( $user_id, $input, 'create', $context, $info );
/**
* Return the new user ID
*/
return [
'id' => $user_id,
];
}
] );
}
return ( ! empty( self::$mutation ) ) ? self::$mutation : null;
}
/**
* Add the email as a nonNull field for update mutations
*
* @return array
*/
private static function input_fields() {
/**
* Update mutations require an ID to be passed
*/
return array_merge(
UserMutation::input_fields(),
[
'username' => [
'type' => Types::non_null( Types::string() ),
// translators: the placeholder is the name of the type of post object being updated
'description' => __( 'A string that contains the user\'s username for logging in.', 'wp-graphql' ),
],
]
);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace WPGraphQL\Type\User\Mutation;
use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Types;
/**
* Class UserDelete
*
* @package WPGraphQL\Type\User\Mutation
*/
class UserDelete {
/**
* Stores the user delete mutation
*
* @var array $mutation
* @access private
*/
private static $mutation;
/**
* Processes the user delete mutation
*
* @return array|null
* @access public
*/
public static function mutate() {
if ( empty( self::$mutation ) ) {
self::$mutation = Relay::mutationWithClientMutationId( [
'name' => 'DeleteUser',
'description' => __( 'Delete a user object', 'wp-graphql' ),
'inputFields' => [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The ID of the user you want to delete', 'wp-graphql' ),
],
'reassignId' => [
'type' => Types::id(),
'description' => __( 'Reassign posts and links to new User ID.', 'wp-graphql' ),
]
],
'outputFields' => [
'deletedId' => [
'type' => Types::id(),
'description' => __( 'The ID of the user that you just deleted', 'wp-graphql' ),
'resolve' => function( $payload ) {
$deleted = (object) $payload['userObject'];
return ( ! empty( $deleted->ID ) ) ? Relay::toGlobalId( 'user', $deleted->ID ) : null;
}
],
'user' => [
'type' => Types::user(),
'description' => __( 'The user object for the user you are trying to delete', 'wp-graphql' ),
'resolve' => function( $payload ) {
$deleted = (object) $payload['userObject'];
return ( ! empty( $deleted ) ) ? $deleted : null;
}
]
],
'mutateAndGetPayload' => function( $input ) {
/**
* Get the ID from the global ID
*/
$id_parts = Relay::fromGlobalId( $input['id'] );
if ( ! current_user_can( 'delete_users' ) ) {
throw new UserError( __( 'Sorry, you are not allowed to delete users.', 'wp-graphql' ) );
}
/**
* Retrieve the user object before it's deleted
*/
$user_before_delete = get_user_by( 'id', absint( $id_parts['id'] ) );
/**
* Throw an error if the user we are trying to delete doesn't exist
*/
if ( false === $user_before_delete ) {
throw new UserError( __( 'Could not find an existing user to delete', 'wp-graphql' ) );
}
/**
* Get the DB id for the user to reassign posts to from the relay ID.
*/
$reassign_id_parts = ( ! empty( $input['reassignId'] ) ) ? Relay::fromGlobalId( $input['reassignId'] ) : null;
$reassign_id = ( ! empty( $reassign_id_parts ) ) ? absint( $reassign_id_parts['id'] ) : null;
/**
* If the wp_delete_user doesn't exist yet, load the file in which it is
* registered so it is available in this context. I think we need to
* load this manually here because WordPress only uses this
* function on the user edit screen normally.
*/
if ( ! function_exists( 'wp_delete_user' ) ) {
require_once( ABSPATH . 'wp-admin/includes/user.php' );
}
if ( is_multisite() ) {
$deleted_user = wpmu_delete_user( absint( $id_parts['id'] ) );
} else {
$deleted_user = wp_delete_user( absint( $id_parts['id'] ), $reassign_id );
}
if ( true !== $deleted_user ) {
throw new UserError( __( 'Could not delete the user.', 'wp-grapgql' ) );
}
return [
'userObject' => $user_before_delete,
];
}
] );
}
return ( ! empty( self::$mutation ) ) ? self::$mutation : null;
}
}

View File

@@ -0,0 +1,291 @@
<?php
namespace WPGraphQL\Type\User\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class UserMutation
*
* @package WPGraphQL\Type\User\Mutation
*/
class UserMutation {
/**
* Stores the input fields static definition
*
* @var array $input_fields
* @access private
*/
private static $input_fields = [];
/**
* Defines the accepted input arguments
*
* @return array|null
* @access public
*/
public static function input_fields() {
if ( empty( self::$input_fields ) ) {
$input_fields = [
'password' => [
'type' => Types::string(),
'description' => __( 'A string that contains the plain text password for the user.', 'wp-graphql' ),
],
'nicename' => [
'type' => Types::string(),
'description' => __( 'A string that contains a URL-friendly name for the user. The default is the user\'s username.', 'wp-graphql' ),
],
'websiteUrl' => [
'type' => Types::string(),
'description' => __( 'A string containing the user\'s URL for the user\'s web site.', 'wp-grapql' ),
],
'email' => [
'type' => Types::string(),
'description' => __( 'A string containing the user\'s email address.', 'wp-graphql' ),
],
'displayName' => [
'type' => Types::string(),
'description' => __( 'A string that will be shown on the site. Defaults to user\'s username. It is likely that you will want to change this, for both appearance and security through obscurity (that is if you dont use and delete the default admin user).', 'wp-graphql' ),
],
'nickname' => [
'type' => Types::string(),
'description' => __( 'The user\'s nickname, defaults to the user\'s username.', 'wp-graphql' ),
],
'firstName' => [
'type' => Types::string(),
'description' => __( ' The user\'s first name.', 'wp-graphql' ),
],
'lastName' => [
'type' => Types::string(),
'description' => __( 'The user\'s last name.', 'wp-graphql' ),
],
'description' => [
'type' => Types::string(),
'description' => __( 'A string containing content about the user.', 'wp-graphql' ),
],
'richEditing' => [
'type' => Types::string(),
'description' => __( 'A string for whether to enable the rich editor or not. False if not empty.', 'wp-graphql' ),
],
'registered' => [
'type' => Types::string(),
'description' => __( 'The date the user registered. Format is Y-m-d H:i:s.', 'wp-graphql' ),
],
'roles' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'An array of roles to be assigned to the user.', 'wp-graphql' ),
],
'jabber' => [
'type' => Types::string(),
'description' => __( 'User\'s Jabber account.', 'wp-graphql' ),
],
'aim' => [
'type' => Types::string(),
'description' => __( 'User\'s AOL IM account.', 'wp-graphql' ),
],
'yim' => [
'type' => Types::string(),
'description' => __( 'User\'s Yahoo IM account.', 'wp-graphql' ),
],
'locale' => [
'type' => Types::string(),
'description' => __( 'User\'s locale.', 'wp-graphql' ),
],
];
/**
* Filters all of the fields available for input
*
* @var array $input_fields
*/
self::$input_fields = apply_filters( 'graphql_user_mutation_input_fields', $input_fields );
}
return ( ! empty( self::$input_fields ) ) ? self::$input_fields : null;
}
/**
* Maps the GraphQL input to a format that the WordPress functions can use
*
* @param array $input Data coming from the GraphQL mutation query input
* @param string $mutation_name Name of the mutation being performed
*
* @access public
* @return array
*/
public static function prepare_user_object( $input, $mutation_name ) {
$insert_user_args = [];
if ( ! empty( $input['password'] ) ) {
$insert_user_args['user_pass'] = $input['password'];
} else {
$insert_user_args['user_pass'] = null;
}
if ( ! empty( $input['username'] ) ) {
$insert_user_args['user_login'] = $input['username'];
}
if ( ! empty( $input['nicename'] ) ) {
$insert_user_args['user_nicename'] = $input['nicename'];
}
if ( ! empty( $input['websiteUrl'] ) ) {
$insert_user_args['user_url'] = esc_url( $input['websiteUrl'] );
}
if ( ! empty( $input['email'] ) ) {
if ( false === is_email( apply_filters( 'pre_user_email', $input['email'] ) ) ) {
throw new UserError( __( 'The email address you are trying to use is invalid', 'graphql' ) );
}
$insert_user_args['user_email'] = $input['email'];
}
if ( ! empty( $input['displayName'] ) ) {
$insert_user_args['display_name'] = $input['displayName'];
}
if ( ! empty( $input['nickname'] ) ) {
$insert_user_args['nickname'] = $input['nickname'];
}
if ( ! empty( $input['firstName'] ) ) {
$insert_user_args['first_name'] = $input['firstName'];
}
if ( ! empty( $input['lastName'] ) ) {
$insert_user_args['last_name'] = $input['lastName'];
}
if ( ! empty( $input['description'] ) ) {
$insert_user_args['description'] = $input['description'];
}
if ( ! empty( $input['richEditing'] ) ) {
$insert_user_args['rich_editing'] = $input['richEditing'];
}
if ( ! empty( $input['registered'] ) ) {
$insert_user_args['user_registered'] = $input['registered'];
}
if ( ! empty( $input['roles'] ) ) {
/**
* Pluck the first role out of the array since the insert and update functions only
* allow one role to be set at a time. We will add all of the roles passed to the
* mutation later on after the initial object has been created or updated.
*/
$insert_user_args['role'] = $input['roles'][0];
}
if ( ! empty( $input['locale'] ) ) {
$insert_user_args['locale'] = $input['locale'];
}
/**
* Filters the mappings for input to arguments
*
* @var array $insert_user_args The arguments to ultimately be passed to the WordPress function
* @var array $input Input data from the GraphQL mutation
* @var string $mutation_name What user mutation is being performed for context
*/
$insert_user_args = apply_filters( 'graphql_user_insert_post_args', $insert_user_args, $input, $mutation_name );
return $insert_user_args;
}
/**
* This updates additional data related to the user object after the initial mutation has happened
*
* @param int $user_id The ID of the user being mutated
* @param array $input The input data from the GraphQL query
* @param string $mutation_name Name of the mutation currently being run
* @param AppContext $context The AppContext passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the Resolve Tree
*/
public static function update_additional_user_object_data( $user_id, $input, $mutation_name, AppContext $context, ResolveInfo $info ) {
$roles = ! empty( $input['roles'] ) ? $input['roles'] : [];
self::add_user_roles( $user_id, $roles );
/**
* Run an action after the additional data has been updated. This is a great spot to hook into to
* update additional data related to users, such as setting relationships, updating additional usermeta,
* or sending emails to Kevin... whatever you need to do with the userObject.
*
* @param int $user_id The ID of the user being mutated
* @param array $input The input for the mutation
* @param string $mutation_name The name of the mutation (ex: create, update, delete)
* @param AppContext $context The AppContext passed down the resolve tree
* @param ResolveInfo $info The ResolveInfo passed down the Resolve Tree
*/
do_action( 'graphql_user_object_mutation_update_additional_data', $user_id, $input, $mutation_name, $context, $info );
}
/**
* Method to add user roles to a user object
*
* @param int $user_id The ID of the user
* @param array $roles List of roles that need to get added to the user
*
* @access private
*/
private static function add_user_roles( $user_id, $roles ) {
if ( empty( $roles ) || ! is_array( $roles ) ) {
return;
}
$user = get_user_by( 'ID', $user_id );
if ( false !== $user ) {
foreach ( $roles as $role ) {
self::verify_user_role( $role );
$user->add_role( $role );
}
}
}
/**
* Method to check if the user role is valid, and if the current user has permission to add, or remove it from a
* user.
*
* @param string $role Name of the role trying to get added to a user object
*
* @return bool
* @throws \Exception
* @access private
*/
private static function verify_user_role( $role ) {
/**
* The function for this is only loaded on admin pages. See note: https://codex.wordpress.org/Function_Reference/get_editable_roles#Notes
*/
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/admin.php';
}
$editable_roles = get_editable_roles();
if ( empty( $editable_roles[ $role ] ) ) {
// Translators: %s is the name of the role that can't be added to the user.
throw new UserError( sprintf( __( 'Sorry, you are not allowed to give this the following role: %s.', 'wp-graphql' ), $role ) );
} else {
return true;
}
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace WPGraphQL\Type\User\Mutation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Types;
/**
* Class UserUpdate
*
* @package WPGraphQL\Type\User\Mutation
*/
class UserUpdate {
/**
* Stores the user update mutation
*
* @var array $mutation
*/
private static $mutation;
/**
* Process the user update mutation
*
* @return array|null
* @access public
*/
public static function mutate() {
if ( empty( self::$mutation ) ) {
self::$mutation = Relay::mutationWithClientMutationId( [
'name' => 'UpdateUser',
'description' => 'Updates a user object',
'inputFields' => self::input_fields(),
'outputFields' => [
'user' => [
'type' => Types::user(),
'description' => __( 'The updated user', 'wp-graphql' ),
'resolve' => function( $payload ) {
return get_user_by( 'ID', $payload['userId'] );
}
]
], 'mutateAndGetPayload' => function( $input, AppContext $context, ResolveInfo $info ) {
$id_parts = ! empty( $input['id'] ) ? Relay::fromGlobalId( $input['id'] ) : null;
$existing_user = get_user_by( 'ID', $id_parts['id'] );
/**
* If there's no existing user, throw an exception
*/
if ( empty( $id_parts['id'] ) || false === $existing_user ) {
throw new UserError( $id_parts['id'] );
}
if ( ! current_user_can( 'edit_users' ) ) {
throw new UserError( __( 'You do not have the appropriate capabilities to perform this action', 'wp-graphql' ) );
}
$user_args = UserMutation::prepare_user_object( $input, 'userCreate' );
$user_args['ID'] = absint( $id_parts['id'] );
/**
* If the query is trying to modify the users role, but doesn't have permissions to do so, throw an exception
*/
if ( ! current_user_can( 'promote_users' ) && isset( $user_args['role'] ) ) {
throw new UserError( __( 'You do not have the appropriate capabilities to change this users role.', 'wp-graphql' ) );
}
/**
* Update the user
*/
$user_id = wp_update_user( $user_args );
/**
* Throw an exception if the post failed to create
*/
if ( is_wp_error( $user_id ) ) {
$error_message = $user_id->get_error_message();
if ( ! empty( $error_message ) ) {
throw new UserError( esc_html( $error_message ) );
} else {
throw new UserError( __( 'The user failed to update but no error was provided', 'wp-graphql' ) );
}
}
/**
* If the $user_id is empty, we should throw an exception
*/
if ( empty( $user_id ) ) {
throw new UserError( __( 'The user failed to update', 'wp-graphql' ) );
}
/**
* Update additional user data
*/
UserMutation::update_additional_user_object_data( $user_id, $input, 'update', $context, $info );
/**
* Return the new user ID
*/
return [
'userId' => $user_id,
];
}
] );
}
return ( ! empty( self::$mutation ) ) ? self::$mutation : null;
}
/**
* Add the id as a nonNull field for update mutations
*
* @return array
*/
private static function input_fields() {
/**
* Update mutations require an ID to be passed
*/
return array_merge(
[
'id' => [
'type' => Types::non_null( Types::id() ),
// translators: the placeholder is the name of the type of post object being updated
'description' => __( 'The ID of the user', 'wp-graphql' ),
],
],
UserMutation::input_fields()
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WPGraphQL\Type\User;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Types;
/**
* Class UserQuery
* @package WPGraphQL\Type\TermObject
* @Since 0.0.5
*/
class UserQuery {
/**
* Holds the root_query field definition
* @var array $root_query
* @since 0.0.5
*/
private static $root_query;
/**
* Method that returns the root query field definition for the post object type
*
* @return array
* @since 0.0.5
*/
public static function root_query() {
if ( null === self::$root_query ) {
self::$root_query = [
'type' => Types::user(),
'description' => __( 'Returns a user', 'wp-graphql' ),
'args' => [
'id' => Types::non_null( Types::id() ),
],
'resolve' => function( $source, array $args, AppContext $context, ResolveInfo $info ) {
$id_components = Relay::fromGlobalId( $args['id'] );
return DataSource::resolve_user( $id_components['id'] );
},
];
}
return self::$root_query;
}
}

View File

@@ -0,0 +1,301 @@
<?php
namespace WPGraphQL\Type\User;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQLRelay\Relay;
use WPGraphQL\AppContext;
use WPGraphQL\Type\Comment\Connection\CommentConnectionDefinition;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionDefinition;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Types;
/**
* Class UserType
* @package WPGraphQL\Type
* @since 0.0.5
*/
class UserType extends WPObjectType {
/**
* Holds the type name
* @var string $type_name
*/
private static $type_name;
/**
* This holds the field definitions
* @var array $fields
* @since 0.0.5
*/
private static $fields;
/**
* UserType constructor.
* @since 0.0.5
*/
public function __construct() {
/**
* Set the type_name
* @since 0.0.5
*/
self::$type_name = 'User';
$config = [
'name' => self::$type_name,
'description' => __( 'A User object', 'wp-graphql' ),
'fields' => self::fields(),
'interfaces' => [ self::node_interface() ],
];
parent::__construct( $config );
}
/**
* fields
*
* This defines the fields for the UserType. The fields are passed through a filter so the shape of the schema
* can be modified
*
* @return array|\GraphQL\Type\Definition\FieldDefinition[]
* @since 0.0.5
*/
private static function fields() {
if ( null === self::$fields ) {
self::$fields = function() {
$fields = [
'id' => [
'type' => Types::non_null( Types::id() ),
'description' => __( 'The globally unique identifier for the user', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ( ! empty( $info->parentType ) && ! empty( $user->ID ) ) ? Relay::toGlobalId( 'user', $user->ID ) : null;
},
],
'capabilities' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'This field is the id of the user. The id of the user matches WP_User->ID field and the value in the ID column for the `users` table in SQL.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
if ( ! empty( $user->allcaps ) ) {
// Filters list for capabilities the user has.
$capabilities = array_keys( array_filter( $user->allcaps, function( $cap ) {
return true === $cap;
} ) );
}
return ! empty( $capabilities ) ? $capabilities : null;
},
],
'capKey' => [
'type' => Types::string(),
'description' => __( 'User metadata option name. Usually it will be `wp_capabilities`.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->cap_key ) ? $user->cap_key : null;
},
],
'roles' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'A list of roles that the user has. Roles can be used for querying for certain types of users, but should not be used in permissions checks.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->roles ) ? $user->roles : null;
},
],
'email' => [
'type' => Types::string(),
'description' => __( 'Email of the user. This is equivalent to the WP_User->user_email property.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->user_email ) ? $user->user_email : null;
},
],
'firstName' => [
'type' => Types::string(),
'description' => __( 'First name of the user. This is equivalent to the WP_User->user_first_name property.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->first_name ) ? $user->first_name : null;
},
],
'lastName' => [
'type' => Types::string(),
'description' => __( 'Last name of the user. This is equivalent to the WP_User->user_last_name property.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->last_name ) ? $user->last_name : null;
},
],
'extraCapabilities' => [
'type' => Types::list_of( Types::string() ),
'description' => __( 'A complete list of capabilities including capabilities inherited from a role. This is equivalent to the array keys of WP_User->allcaps.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->allcaps ) ? array_keys( $user->allcaps ) : null;
},
],
'description' => [
'type' => Types::string(),
'description' => __( 'Description of the user.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->description ) ? $user->description : null;
},
],
'username' => [
'type' => Types::string(),
'description' => __( 'Username for the user. This field is equivalent to WP_User->user_login.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, $context, ResolveInfo $info ) {
return ! empty( $user->user_login ) ? $user->user_login : null;
},
],
'name' => [
'type' => Types::string(),
'description' => __( 'Display name of the user. This is equivalent to the WP_User->dispaly_name property.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->display_name ) ? $user->display_name : null;
},
],
'registeredDate' => [
'type' => Types::string(),
'description' => __( 'The date the user registered or was created. The field follows a full ISO8601 date string format.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->user_registered ) ? date( 'c', strtotime( $user->user_registered ) ) : null;
},
],
'nickname' => [
'type' => Types::string(),
'description' => __( 'Nickname of the user.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->nickname ) ? $user->nickname : null;
},
],
'url' => [
'type' => Types::string(),
'description' => __( 'A website url that is associated with the user.', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->user_url ) ? $user->user_url : null;
},
],
'slug' => [
'type' => Types::string(),
'description' => __( 'The slug for the user. This field is equivalent to WP_User->user_nicename', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->user_nicename ) ? $user->user_nicename : null;
},
],
'nicename' => [
'type' => Types::string(),
'description' => __( 'The nicename for the user. This field is equivalent to WP_User->user_nicename', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->user_nicename ) ? $user->user_nicename : null;
},
],
'locale' => [
'type' => Types::string(),
'description' => __( 'The preferred language locale set for the user. Value derived from get_user_locale().', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
$user_locale = get_user_locale( $user );
return ! empty( $user_locale ) ? $user_locale : null;
},
],
'userId' => [
'type' => Types::int(),
'description' => __( 'The Id of the user. Equivelant to WP_User->ID', 'wp-graphql' ),
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
return ! empty( $user->ID ) ? $user->ID : null;
},
],
'avatar' => [
'type' => Types::avatar(),
'description' => __( 'Avatar object for user. The avatar object can be retrieved in different sizes by specifying the size argument.', 'wp-graphql' ),
'args' => [
'size' => [
'type' => Types::int(),
'description' => __( 'The size attribute of the avatar field can be used to fetch avatars of different sizes. The value corresponds to the dimension in pixels to fetch. The default is 96 pixels.', 'wp-graphql' ),
'defaultValue' => 96,
],
'forceDefault' => [
'type' => Types::boolean(),
'description' => __( 'Whether to always show the default image, never the Gravatar. Default false' ),
],
'rating' => [
'type' => new WPEnumType( [
'name' => 'AvatarRatingEnum',
'description' => __( 'What rating to display avatars up to. Accepts \'G\', \'PG\', \'R\', \'X\', and are judged in that order. Default is the value of the \'avatar_rating\' option', 'wp-graphql' ),
'values' => [
'G' => [
'value' => 'G',
],
'PG' => [
'value' => 'PG',
],
'R' => [
'value' => 'R',
],
'X' => [
'value' => 'X',
],
],
] ),
],
],
'resolve' => function( \WP_User $user, $args, AppContext $context, ResolveInfo $info ) {
$avatar_args = [];
if ( is_numeric( $args['size'] ) ) {
$avatar_args['size'] = absint( $args['size'] );
if ( ! $avatar_args['size'] ) {
$avatar_args['size'] = 96;
}
}
if ( ! empty( $args['forceDefault'] ) && true === $args['forceDefault'] ) {
$avatar_args['force_default'] = true;
}
if ( ! empty( $args['rating'] ) ) {
$avatar_args['rating'] = esc_sql( $args['rating'] );
}
$avatar = get_avatar_data( $user->ID, $avatar_args );
return ( ! empty( $avatar ) && true === $avatar['found_avatar'] ) ? $avatar : null;
},
],
'comments' => CommentConnectionDefinition::connection( 'User' ),
];
/**
* Get the allowed_post_types so that we can create a connection from users
* to post_types
*
* @since 0.0.5
*/
$allowed_post_types = \WPGraphQL::$allowed_post_types;
/**
* Add connection to each of the allowed post_types as users can have connections
* to any post_type.
*
* @since 0.0.5
*/
if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) {
foreach ( $allowed_post_types as $post_type ) {
// @todo: maybe look into narrowing this based on permissions?
$post_type_object = get_post_type_object( $post_type );
$fields[ $post_type_object->graphql_plural_name ] = PostObjectConnectionDefinition::connection( $post_type_object, 'User' );
}
}
/**
* This prepares the fields by sorting them and applying a filter for adjusting the schema.
* Because these fields are implemented via a closure the prepare_fields needs to be applied
* to the fields directly instead of being applied to all objects extending
* the WPObjectType class.
*/
return self::prepare_fields( $fields, self::$type_name );
};
}
return self::$fields;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace WPGraphQL\Type;
use GraphQL\Type\Definition\EnumType;
/**
* Class WPEnumType
*
* EnumTypes should extend this class to have filters and sorting applied, etc.
*
* @package WPGraphQL\Type
*/
class WPEnumType extends EnumType {
/**
* WPInputObjectType constructor.
*
* @param array $config
*/
public function __construct( $config ) {
$config['name'] = ucfirst( $config['name'] );
$config['values'] = self::prepare_values( $config['values'], $config['name'] );
parent::__construct( $config );
}
/**
* prepare_values
*
* This function sorts the values and applies a filter to allow for easily
* extending/modifying the shape of the Schema for the enum.
*
* @param array $values
* @param string $type_name
* @return mixed
* @since 0.0.5
*/
private static function prepare_values( $values, $type_name ) {
/**
* Pass the values through a filter
*
* lcfirst( $type_name ) filter was added for backward compatibility
*
* @param array $values
*
* @since 0.0.5
*/
$values = apply_filters( 'graphql_' . lcfirst( $type_name ) . '_values', $values );
$values = apply_filters( 'graphql_' . $type_name . '_values', $values );
/**
* Sort the values alphabetically by key. This makes reading through docs much easier
* @since 0.0.5
*/
ksort( $values );
/**
* Return the filtered, sorted $fields
* @since 0.0.5
*/
return $values;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace WPGraphQL\Type;
use GraphQL\Type\Definition\InputObjectType;
/**
* Class WPInputObjectType
*
* Input types should extend this class to take advantage of the helper methods for formatting
* and adding consistent filters.
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class WPInputObjectType extends InputObjectType {
/**
* WPInputObjectType constructor.
*
* @param array $config The configuration for the InputObjectType
*/
public function __construct( $config = [] ) {
if ( ! empty( $config['fields'] ) && is_array( $config['fields'] ) ) {
$config['fields'] = self::prepare_fields( $config['fields'], $config['name'], $config );
}
parent::__construct( $config );
}
/**
* prepare_fields
*
* This function sorts the fields and applies a filter to allow for easily
* extending/modifying the shape of the Schema for the type.
*
* @param array $fields
* @param string $type_name
* @param array $config
* @return mixed
* @since 0.0.5
*/
public static function prepare_fields( array $fields, $type_name, $config = [] ) {
/**
* Filter all object fields, passing the $typename as a param
*
* This is useful when several different types need to be easily filtered at once. . .for example,
* if ALL types with a field of a certain name needed to be adjusted, or something to that tune
*
* @param array $fields The array of fields for the object config
* @param string $type_name The name of the object type
*/
$fields = apply_filters( 'graphql_input_fields', $fields, $type_name, $config );
/**
* Pass the fields through a filter
*
* @param array $fields
* @param string $type_name
* @param array $config
* @since 0.0.5
*/
$fields = apply_filters( 'graphql_' . lcfirst( $type_name ) . '_fields', $fields, $type_name, $config );
$fields = apply_filters( "graphql_{$type_name}_fields", $fields, $type_name, $config );
/**
* Sort the fields alphabetically by key. This makes reading through docs much easier
* @since 0.0.2
*/
ksort( $fields );
/**
* Return the filtered, sorted $fields
* @since 0.0.5
*/
return $fields;
}
/**
* format_enum_name
*
* This formats enum_names to be all caps with underscores for spaces/word-breaks
*
* @param $name
* @return string
* @since 0.0.5
*/
public static function format_enum_name( $name ) {
return strtoupper( preg_replace( '/[^A-Za-z0-9]/i', '_', $name ) );
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace WPGraphQL\Type;
use GraphQL\Type\Definition\ObjectType;
use WPGraphQL\Data\DataSource;
/**
* Class WPObjectType
*
* Object Types should extend this class to take advantage of the helper methods
* and consistent filters.
*
* @package WPGraphQL\Type
* @since 0.0.5
*/
class WPObjectType extends ObjectType {
/**
* Holds the $prepared_fields definition for the PostObjectType
*
* @var $fields
*/
private static $prepared_fields;
/**
* Holds the node_interface definition allowing WPObjectTypes
* to easily define themselves as a node type by implementing
* self::$node_interface
*
* @var $node_interface
* @since 0.0.5
*/
private static $node_interface;
/**
* WPObjectType constructor.
*
* @since 0.0.5
*/
public function __construct( $config ) {
/**
* Filter the config of WPObjectType
*
* @param array $config Array of configuration options passed to the WPObjectType when instantiating a new type
* @param Object $this The instance of the WPObjectType class
*/
$config = apply_filters( 'graphql_wp_object_type_config', $config, $this );
/**
* Set the Types to start with capitals
*/
$config['name'] = ucfirst( $config['name'] );
/**
* Filter the Type config
*/
apply_filters( 'graphql_type_config', $config );
/**
* Run an action when the WPObjectType is instantiating
*
* @param array $config Array of configuration options passed to the WPObjectType when instantiating a new type
* @param Object $this The instance of the WPObjectType class
*/
do_action( 'graphql_wp_object_type', $config, $this );
parent::__construct( $config );
}
/**
* node_interface
*
* This returns the node_interface definition allowing
* WPObjectTypes to easily implement the node_interface
*
* @return array|\WPGraphQL\Data\node_interface
* @since 0.0.5
*/
public static function node_interface() {
if ( null === self::$node_interface ) {
$node_interface = DataSource::get_node_definition();
self::$node_interface = $node_interface['nodeInterface'];
}
return self::$node_interface;
}
/**
* prepare_fields
*
* This function sorts the fields and applies a filter to allow for easily
* extending/modifying the shape of the Schema for the type.
*
* @param array $fields
* @param string $type_name
*
* @return mixed
* @since 0.0.5
*/
public static function prepare_fields( $fields, $type_name ) {
if ( null === self::$prepared_fields ) {
self::$prepared_fields = [];
}
if ( empty( self::$prepared_fields[ $type_name ] ) ) {
/**
* Filter all object fields, passing the $typename as a param
*
* This is useful when several different types need to be easily filtered at once. . .for example,
* if ALL types with a field of a certain name needed to be adjusted, or something to that tune
*
* @param array $fields The array of fields for the object config
* @param string $type_name The name of the object type
*/
$fields = apply_filters( 'graphql_object_fields', $fields, $type_name );
/**
* Filter once with lowercase, once with uppercase for Back Compat.
*/
$lc_type_name = lcfirst( $type_name );
$uc_type_name = ucfirst( $type_name );
/**
* Filter the fields with the typename explicitly in the filter name
*
* This is useful for more targeted filtering, and is applied after the general filter, to allow for
* more specific overrides
*
* @param array $fields The array of fields for the object config
*/
$fields = apply_filters( "graphql_{$lc_type_name}_fields", $fields );
/**
* Filter the fields with the typename explicitly in the filter name
*
* This is useful for more targeted filtering, and is applied after the general filter, to allow for
* more specific overrides
*
* @param array $fields The array of fields for the object config
*/
$fields = apply_filters( "graphql_{$uc_type_name}_fields", $fields );
/**
* This sorts the fields alphabetically by the key, which is super handy for making the schema readable,
* as it ensures it's not output in just random order
*/
ksort( $fields );
self::$prepared_fields[ $type_name ] = $fields;
}
return ! empty( self::$prepared_fields[ $type_name ] ) ? self::$prepared_fields[ $type_name ] : null;
}
}

View File

@@ -0,0 +1,850 @@
<?php
namespace WPGraphQL;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use WPGraphQL\Type\Avatar\AvatarType;
use WPGraphQL\Type\Comment\CommentType;
use WPGraphQL\Type\CommentAuthor\CommentAuthorType;
use WPGraphQL\Type\EditLock\EditLockType;
use WPGraphQL\Type\Enum\MimeTypeEnumType;
use WPGraphQL\Type\Enum\PostObjectFieldFormatEnumType;
use WPGraphQL\Type\Enum\PostStatusEnumType;
use WPGraphQL\Type\Enum\MediaItemStatusEnumType;
use WPGraphQL\Type\Enum\PostTypeEnumType;
use WPGraphQL\Type\Enum\RelationEnumType;
use WPGraphQL\Type\Enum\TaxonomyEnumType;
use WPGraphQL\Type\Setting\SettingType;
use WPGraphQL\Type\Settings\SettingsType;
use WPGraphQL\Type\PostObject\Connection\PostObjectConnectionArgs;
use WPGraphQL\Type\RootMutationType;
use WPGraphQL\Type\RootQueryType;
use WPGraphQL\Type\Plugin\PluginType;
use WPGraphQL\Type\PostObject\PostObjectType;
use WPGraphQL\Type\PostType\PostTypeType;
use WPGraphQL\Type\Taxonomy\TaxonomyType;
use WPGraphQL\Type\TermObject\Connection\TermObjectConnectionArgs;
use WPGraphQL\Type\TermObject\TermObjectType;
use WPGraphQL\Type\Theme\ThemeType;
use WPGraphQL\Type\Union\CommentAuthorUnionType;
use WPGraphQL\Type\Union\PostObjectUnionType;
use WPGraphQL\Type\Union\TermObjectUnionType;
use WPGraphQL\Type\User\Connection\UserConnectionArgs;
use WPGraphQL\Type\User\UserType;
/**
* Class Types - Acts as a registry and factory for Types.
*
* Each "type" is static ensuring that it will only be instantiated once and can be re-used
* throughout the system. The types that are "dynamic" (such as post_types, taxonomies, etc)
* are added as a sub-property to the Types class based on their unique identifier, and are
* therefore only instantiated once as well.
*
* @since 0.0.5
* @package WPGraphQL
*/
class Types {
/**
* Stores the avatar type object
*
* @var AvatarType object $avatar
* @since 0.5.0
* @access private
*/
private static $avatar;
/**
* Stores the comment type object
*
* @var CommentType object $comment
* @since 0.5.0
* @access private
*/
private static $comment;
/**
* Stores the comment author type object
*
* @var CommentAuthorType object $comment_author
* @since 0.0.21
* @access private
*/
private static $comment_author;
/**
* Stores the comment author union type config
*
* @var CommentAuthorUnionType object $comment_author_union
* @since 0.0.21
* @access private
*/
private static $comment_author_union;
/**
* Stores the EditLock definition
*
* @var EditLockType object $edit_lock
* @access private
*/
private static $edit_lock;
/**
* Stores the mime type enum object
*
* @var MimeTypeEnumType object $mime_type_enum
* @since 0.5.0
* @access private
*/
private static $mime_type_enum;
/**
* Stores the plugin type object
*
* @var PluginType $plugin
* @since 0.5.0
* @access private
*/
private static $plugin;
/**
* Stores the post object type
*
* @var PostObjectType $post_object
* @since 0.5.0
* @access private
*/
private static $post_object;
/**
* Stores the post object type query args
*
* @var PostObjectConnectionArgs object $post_object_query_args
* @since 0.5.0
* @access private
*/
private static $post_object_query_args;
/**
* Stores the post object union type config
*
* @var PostObjectUnionType object $post_object_union
* @since 0.0.6
* @access private
*/
private static $post_object_union;
/**
* Stores the post object field format enum type object
*
* @var PostObjectFieldFormatEnumType object $post_object_field_format_enum
* @since 0.0.18
* @access private
*/
private static $post_object_field_format_enum;
/**
* Stores the post status enum type object
*
* @var PostStatusEnumType object $post_status_enum
* @since 0.0.5
* @access private
*/
private static $post_status_enum;
/**
* Stores the media item (attachment) status enum type object
*
* @var MediaItemStatusEnumType object $media_item_status_enum
* @access private
*/
private static $media_item_status_enum;
/**
* Stores the post type enum type object
*
* @var PostTypeEnumType object $post_type_enum
* @since 0.5.0
* @access private
*/
private static $post_type_enum;
/**
* Stores the post type type object
*
* @var PostTypeType object $post_type
* @since 0.5.0
* @access private
*/
private static $post_type;
/**
* Stores the relation enum type object
*
* @var RelationEnumType object $relation_enum
* @since 0.5.0
* @access private
*/
private static $relation_enum;
/**
* Stores the root mutation type object
*
* @var RootMutationType object $root_mutation
* @since 0.0.6
* @access private
*/
private static $root_mutation;
/**
* Stores the root query type object
*
* @var RootQueryType object $root_query
* @since 0.5.0
* @access private
*/
private static $root_query;
/**
* Stores the setting object type
*
* @var SettingType object $setting
* @access private
*/
private static $setting;
/**
* Stores the settings object type
*
* @var SettingsType object $settings
* @access private
*/
private static $settings;
/**
* Stores the taxonomy type object
*
* @var TaxonomyType object $taxonomy
* @since 0.5.0
* @access private
*/
private static $taxonomy;
/**
* Stores the taxonomy enum type object
*
* @var TaxonomyEnumType object $taxonomy_enum
* @since 0.5.0
* @access private
*/
private static $taxonomy_enum;
/**
* Stores the term type object
*
* @var TermObjectType object $term_object
* @since 0.5.0
* @access private
*/
private static $term_object;
/**
* Stores the term object union definition
*
* @var TermObjectUnionType object $term_object_union
* @access private
*/
private static $term_object_union;
/**
* Stores the term object query args type
*
* @var TermObjectConnectionArgs object $term_object_query_args
* @since 0.5.0
* @access private
*/
private static $term_object_query_args;
/**
* Stores the theme type object
*
* @var ThemeType object $theme
* @since 0.5.0
* @access private
*/
private static $theme;
/**
* Stores the user type object
*
* @var UserType object $user
* @since 0.5.0
* @access private
*/
private static $user;
/**
* Stores the user connection query args type object
*
* @var UserConnectionArgs object $user_connection_query_args
* @since 0.5.0
* @access private
*/
private static $user_connection_query_args;
/**
* This returns the definition for the AvatarType
*
* @return AvatarType object
* @since 0.0.5
* @access public
*/
public static function avatar() {
return self::$avatar ? : ( self::$avatar = new AvatarType() );
}
/**
* This returns the definition for the CommentType
*
* @return CommentType object
* @since 0.0.5
* @access public
*/
public static function comment() {
return self::$comment ? : ( self::$comment = new CommentType() );
}
/**
* This returns the definition for the CommentAuthorType
*
* @return CommentAuthorType object
* @since 0.0.21
* @access public
*/
public static function comment_author() {
return self::$comment_author ? : ( self::$comment_author = new CommentAuthorType() );
}
/**
* This returns the definition for the PostObjectUnionType
*
* @return CommentAuthorUnionType object
* @since 0.0.21
* @access public
*/
public static function comment_author_union() {
return self::$comment_author_union ? : ( self::$comment_author_union = new CommentAuthorUnionType() );
}
/**
* This returns the definition for the EditLock type
*
* @return EditLockType object
* @access public
*/
public static function edit_lock() {
return self::$edit_lock ? : ( self::$edit_lock = new EditLockType() );
}
/**
* This returns the definition for the MimeTypeEnumType
*
* @return MimeTypeEnumType object
* @since 0.0.5
* @access public
*/
public static function mime_type_enum() {
return self::$mime_type_enum ? : ( self::$mime_type_enum = new MimeTypeEnumType() );
}
/**
* This returns the definition for the SettingType
*
* @return SettingType object
* @access public
*/
public static function setting( $setting_type ) {
if ( null === self::$setting ) {
self::$setting = [];
}
if ( empty( self::$setting[ $setting_type ] ) ) {
self::$setting[ $setting_type ] = new SettingType( $setting_type );
}
return ! empty( self::$setting[ $setting_type ] ) ? self::$setting[ $setting_type ] : null;
}
/**
* This returns the definition for the SettingsType
*
* @return SettingsType object
* @access public
*/
public static function settings() {
if ( empty( self::$settings ) ) {
self::$settings = new SettingsType();
}
return ! empty( self::$settings ) ? self::$settings : null;
}
/**
* This returns the definition for the PluginType
*
* @return PluginType object
* @since 0.0.5
* @access public
*/
public static function plugin() {
return self::$plugin ? : ( self::$plugin = new PluginType() );
}
/**
* This returns the definition for the PostObjectType
*
* @param string $post_type Name of the post type you want to retrieve the PostObjectType for
*
* @return PostObjectType object
* @since 0.0.5
* @access public
*/
public static function post_object( $post_type ) {
if ( null === self::$post_object ) {
self::$post_object = [];
}
if ( empty( self::$post_object[ $post_type ] ) ) {
self::$post_object[ $post_type ] = new PostObjectType( $post_type );
}
return ! empty( self::$post_object[ $post_type ] ) ? self::$post_object[ $post_type ] : null;
}
/**
* This returns the definition for the PostObjectUnionType
*
* @return PostObjectUnionType object
* @since 0.0.5
* @access public
*/
public static function post_object_union() {
return self::$post_object_union ? : ( self::$post_object_union = new PostObjectUnionType() );
}
/**
* This returns the definition for the PostObjectFieldFormatEnumType
*
* @return PostObjectFieldFormatEnumType object
* @since 0.1.18
* @access public
*/
public static function post_object_field_format_enum() {
return self::$post_object_field_format_enum ? : ( self::$post_object_field_format_enum = new PostObjectFieldFormatEnumType() );
}
/**
* This returns the definition for the PostStatusEnumType
*
* @return PostStatusEnumType object
* @since 0.0.5
* @access public
*/
public static function post_status_enum() {
return self::$post_status_enum ? : ( self::$post_status_enum = new PostStatusEnumType() );
}
/**
* This returns the definition for the MediaItemStatusEnumType
*
* @return MediaItemStatusEnumType object
* @access public
*/
public static function media_item_status_enum() {
return self::$media_item_status_enum ? : ( self::$media_item_status_enum = new MediaItemStatusEnumType() );
}
/**
* This returns the definition for the PostStatusEnumType
*
* @return PostTypeEnumType object
* @since 0.0.5
* @access public
*/
public static function post_type_enum() {
return self::$post_type_enum ? : ( self::$post_type_enum = new PostTypeEnumType() );
}
/**
* This returns the definition for the PostObjectConnectionArgs
* @param string $connection The connection the args belong to
* @return PostObjectConnectionArgs object
* @since 0.0.5
* @access public
*/
public static function post_object_query_args( $connection ) {
if ( null === self::$post_object_query_args ) {
self::$post_object_query_args = [];
}
if ( empty( self::$post_object_query_args[ $connection ] ) ) {
self::$post_object_query_args[ $connection ] = new PostObjectConnectionArgs( [], $connection );
}
return ! empty( self::$post_object_query_args[ $connection ] ) ? self::$post_object_query_args[ $connection ] : null;
}
/**
* This returns the definition for the PostTypeType
*
* @return PostTypeType object
* @since 0.0.5
* @access public
*/
public static function post_type() {
return self::$post_type ? : ( self::$post_type = new PostTypeType() );
}
/**
* This returns the definition for the RelationEnum
*
* @return RelationEnumType object
* @since 0.0.5
* @access public
*/
public static function relation_enum() {
return self::$relation_enum ? : ( self::$relation_enum = new RelationEnumType() );
}
/**
* This returns the definition for the RootMutationType
*
* @return RootMutationType object
* @since 0.0.8
* @access public
*/
public static function root_mutation() {
return self::$root_mutation ? : ( self::$root_mutation = new RootMutationType() );
}
/**
* This returns the definition for the RootQueryType
*
* @return RootQueryType object
* @since 0.0.5
* @access public
*/
public static function root_query() {
return self::$root_query ? : ( self::$root_query = new RootQueryType() );
}
/**
* This returns the definition for the TaxonomyType
*
* @return TaxonomyType object
* @since 0.0.5
* @access public
*/
public static function taxonomy() {
return self::$taxonomy ? : ( self::$taxonomy = new TaxonomyType() );
}
/**
* This returns the definition for the TaxonomyEnumType
*
* @return TaxonomyEnumType object
* @since 0.0.5
* @access public
*/
public static function taxonomy_enum() {
return self::$taxonomy_enum ? : ( self::$taxonomy_enum = new TaxonomyEnumType() );
}
/**
* This returns the definition for the TermObjectType
*
* @param string $taxonomy Name of the taxonomy you want to get the TermObjectType for
*
* @return TermObjectType object
* @since 0.0.5
* @access public
*/
public static function term_object( $taxonomy ) {
if ( null === self::$term_object ) {
self::$term_object = [];
}
if ( empty( self::$term_object[ $taxonomy ] ) ) {
self::$term_object[ $taxonomy ] = new TermObjectType( $taxonomy );
}
return ! empty( self::$term_object[ $taxonomy ] ) ? self::$term_object[ $taxonomy ] : null;
}
/**
* This returns the definition for the TermObjectConnectionArgs
*
* @param string $connection
* @return TermObjectConnectionArgs object
* @since 0.0.5
* @access public
*/
public static function term_object_query_args( $connection ) {
if ( null === self::$term_object_query_args ) {
self::$term_object_query_args = [];
}
if ( empty( self::$term_object_query_args[ $connection ] ) ) {
self::$term_object_query_args[ $connection ] = new TermObjectConnectionArgs( [], $connection );
}
return ! empty( self::$term_object_query_args[ $connection ] ) ? self::$term_object_query_args[ $connection ] : null;
}
/**
* This returns the definition for the termObjectUnionType
*
* @return TermObjectUnionType object
* @access public
*/
public static function term_object_union() {
return self::$term_object_union ? : ( self::$term_object_union = new TermObjectUnionType() );
}
/**
* This returns the definition for the ThemeType
*
* @return ThemeType object
* @since 0.0.5
* @access public
*/
public static function theme() {
return self::$theme ? : ( self::$theme = new ThemeType() );
}
/**
* This returns the definition for the UserType
*
* @return UserType object
* @since 0.0.5
* @access public
*/
public static function user() {
return self::$user ? : ( self::$user = new UserType() );
}
/**
* This returns the definition for the UserConnectionArgs
*
* @param string $connection The connection the args are for
* @return UserConnectionArgs object
* @since 0.0.5
* @access public
*/
public static function user_connection_query_args( $connection ) {
if ( null === self::$user_connection_query_args ) {
self::$user_connection_query_args = [];
}
if ( empty( self::$user_connection_query_args ) ) {
self::$user_connection_query_args[ $connection ] = new UserConnectionArgs( [], $connection );
}
return ! empty( self::$user_connection_query_args[ $connection ] ) ? self::$user_connection_query_args[ $connection ] : null;
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @return \GraphQL\Type\Definition\BooleanType
* @since 0.0.5
* @access public
*/
public static function boolean() {
return Type::boolean();
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @return \GraphQL\Type\Definition\FloatType
* @since 0.0.5
* @access public
*/
public static function float() {
return Type::float();
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @return \GraphQL\Type\Definition\idType
* @since 0.0.5
* @access public
*/
public static function id() {
return Type::id();
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @return \GraphQL\Type\Definition\IntType
* @since 0.0.5
* @access public
*/
public static function int() {
return Type::int();
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @return \GraphQL\Type\Definition\StringType
* @since 0.0.5
* @access public
*/
public static function string() {
return Type::string();
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @param object $type instance of GraphQL\Type\Definition\Type or callable returning instance
* of that class
*
* @return \GraphQL\Type\Definition\ListOfType
* @since 0.0.5
* @access public
*/
public static function list_of( $type ) {
return new ListOfType( $type );
}
/**
* This is a wrapper for the GraphQL type to give a consistent experience
*
* @param object $type instance of GraphQL\Type\Definition\Type or callable returning instance
* of that class
*
* @return \GraphQL\Type\Definition\NonNull
* @since 0.0.5
* @access public
*/
public static function non_null( $type ) {
return new NonNull( $type );
}
/**
* Resolve the type on the individual setting field
* for the settingsType
*
* @param $type
* @access public
*
* @return \GraphQL\Type\Definition\BooleanType|\GraphQL\Type\Definition\FloatType|\GraphQL\Type\Definition\IntType|\GraphQL\Type\Definition\StringType
*/
public static function get_type( $type ) {
switch ( $type ) {
case 'integer':
$type = self::int();
break;
case 'float':
case 'number':
$type = self::float();
break;
case 'boolean':
$type = self::boolean();
break;
case 'string':
default:
$type = self::string();
}
return $type;
}
/**
* Maps new input query args and sanitizes the input
*
* @param array $args The raw query args from the GraphQL query
* @param array $map The mapping of where each of the args should go
*
* @since 0.5.0
* @return array
* @access public
*/
public static function map_input( $args, $map ) {
if ( ! is_array( $args ) || ! is_array( $map ) ) {
return array();
}
$query_args = [];
foreach ( $args as $arg => $value ) {
if ( is_array( $value ) && ! empty( $value ) ) {
$value = array_map( function( $value ) {
if ( is_string( $value ) ) {
$value = sanitize_text_field( $value );
}
return $value;
}, $value );
} elseif ( is_string( $value ) ) {
$value = sanitize_text_field( $value );
}
if ( array_key_exists( $arg, $map ) ) {
$query_args[ $map[ $arg ] ] = $value;
} else {
$query_args[ $arg ] = $value;
}
}
return $query_args;
}
/**
* Checks the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* @since 4.7.0
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional. Local publication time. Default null.
* @return string|null ISO8601/RFC3339 formatted datetime.
*/
public static function prepare_date_response( $date_gmt, $date = null ) {
// Use the date if passed.
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
// Return null if $date_gmt is empty/zeros.
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
// Return the formatted datetime.
return mysql_to_rfc3339( $date_gmt );
}
}

View File

@@ -0,0 +1,257 @@
<?php
namespace WPGraphQL\Utils;
use GraphQL\Error\UserError;
use GraphQL\Executor\Executor;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Type\WPObjectType;
/**
* Class InstrumentSchema
*
* @package WPGraphQL\Data
*/
class InstrumentSchema {
/**
* @param \WPGraphQL\WPSchema $schema
*
* @return \WPGraphQL\WPSchema
*/
public static function instrument_schema( \WPGraphQL\WPSchema $schema ) {
$new_types = [];
$types = $schema->getTypeMap();
if ( ! empty( $types ) && is_array( $types ) ) {
foreach ( $types as $type_name => $type_object ) {
if ( $type_object instanceof ObjectType || $type_object instanceof WPObjectType ) {
$fields = $type_object->getFields();
$new_fields = self::wrap_fields( $fields, $type_name );
$new_type_object = $type_object;
$new_type_object->name = ucfirst( esc_html( $type_object->name ) );
$new_type_object->description = esc_html( $type_object->description );
$new_type_object->config['fields'] = $new_fields;
$new_types[ $type_name ] = $new_type_object;
}
}
}
if ( ! empty( $new_types ) && is_array( $new_types ) ) {
$schema->config['types'] = $new_types;
}
return $schema;
}
/**
* Wrap Fields
*
* This wraps fields to provide sanitization on fields output by introspection queries (description/deprecation
* reason) and provides hooks to resolvers.
*
* @param array $fields The fields configured for a Type
* @param string $type_name The Type name
*
* @return mixed
*/
protected static function wrap_fields( $fields, $type_name ) {
if ( ! empty( $fields ) && is_array( $fields ) ) {
foreach ( $fields as $field_key => $field ) {
if ( $field instanceof FieldDefinition ) {
/**
* Filter the field definition
*
* @param \GraphQL\Type\Definition\FieldDefinition $field The field definition
* @param string $type_name The name of the Type the field belongs to
*/
$field = apply_filters( 'graphql_field_definition', $field, $type_name );
/**
* Get the fields resolve function
*
* @since 0.0.1
*/
$field_resolver = ! empty( $field->resolveFn ) ? $field->resolveFn : null;
/**
* Sanitize the description and deprecation reason
*/
$field->description = ! empty( $field->description ) ? esc_html( $field->description ) : '';
$field->deprecationReason = ! empty( $field->deprecationReason ) ? esc_html( $field->deprecationReason ) : '';
/**
* Replace the existing field resolve method with a new function that captures data about
* the resolver to be stored in the resolver_report
*
* @since 0.0.1
*
* @param mixed $source The source passed down the Resolve Tree
* @param array $args The args for the field
* @param AppContext $context The AppContext passed down the ResolveTree
* @param ResolveInfo $info The ResolveInfo passed down the ResolveTree
*
* @use function|null $field_resolve_function
* @use string $type_name
* @use string $field_key
* @use object $field
*
* @return mixed
* @throws \Exception
*/
$field->resolveFn = function( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $field_resolver, $type_name, $field_key, $field ) {
/**
* Fire an action BEFORE the field resolves
*
* @param mixed $source The source passed down the Resolve Tree
* @param array $args The args for the field
* @param AppContext $context The AppContext passed down the ResolveTree
* @param ResolveInfo $info The ResolveInfo passed down the ResolveTree
* @param string $type_name The name of the type the fields belong to
* @param string $field_key The name of the field
* @param FieldDefinition $field The Field Definition for the resolving field
*/
do_action( 'graphql_before_resolve_field', $source, $args, $context, $info, $field_resolver, $type_name, $field_key, $field );
/**
* If the current field doesn't have a resolve function, use the defaultFieldResolver,
* otherwise use the $field_resolver
*/
if ( null === $field_resolver || ! is_callable( $field_resolver ) ) {
$result = Executor::defaultFieldResolver( $source, $args, $context, $info );
} else {
$result = call_user_func( $field_resolver, $source, $args, $context, $info );
}
/**
* Fire an action before the field resolves
*
* @param mixed $result The result of the field resolution
* @param mixed $source The source passed down the Resolve Tree
* @param array $args The args for the field
* @param AppContext $context The AppContext passed down the ResolveTree
* @param ResolveInfo $info The ResolveInfo passed down the ResolveTree
* @param string $type_name The name of the type the fields belong to
* @param string $field_key The name of the field
* @param FieldDefinition $field The Field Definition for the resolving field
* @param mixed $field_resolver The default field resolver
*/
$result = apply_filters( 'graphql_resolve_field', $result, $source, $args, $context, $info, $type_name, $field_key, $field, $field_resolver );
/**
* Fire an action AFTER the field resolves
*
* @param mixed $source The source passed down the Resolve Tree
* @param array $args The args for the field
* @param AppContext $context The AppContext passed down the ResolveTree
* @param ResolveInfo $info The ResolveInfo passed down the ResolveTree
* @param string $type_name The name of the type the fields belong to
* @param string $field_key The name of the field
* @param FieldDefinition $field The Field Definition for the resolving field
*/
do_action( 'graphql_after_resolve_field', $source, $args, $context, $info, $field_resolver, $type_name, $field_key, $field );
return $result;
};
}
}
}
/**
* Return the fields
*/
return $fields;
}
/**
* Check field permissions when resolving.
*
* This takes into account auth params defined in the Schema
*
* @param mixed $source The source passed down the Resolve Tree
* @param array $args The args for the field
* @param AppContext $context The AppContext passed down the ResolveTree
* @param ResolveInfo $info The ResolveInfo passed down the ResolveTree
* @param string $type_name The name of the type the fields belong to
* @param string $field_key The name of the field
* @param FieldDefinition $field The Field Definition for the resolving field
*
* @return bool|mixed
*/
public static function check_field_permissions( $source, $args, $context, $info, $field_resolver, $type_name, $field_key, $field ) {
/**
* Set the default auth error message
*/
$default_auth_error_message = __( 'You do not have permission to view this', 'wp-graphql' );
/**
* Filter the $auth_error
*/
$auth_error = apply_filters( 'graphql_field_resolver_auth_error_message', $default_auth_error_message, $field );
/**
* Check to see if
*/
if ( $field instanceof FieldDefinition && (
isset ( $field->config['isPrivate'] ) ||
( ! empty( $field->config['auth'] ) && is_array( $field->config['auth'] ) ) )
) {
/**
* If the schema for the field is configured to "isPrivate" or has "auth" configured,
* make sure the user is authenticated before resolving the field
*/
if ( empty( get_current_user_id() ) ) {
throw new UserError( $auth_error );
}
/**
* If the user is authenticated, and the field has a custom auth callback configured,
* execute the callback before continuing resolution
*/
if ( ! empty( $field->config['auth']['callback'] ) && is_callable( $field->config['auth']['callback'] ) ) {
return call_user_func( $field->config['auth']['callback'], $field, $field_key, $source, $args, $context, $info, $field_resolver );
}
/**
* If the user is authenticated and the field has "allowedCaps" configured,
* ensure the user has at least one of the allowedCaps before resolving
*/
if ( ! empty( $field->config['auth']['allowedCaps'] ) && is_array( $field->config['auth']['allowedCaps'] ) ) {
$caps = ! empty( wp_get_current_user()->allcaps ) ? wp_get_current_user()->allcaps : [];
if ( empty( array_intersect( array_keys( $caps ), array_values( $field->config['auth']['allowedCaps'] ) ) ) ) {
throw new UserError( $auth_error );
}
}
/**
* If the user is authenticated and the field has "allowedRoles" configured,
* ensure the user has at least one of the allowedRoles before resolving
*/
if ( ! empty( $field->config['auth']['allowedRoles'] ) && is_array( $field->config['auth']['allowedRoles'] ) ) {
$roles = ! empty( wp_get_current_user()->roles ) ? wp_get_current_user()->roles : [];
$allowed_roles = array_values( $field->config['auth']['allowedRoles'] );
if ( empty( array_intersect( array_values( $roles ), array_values( $allowed_roles ) ) ) ) {
throw new UserError( $auth_error );
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WPGraphQL;
use GraphQL\Error\UserError;
use GraphQL\Executor\Executor;
use GraphQL\Schema;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\Type\WPObjectType;
/**
* Class WPSchema
*
* Extends the Schema to make some properties accessible via hooks/filters
*
* @package WPGraphQL
*/
class WPSchema extends Schema {
/**
* Holds the $filterable_config which allows WordPress access to modifying the
* $config that gets passed down to the Executable Schema
*
* @var array|null
* @since 0.0.9
*/
public $filterable_config;
/**
* WPSchema constructor.
*
* @param array|null $config
*
* @since 0.0.9
*/
public function __construct( $config ) {
/**
* Set the $filterable_config as the $config that was passed to the WPSchema when instantiated
*
* @since 0.0.9
*/
$this->filterable_config = apply_filters( 'graphql_schema_config', $config );
parent::__construct( $this->filterable_config );
}
}