Bot releases are visible (Hide)
Published by jasonbahl over 4 years ago
n/a
@access
tag. Thanks @izzygld!Published by jasonbahl over 4 years ago
This release focuses on adjusting how Nodes are resolved to prevent errors in cases where the nodes are determined to be considered private and non-viewable by the requesting user. (#1138)
More details below:
Schema Breaking changes are changes to the shape of the Schema that would require clients interacting with WPGraphQL to update to remain compatible.
n/a: The shape of the Schema remains unchanged in this release. Clients shouldn't need to adjust their queries/mutations to remain compatible with this release.
ancestors
field removed from Post Type Objects. Possibly will add back in the future as a formal connection.
Internal Breaking Changes are changes to internals that might require plugins that extend WPGraphQL to change in order to remain compatible.
ContentTypeConnectionResolver
, TaxonomyConnectionResolver
and UserRoleConnectionResolver
to not extend the AbstractConnectionResolver class, but instead make use of the Relay::connectionFromArray()
methodresolveNode
config arg from most connections registered by the core plugin as the new method for resolving connections doesn't wait until the last second to resolve nodes
WPGraphQL makes use of a concept called (Deferred Resolution)[https://github.com/wp-graphql/wp-graphql/pull/722#issue-261315185], to ensure that database queries are executed as efficiently as possible.
WPGraphQL was deferring the resolution of nodes too late, causing errors when Nodes were determined to be private after being loaded.
Take the following query for example :
{
posts {
nodes {
id
databaseId
title
}
}
}
This query might return a payload like so:
{
"data": {
"posts": {
"nodes": [
{
"id": "cG9zdDoyNDI=",
"databaseId": 242,
"title": "Test Post"
},
{
"id": "cG9zdDox",
"databaseId": 1,
"title": "Hello world!"
}
]
}
},
}
Looks great! Just what we'd expect (assuming the site had only 2 posts).
Well, let's say we had a membership plugin installed (or something similar) that used meta to determine whether a Post should be considered private or not. And let's say that Post 242
was set to private and should not be returned to a public user.
Because of how the Deferred resolution was working (before this release), the Post would have been resolved too late to be stripped out of the results, and would return a null
within the list of Post nodes, and would include an error like so:
{
"errors": [
{
"debugMessage": "Cannot return null for non-nullable field Post.id",
...
}
],
"data": {
"posts": {
"nodes": [
null,
{
"id": "cG9zdDox",
"databaseId": 1,
"title": "Hello world!"
}
]
}
},
}
This behavior is problematic.
First, it throws errors, when there really isn't an error. Nothing has actually gone wrong. The user is asking for Posts, and GraphQL should be able to return posts without error.
If a Post is private, it shouldn't be exposed at all. It should be as if it doesn't even exist in the system. Be returning a null value, we are exposing that something is there behind the scenes.
The correct behavior should be to return a list of Posts, and no errors returned. If a Post is private, it should simply not be included in the list at all.
This release fixes this issue by changing how the Deferred resolution of nodes happens.
Given the query above, resolution used to work like so:
Because of the late resolution of the Node, this was causing the Cannot return null for non-nullable field Post.id
error. There's no way to strip a private node out of the list of returned nodes if we're resolving nodes at the last possible tree in the Graph.
This pull request changes the behavior to resolve the nodes earlier.
Given the query above, resolution now works like so:
Cannot return null for non-nullable field Post.id
will be returned.To accomplish this, some changes to the ConnectionResolver classes were made.
Now, Nodes are resolved a step earlier, and the resolved nodes are now passed from the Connection down to Edges/Nodes.
Edges/Nodes now have the full nodes as context in their resolvers, instead of just an ID.
This can be HUGE when needing to add edge data to connections, where before an entity ID was the only context provided, and that can be too little information to be useful.
You can read more about a concrete case where the functionality was problematic, and how this release fixes it here: https://github.com/wp-graphql/wp-graphql/issues/1138#issuecomment-580269285
Below is a list of changes the AbstractConnectionResolver Class. If your Plugin extends this class, the below information should help with upgrading.
abstract public function get_loader_name()
abstract public function get_ids()
get_items()
methodabstract public function get_node_by_id()
get_amount_requested()
, get_offset()
, get_query_amount()
, has_next_page()
, has_previous_page()
, get_start_cursor()
, get_end_cursor()
, get_nodes()
, get_cursor_for_node()
, get_edges()
, get_page_info()
Published by jasonbahl over 4 years ago
Published by jasonbahl over 4 years ago
This feature focused primarily on #1045, exposing PostTypes and Taxonomies at the root of the graph.
PageToPageConnection
for revisions is now named PageToRevisionConnection
in the SchemapostTypeInfo
field from connections to PostTypeObjects. Each PostTypeObject node now has a contentType
field to access the ContentType node it's associated withtaxonomy
field is now different as well.contentType
instead of postType
category.taxonomy
and tag.taxonomy
field{
tags {
taxonomyInfo { #This returns a Taxonomy node, similar to calling get_taxonomy( 'post_tag' )
id
name
}
nodes {
id
name
taxonomy { #This returns a Taxonomy type, similar to calling get_taxonomy( 'post_tag' )
name
}
}
}
}
{
tags {
nodes {
id
name
taxonomy { # This now returns a oneToOne connection to a Taxonomy node
# Note this edge space between taxonomy and node.
# Contextual info about the connection between the Tag node and the Taxonomy
# it belongs to (should there ever be any) can be added here.
node { #This returns a Taxonomy node, similar to calling get_taxonomy( 'post_tag' )
id
name
}
}
}
}
}
{
posts {
postTypeInfo { # This returned a PostType type, similar to calling get_post_type_object( 'post' );
name
}
nodes {
id
title
# Note: there is no field to query for postType or contentType from a Post object node
}
}
}
{
posts {
nodes {
id
title
contentType {
# Note this edge space between contentType and node.
# Contextual info about the connection between the Post node and the ContentType node
# it belongs to (should there ever be any) can be added here.
node { # This returns a ContentType node, similar to calling get_post_type_object( 'post' );
name
}
}
}
}
}
This RootQuery->ContentType connection allows for Content Types (post types) to be queried from the root. All properties of the ContentType can be asked for, such as labels, etc.
{
contentTypes {
nodes {
id
name
graphqlSingleName
graphqlPluralName
}
}
}
{
taxonomy(id: "dGF4b25vbXk6cG9zdF90YWc=") {
id
name
graphqlSingleName
graphqlPluralName
}
}
{
taxonomy(id: "post_tag" idType: NAME ) {
id
name
graphqlSingleName
graphqlPluralName
}
}
{
contentType(id: "Y29udGVudFR5cGU6cG9zdA==") {
id
name
graphqlSingleName
graphqlPluralName
}
}
{
contentType( id: "post", idType: NAME ) {
id
name
graphqlSingleName
graphqlPluralName
}
}
Published by jasonbahl over 4 years ago
Published by jasonbahl over 4 years ago
This fixes #1144, a bug in the Model Layer that was causing cached values of a Model to attempt to execute if the string name was a function. For example, if a field's value was the string count
the Model would attempt to execute the count()
method instead of returning the string count
.
Published by jasonbahl over 4 years ago
This is a minor release that should have no breaking changes.
menuOrder
field wasn't being registered to post types that support page attributes. This was fixed by #1131. Thanks for reporting @moonmeister!Published by jasonbahl over 4 years ago
This release focused primarily on adding Interfaces for Post and Term objects.
Interfaces in GraphQL allow for common fields to be registered and shared across Types. Similar to Unions, they allow for multiple types to be queried from the same field.
By having Post Objects (posts, pages, etc) and Terms (Tags, Categories, etc) implement common interfaces, various Types can be queried for from the same entry point. So, instead of having to query only Posts or only Pages, interfaces make it possible to query for Post Objects of any type and have predictable results.
See below for examples of such queries.
uri
fields when querying nodesauthor
input on its mutation anymore. Or a non-hierarchical Post Type doesn't have a parent
input field by default in its mutation anymore. Might be a breaking change depending on your use of mutations and fields that a Post Type doesn't supportauthor
, you could have queried for the author
on that node, and got null. Now the author field will not exist on that Type and your query would not work.__get
methods from WPObjectType, WPUnionType, etc in favor of making the type_registry a public property (@kidunot89 this will affect WPGraphQL for WooCommerce)parent
field to revisionOf
field for Types that support revisions. If the node is a revision, the revisionOf
field will return the node that it is a revision of. This is applied only to post types that support revisions via the NodeWithRevisions
Interfaceabstract public function is_valid_offset( $offset );
to AbstractConnectionResolver. If you extend the AbstractConnectionResolver in your code, you will need to have your extending class implement this method. It is used for pagination to respect the Relay spec.post format
taxonomy - thanks @aberkow!RootQuery.terms
connection to query terms of any Taxonomy type from the root of the graphpageTemplate
field to the Post model layerpostBy( slug: "slug" ) { ...postFields }
, but are encouraged to query by the single entry point with the newly added idType
input instead, like so: post( id: "slug" idType: SLUG ) { ...postFields }
idType
inputs. Now single resources can be queried for using various types of unique identifiers such as database id, slug, name, uri. The type of ID available depends on the type of node.graphql()
function in PHP to break the admin barWe're deprecating the $postType.'By'
entry points, and have added a new idType
field and enum for single entry points.
This means queries such as the following will still work, but will not show in the Schema documentation, and may be formally removed from the codebase at a later date:
{
postBy( slug: "some-slug" ) {
id
title
}
}
{
pageBy( uri: "some-uri" ) {
id
title
}
}
These are deprecated in favor of existing single entry points for each post type, and the newly added idType
field on those entry points.
With the added idType
field on the singular post object entry points, below is a series of example queries now made possible:
{
post(id: 1739, idType: DATABASE_ID) {
id
title
uri
slug
postId
}
}
{
post(id: "/test-5/", idType: URI) {
id
title
uri
slug
postId
}
}
{
post(id: "cG9zdDoxNzM5", idType: ID) {
id
title
uri
slug
postId
}
}
or without specifying idType
as the default is the hashed ID
{
post(id: "cG9zdDoxNzM5") {
id
title
uri
slug
}
}
{
post(id: "test-5", idType: SLUG) {
id
title
uri
slug
}
}
This release also introduces the ContentNode
Interface that is implemented by all Post Object types (posts, pages, custom post types).
This Interface allows for new types of queries to be executed that span many post types.
Here's an example of a new query:
{
contentNodes {
nodes {
__typename
id
title
link
uri
isRevision
... on Page {
isFrontPage
}
}
}
}
In the results above, we can see that a mix of Posts and Pages have been returned.
We can also pass arguments to do things like search across multiple post types:
{
contentNodes(where: {search: "test"}) {
nodes {
__typename
id
title
link
uri
isRevision
... on Page {
isFrontPage
}
}
}
}
We can now also fetch single Content (post) nodes like so:
{
contentNode(id: "cG9zdDoxNzM5") {
__typename
id
title
link
uri
isRevision
... on Page {
isFrontPage
}
}
}
But, we can fetch single nodes of any Post type by other unique identifiers as well, such as the URI:
{
contentNode(id: "/2019/12/05/test-5/", idType: URI) {
__typename
id
title
link
uri
isRevision
... on Page {
isFrontPage
}
}
}
or
{
contentNode(id: "/test/", idType: URI) {
__typename
id
title
link
uri
isRevision
... on Page {
isFrontPage
}
}
}
This is a SUPER powerful feature as it allows for fragments to be more easily shared and re-used across queries and components.
Prior to this release, there wasn't really great ways of fetching singular term objects. You could do a query for a connection of terms and limit it to the first: 1
, and in some cases that worked fine, but it wasn't always ideal.
This release adds better support for fetching individual Term Nodes.
Instead of only fetching by the hashed global ID, we can now fetch by other unique identifiers using the idType
field.
{
tag(id: "cG9zdF90YWc6Mw==") {
id
name
slug
tagId
}
}
{
tag(id: 3, idType: DATABASE_ID) {
id
name
slug
tagId
}
}
{
tag(id: "Another Test", idType: NAME) {
id
name
slug
tagId
}
}
{
tag(id: "another-test", idType: SLUG) {
id
name
slug
tagId
}
}
{
tag(id: "tag/another-test/", idType: URI) {
id
name
slug
tagId
uri
}
}
This release also introduces the TermNode interface, which allows for querying terms across Taxonomy types.
For example:
{
terms {
nodes {
id
__typename
name
uri
}
}
}
This enables us to do things, like search terms across multiple taxonomies and get both Categories and Tags (and custom taxonomies) back in the same query:
{
terms(where: {search: "test"}) {
nodes {
id
__typename
name
uri
... on Tag {
tagId
}
... on Category {
categoryId
}
}
}
}
We can also query single term nodes by various identifiers, such as Database ID
, ID
, Name
, Slug
or URI
:
{
termNode(id: "cG9zdF90YWc6Mw==") {
__typename
id
name
link
slug
uri
}
}
{
termNode(id: 3, idType: DATABASE_ID) {
__typename
id
name
link
slug
uri
databaseId
}
}
{
termNode(id: "Another Test", idType: NAME, taxonomy: TAG) {
__typename
id
name
link
slug
uri
databaseId
}
}
{
termNode(id: "another-test", idType: SLUG, taxonomy: TAG) {
__typename
id
name
link
slug
uri
databaseId
}
}
{
termNode(id: "tag/another-test/", idType: URI) {
__typename
id
name
link
slug
uri
databaseId
}
}
Another thing this release provides is a way to get any node by URI, across types. This comes in handy because in many cases when a URI is provided, the type of content it is isn't always known ahead of time. By having an entry point for all Types that implement the 1UniformResourceIdentifiable` Interface, we can create a main entry point for entire applications.
Below is an example. This is showing many queries at once, but this could easily be one query with fragments for each Type, controlled by their appropriate component for rendering said type of content:
{
page: nodeByUri(uri: "about/") {
...URI
}
post: nodeByUri(uri: "2019/12/05/test-5/") {
...URI
}
tag: nodeByUri(uri: "tag/8bit/") {
...URI
}
category: nodeByUri(uri: "category/alignment/") {
...URI
}
user: nodeByUri(uri: "author/jasonbahl/") {
...URI
}
}
fragment URI on UniformResourceIdentifiable {
__typename
... on Page {
pageId
}
... on Post {
postId
}
... on Category {
categoryId
}
... on Tag {
tagId
}
... on User {
userId
}
}
Published by jasonbahl almost 5 years ago
Apology: This was intended to be a non-breaking minor release, but I moved too quickly and it was brought to my attention that there actually were some regressions not on my radar. I'll try to be more careful and communicate breaking changes better in the future, namely by not releasing them as minor point releases
/graphql
endpoint worked for both site_url()
and home_url()
, but in a Headless WordPress world we live in, many people are changing their WordPress Address and Site Address to be different things.For example, I might have my WordPress dashboard at data.site.com/wp-admin
and my headless site at site.com
, so my settings in general settings might look like:
Previously, WPGraphQL would resolve under the the site address /graphql, but if the site is actually not WordPress at all, say a Gatsby site, this doesn't make sense.
This release changed so that WPGraphQL endpoint is active at site_url(). /graphql
, so in the example above that would be data.site.com/graphql
We didn't fully think through the weight of this change, largely because many of the environments I test on aren't set up with these URLs being different, so it wasn't top of mind.
I apologize for releasing this without fully thinking through the implications for users that are already in environments where their WordPress URL and Site Address are configured to be different.
Thanks @aberkow and @mlbrgl for bringing this to my attention!
There was one breaking change introduced in this release in the way PostObjectConnectionResolver
behaves. Previously this resolver class allowed only a single post_type
to be passed to the class for resolutions, so $this->post_type
was always a string.
Now, it is usually cast as an array, with the exception being attachment
and revision
as these are validated differently because of their inherit
post_type nature.
If you were using the graphql_map_input_fields_to_wp_query
filter, the 7th argument passed through will likely have different behavior for you now.
postBy()
query doesn't return a node (#1059)extensions
are not passed in the response (#1071)is_graphql_http_request
to use site_url()
instead of home_url
(thanks @kidunot89!)graphql()
function to be used on page templates without breaking the admin header bar (#964)is_graphql_http_request()
and add tests (Thanks @esamattis!)Published by jasonbahl almost 5 years ago
This release is a breaking change release.
The focus of this release is on how Revisions are handled.
Prior to this release, Revisions are addd to the WPGraphQL Schema as their own Revision
Type. This largely mimics WordPress implementation, in that revisions are a different Post Type. However, this doesn't lend itself well to actual use.
Revisions of a Post, declaratively speaking, are still Posts. If I paint my house, it's still a house, just with different properties. If I change the title of a Post, it's still a Post, it should be considered as much in the Graph.
The intent of revisions is to show a thing (Post, Page, etc) as it exists over time.
The Revision
Type declares that the thing is in fact, a different thing, and the utility of Revisions is compromised.
By changing Revisions in the WPGraphQL Schema to be of the same Type of the object being revised, clients can actually represent their objects as they've been revised over time.
The Revision
Type no longer exists. If you were querying for Revisions your queries will need to be updated as the Type being returned will no longer be a Revision
.
When querying revisions from the Root, the Type returned to the connection is a ContentRevisionUnion
, which is comprised of any Types that support revisions (for example, Post and Page, or any Post Type that has registered support for revisions
)
You can now query revisions from the Root like so:
{
revisions {
nodes {
__typename
... on Post {
id
title
postId
}
... on Page {
id
title
pageId
}
}
}
}
and you would get results similar to the following (dependent on your data set):
{
"data": {
"revisions": {
"nodes": [
{
"__typename": "Post",
"id": "cmV2aXNpb246MTcxNQ==",
"title": "Another Test"
"postId":1715
},
{
"__typename": "Page",
"id": "cmV2aXNpb246MTY0Mg==",
"title": "Test Page Revision",
"pageId": 1642,
}
{
"__typename": "Post",
"id": "cmV2aXNpb246MTcxNA==",
"title": "Test revision",
"postId": 1543
}
]
}
},
}
If you were extending the Revision
Type, you will need to consider alternatives for extending the Schema for your use case.
Querying revisions of a Post will return a connection to Posts. The WordPress database will still fetch posts of the "revision" post type, but the GraphQL Schema will represent the revisions of a Post as a Post.
Querying revisions of a Page will return a connection to Pages. The WordPress database will still fetch posts of the "revision" post type, but the GraphQL Schema will represent the revisions of Pages as a Page.
Same goes for Custom Post Types.
Since revisions of a Post are always a Post, the connection is not a Union as it is when querying Revisions from the RootQuery, so we can query like so (of course this will vary based on your data set):
{
post(id: "cG9zdDoxNzEz") {
__typename
id
title
date
revisions {
nodes {
__typename
id
title
date
}
}
}
}
and you might get results like so:
{
"data": {
"post": {
"__typename": "Post",
"id": "cG9zdDoxNzEz",
"title": "Test",
"date": "2019-11-30T17:32:39",
"revisions": {
"nodes": [
{
"__typename": "Post",
"id": "cmV2aXNpb246MTcxNQ==",
"title": "Test",
"date": "2019-11-30T17:33:10"
},
{
"__typename": "Post",
"id": "cmV2aXNpb246MTcxNA==",
"title": "Test",
"date": "2019-11-30T17:32:20"
}
]
}
}
}
Because Revisions of a Post are now represented as a Post, we can make use of some great GraphQL features, such as Query Fragments. We could refactor the above query like below, using the same fragment for the primary Post and the revisions of it, allowing for the same components to be used to render a Post and a revision of a Post. Powerful stuff!:
{
post(id: "cG9zdDoxNzEz") {
__typename
...PostFields
revisions {
nodes {
__typename
...PostFields
}
}
}
}
fragment PostFields on Post {
id
title
date
}
closes #1017
Published by jasonbahl almost 5 years ago
is_graphql_request
and is_graphql_http_request
Thanks @esamattis @kidunot89!TypeRegistry->getTypes()
not available to graphql_register_types
actiondisplayName
to UserRole typePublished by jasonbahl almost 5 years ago
Published by jasonbahl almost 5 years ago
Router::is_graphql_request()
method - Thanks @esamattis!Page.isFrontPage
field to the Schema - Thanks @zgordon!Published by jasonbahl almost 5 years ago
This release is centered around Issue #846. Plugins that extend the WPGraphQL Schema are very difficult to test because once the Schema is created, it can't be cleared or modified. This is because of the heavy use of Statics, which make testing very difficult.
This Release changes many of the Static classes to classic objects that are instantiated and passed down using Dependency Injection.
This makes it much easier to create instances of the Schema on the fly, which is essential for extension plugins to do when testing their extensions. It also helps the core plugin properly test API methods like register_graphql_field
, register_graphql_mutation
, etc.
These changes make it easier for extensions to write and execute tests.
Because of the statics, the WPGraphQL Schema could be generated just once for the entire test suite, which means to test things like registering custom fields in ACF will add fields to the WPGraphQL Schema would mean that all fields would need to be added in the Bootstrap of the test suite, and not within the individual test that's testing the fields.
This is problematic to scaling tests. Ideally, we want to follow the AAA method of testing:
What this means is that if we wanted to test something like adding an ACF Text field (doesn't have to be ACF, just an example) we would want a test that:
We cannot do that today, but with this release, we can! Now, each test can "own" everything needed to test its behavior.
The primary breaking change here is making the TypeRegistry class no longer a singleton, so the methods are not statically accessible anymore. Instead, the TypeRegistry is an object that is passed down in the context of the graphql_register_types
action.
If your code was previously making use of any static methods from the TypeRegistry
class, such as TypeRegistry::get_type( 'TypeName' )
you will need to refactor a bit.
Upgrading
Below is an example from the WPGraphQL for Gravity Forms plugin that needs to change:
@kellenmace pointed out:
I am using TypeRegistry::get_type(), here: https://github.com/harness-software/wp-graphql-gravity-forms/blob/master/src/Types/Union/ObjectFieldUnion.php#L45
src/Types/Union/ObjectFieldUnion.php:45
To upgrade to v0.4.0, this code should be changed from:
public function register_type() {
$field_mappings = $this->get_field_type_mappings();
register_graphql_union_type( self::TYPE, [
'typeNames' => array_values( $field_mappings ),
'resolveType' => function( GF_Field $field ) use ( $field_mappings ) {
if ( isset( $field_mappings[ $field['type'] ] ) ) {
return TypeRegistry::get_type( $field_mappings[ $field['type'] ] );
}
return null;
},
] );
}
to:
public function register_type( $type_registry ) {
$field_mappings = $this->get_field_type_mappings();
register_graphql_union_type( self::TYPE, [
'typeNames' => array_values( $field_mappings ),
'resolveType' => function( GF_Field $field ) use ( $field_mappings, $type_registry ) {
if ( isset( $field_mappings[ $field['type'] ] ) ) {
return $type_registry->get_type( $field_mappings[ $field['type'] ] );
}
return null;
},
] );
}
Here we can see that the callback is passed the $type_registry
as it's a callback of the graphql_register_types
action. The $type_registry
is the instance of the TypeRegistry, and now the get_type
method can be used. We changed TypeRegistry::get_type()
to $type_registry->get_type()
.
NOTE:, we are using a closure for the resolveType
so we have to pass $typeRegistry
through with the use
statement.
Published by jasonbahl almost 5 years ago
none
none
Published by jasonbahl about 5 years ago
Shouldn't be any. Please report if you come across any issues.
Published by jasonbahl about 5 years ago
Published by jasonbahl about 5 years ago
If you were making use of filtering users by role, the behavior has changed due to #890 to respect access control properly.
contentType
field in Comments Schema. Thanks @let00!Published by jasonbahl over 5 years ago
Implemented enhancements:
Fixed bugs:
Closed issues:
Merged pull requests:
Published by jasonbahl over 5 years ago
If you were using WPGraphQL::$allowed_post_types
or WP_GraphQL::$allowed_taxonomies
in your custom code, we recommend updating to use WPGraphQL::get_allowed_post_types()
and WPGraphQL::get_allowed_taxonomies()
instead, as some changes in this PR could potentially cause you issues.
This release fixes several regressions that snuck by in the v0.3.0 release cycle.
$where
args for querying menus was not properly being respected post v0.3.0. Thanks for reporting and your patience as we resolved, @josephCarrington!WPGraphQL::$allowed_post_types
were causing funkiness.hideEmpty
description on Term connection args. Thanks @epeli!createComment
payload so the mutation can respond with something indicating the createComment
succeeded, even if the user doesn't have access to get an unapproved comment returned as a result of the mutation. Thanks for working through this with me @paulisloud!