-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GraphQL API #51
Add GraphQL API #51
Conversation
…e a blank graphQL class
…Block filter for this
@ingeniumed I noticed some errors in a response while testing today. In a post, I have this content: <!-- wp:paragraph -->
<p>Unnested paragraph.</p>
<!-- /wp:paragraph -->
<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:paragraph -->
<p>Column 1</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:paragraph -->
<p>Column 2</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:paragraph -->
<p>Column 3</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns --> And then I query for the blocks/innerBlocks of that post: query NewQuery {
post(id: 419, idType: DATABASE_ID) {
blocksData {
blocks {
attributes {
name
value
}
id
name
innerBlocks {
attributes {
name
value
}
id
name
parentId
}
}
}
}
} The result appears to hold the correct blocks, but also a bunch of Click to expand{
"errors": [
{
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 12,
"column": 11
}
],
"path": [
"post",
"blocksData",
"blocks",
1,
"innerBlocks",
0,
"attributes"
]
},
{
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 12,
"column": 11
}
],
"path": [
"post",
"blocksData",
"blocks",
1,
"innerBlocks",
2,
"attributes"
]
},
{
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 12,
"column": 11
}
],
"path": [
"post",
"blocksData",
"blocks",
1,
"innerBlocks",
4,
"attributes"
]
}
],
"data": {
"post": {
"blocksData": {
"blocks": [
{
"attributes": [
{
"name": "content",
"value": "Unnested paragraph."
},
{
"name": "dropCap",
"value": ""
}
],
"id": "1",
"name": "core/paragraph",
"innerBlocks": null
},
{
"attributes": [
{
"name": "isStackedOnMobile",
"value": "1"
}
],
"id": "2",
"name": "core/columns",
"innerBlocks": [
{
"attributes": null,
"id": "3",
"name": "core/column",
"parentId": "2"
},
{
"attributes": [
{
"name": "content",
"value": "Column 1"
},
{
"name": "dropCap",
"value": ""
}
],
"id": "4",
"name": "core/paragraph",
"parentId": "3"
},
{
"attributes": null,
"id": "5",
"name": "core/column",
"parentId": "2"
},
{
"attributes": [
{
"name": "content",
"value": "Column 2"
},
{
"name": "dropCap",
"value": ""
}
],
"id": "6",
"name": "core/paragraph",
"parentId": "5"
},
{
"attributes": null,
"id": "7",
"name": "core/column",
"parentId": "2"
},
{
"attributes": [
{
"name": "content",
"value": "Column 3"
},
{
"name": "dropCap",
"value": ""
}
],
"id": "8",
"name": "core/paragraph",
"parentId": "7"
}
]
}
]
}
}
},
"extensions": {
"debug": [
{
"type": "DEBUG_LOGS_INACTIVE",
"message": "GraphQL Debug logging is not active. To see debug logs, GRAPHQL_DEBUG must be enabled."
}
]
}
} |
README.md
Outdated
```php | ||
/** | ||
* Filter to enable/disable the graphQL API. By default, it is enabled. | ||
* | ||
* @param bool $is_graphql_to_be_enabled Whether the graphQL API should be enabled or not. | ||
*/ | ||
apply_filters( 'vip_block_data_api__is_graphql_enabled', true ); | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little odd to include the apply_filters
definition here, which isn't relevant to usage. I would remove it
```php | |
/** | |
* Filter to enable/disable the graphQL API. By default, it is enabled. | |
* | |
* @param bool $is_graphql_to_be_enabled Whether the graphQL API should be enabled or not. | |
*/ | |
apply_filters( 'vip_block_data_api__is_graphql_enabled', true ); | |
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README.md
Outdated
add_filter( 'vip_block_data_api__is_graphql_enabled', function( ) { | ||
return false; | ||
}, 10, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_filter( 'vip_block_data_api__is_graphql_enabled', function( ) { | |
return false; | |
}, 10, 1); | |
add_filter( 'vip_block_data_api__is_graphql_enabled', '__return_false' ); |
src/graphql/graphql-api.php
Outdated
return new \Exception( $parser_results->get_error_message() ); | ||
} | ||
|
||
$parser_results['blocks'] = array_map(function ( $block ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there no phpcs on this repo?
$parser_results['blocks'] = array_map(function ( $block ) { | |
$parser_results['blocks'] = array_map( function ( $block ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, weird! We do have phpcs
configured, and it seems to spot other problems in this file just fine, but it's not enforcing parentheses spacing. Our phpcs configuration is a close relation to vip-go-mu-plugins's config which appears to be roughly the same. Looking into what sniff might be missing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Figured it out. I noticed if I add a missing space a couple of lines up, PHPCS will report it:
$block['id'] = Relay::toGlobalId('ID', wp_unique_id() );
// ^ missing space
FILE: src/graphql/graphql-api.php
--------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
--------------------------------------------------------------------------
73 | ERROR | [x] Expected 1 spaces after opening parenthesis; 0 found
| | (PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket)
However, that same PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket
sniff doesn't kick in for array_map()
. After looking at our configuration, I noticed we have some sniffs disabled related to anonymous functions and function calls. If I remove those, we'll now get multiple errors for the multi-line function call:
72 | ERROR | [x] Opening parenthesis of a multi-line function call must be the last content on the line
| | (PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket)
74 | ERROR | [x] Only one argument is allowed per line in a multi-line function call (PEAR.Functions.FunctionCallSignature.MultipleArguments)
74 | ERROR | [x] Closing parenthesis of a multi-line function call must be on a line by itself (PEAR.Functions.FunctionCallSignature.CloseBracketLine)
So, in short, if we want to disable those sniffs to use some standard PHP array_map( function( ... ) {
practices, we also lose the sniff that tells us about weird spacing after array_map()
.
Fixed manually.
src/graphql/graphql-api.php
Outdated
/** | ||
* Transform the block's format to the format expected by the graphQL API. | ||
* | ||
* @param array $block An associative array of parsed block data with keys 'name' and 'attribute'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for type declarations instead of hand-written descriptions
* @param array $block An associative array of parsed block data with keys 'name' and 'attribute'. | |
* @param array $block An associative array of parsed block data with keys 'name' and 'attributes'. |
src/graphql/graphql-api.php
Outdated
/** | ||
* Convert the attributes to be in the name-value format that the schema expects. | ||
* | ||
* @param array $block An associative array of parsed block data with keys 'name' and 'attribute'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param array $block An associative array of parsed block data with keys 'name' and 'attribute'. | |
* @param array $block An associative array of parsed block data with keys 'name' and 'attributes'. |
// check if type of attributes is stdClass and unset it as that's not supported by graphQL. | ||
if ( isset( $block['attributes'] ) && is_object( $block['attributes'] ) ) { | ||
unset( $block['attributes'] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how / why would this happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ingeniumed Can you explain a bit more about the stdClass
comment here? I'm not sure where that comes from either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is because in the contentParser we set the actual attributes to be (object)[]
which allows it to be given back as empty json in the rest api rather than as an empty array. Due to that, the internal data type comes back as stdClass rather than an array. This workaround is due to that.
I experimented with switching the (object)[]
to a new ArrayObject but in that case it adds an empty array with key storage
under the attributes which requires an even worse workaround. That's why it's been done this way.
I tried to search for a better solution but couldn't quite find one and I didn't want to break the rest response or the way the public function works because there are customers already using it.
This is the line I'm referencing btw in the contentParser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully that explains it properly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ingeniumed That sounds familiar now! As a side-effect of returning data in JSON via the REST API, if we want to return attributes: {}
in a response, we need to cast []
into an object to avoid returning an array instead. This is just dealing with that unusual data in the GraphQL layer.
src/graphql/graphql-api.php
Outdated
function ( $name, $value ) { | ||
return [ | ||
'name' => $name, | ||
'value' => $value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess WPGraphQL is doing the casting to string? You might want to do it here in code just to be explicit and test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess WPGraphQL is doing the casting to string?
I think you're right! I tested with a block delimiter value like {"test": 123}
, which is returned as an integer from the parser and REST API. However, once we return it in a BlockAttribute
, it is converted to a string. Added explicit conversion in 8cdc1e2 anyway.
src/graphql/graphql-api.php
Outdated
// Set the parentId to be the ID of the parent block whose inner blocks are being flattened. | ||
$inner_block['parentId'] = $parent_id; | ||
|
||
// This block doesnt have any inner blocks, so just add it to the flattened blocks. Ensure the parentId is set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indenting is off here
src/graphql/graphql-api.php
Outdated
], | ||
'parentId' => [ | ||
'type' => 'ID', | ||
'description' => 'ID of the parent for this inner block, if it is an inner block. Otherwise, it will not be set. If it set, this will match the ID of the block', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last part confusing / redundant
'description' => 'ID of the parent for this inner block, if it is an inner block. Otherwise, it will not be set. If it set, this will match the ID of the block', | |
'description' => 'ID of the parent for this inner block, if it is an inner block. Otherwise, it will be null.', |
src/graphql/graphql-api.php
Outdated
'args' => [ | ||
'flatten' => [ | ||
'type' => 'Boolean', | ||
'description' => 'Collate the inner blocks under each root block into a single list with a parent-child relationship. This is set to true by default, and setting it to false will reverse that to preserve the original block hierarchy, at the expense of knowing the exact depth when querying the inner blocks. Default: true', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'description' => 'Collate the inner blocks under each root block into a single list with a parent-child relationship. This is set to true by default, and setting it to false will reverse that to preserve the original block hierarchy, at the expense of knowing the exact depth when querying the inner blocks. Default: true', | |
'description' => 'Collate the inner blocks under each root block into a single list with a parent-child relationship. This is set to true by default, and setting it to false will preserve the original block hierarchy, but will require nested inner block queries to the desired depth. Default: true', |
src/graphql/graphql-api.php
Outdated
], | ||
], | ||
'resolve' => function ( $blocks, $args ) { | ||
if ( ! isset( $args['flatten'] ) || true === $args['unflatten'] ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bug, probably needs tests
if ( ! isset( $args['flatten'] ) || true === $args['unflatten'] ) { | |
if ( ! isset( $args['flatten'] ) || false === $args['flatten'] ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! I fixed this, tested it manually, and put it in d578fe4. I spent some time trying to add a test, but after my attempt I think @ingeniumed has already done a decent job trying to wrap the core logic from the resolver with a test:
vip-block-data-api/src/graphql/graphql-api.php
Lines 220 to 233 in d578fe4
'resolve' => function ( $blocks, $args ) { | |
if ( ! isset( $args['flatten'] ) || true === $args['flatten'] ) { | |
$blocks['blocks'] = array_map( function ( $block ) { | |
// Flatten the inner blocks, if any. | |
if ( isset( $block['innerBlocks'] ) ) { | |
$block['innerBlocks'] = self::flatten_inner_blocks( $block['innerBlocks'], $block['id'] ); | |
} | |
return $block; | |
}, $blocks['blocks'] ); | |
} | |
return $blocks['blocks']; | |
}, |
Our unit testing does not include the WPGraphQL plugin, so the logic that's in this resolver isn't directly tested in test_flatten_inner_blocks()
. The bug here is in the thin shell of WPGraphQL code around the flatten_inner_blocks()
function, which we test directly. Ideally we could have some end-to-end tests that include the request through WPGraphQL (like we do for our REST tests), but I think the testing here is a good enough start. As long as we catch the interface bugs, like this.
src/graphql/graphql-api.php
Outdated
$blocks['blocks'] = array_map(function ( $block ) { | ||
// Flatten the inner blocks, if any. | ||
if ( isset( $block['innerBlocks'] ) ) { | ||
$block['innerBlocks'] = self::flatten_inner_blocks( $block['innerBlocks'], $block['id'] ); | ||
} | ||
|
||
return $block; | ||
}, $blocks['blocks'] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just do this, no?
$blocks['blocks'] = array_map(function ( $block ) { | |
// Flatten the inner blocks, if any. | |
if ( isset( $block['innerBlocks'] ) ) { | |
$block['innerBlocks'] = self::flatten_inner_blocks( $block['innerBlocks'], $block['id'] ); | |
} | |
return $block; | |
}, $blocks['blocks'] ); | |
// Flatten the inner blocks, if any. | |
$blocks['blocks'] = self::flatten_inner_blocks( $blocks['blocks'], null ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replacement doesn't work as-is, because flatten_inner_blocks()
needs the $parent_id
parameter, which is passed to innerBlocks
to determine their parent IDs:
vip-block-data-api/src/graphql/graphql-api.php
Lines 116 to 119 in d578fe4
public static function flatten_inner_blocks( $inner_blocks, $parent_id, $flattened_blocks = [] ) { | |
foreach ( $inner_blocks as $inner_block ) { | |
// Set the parentId to be the ID of the parent block whose inner blocks are being flattened. | |
$inner_block['parentId'] = $parent_id; |
I think this is due to the design that top level blocks are treated differently as "root" blocks, but their children are flattened beneath them all together. I'm sure it's possible to rewrite flatten_inner_blocks()
so this is done internally, but I think this works fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait a minute, I'm rereading this and I'm not sure why it's not working anymore. Let me take a second look.
@chriszarate Thanks a ton for your review! I believe I've addressed all of your suggestions above. Please take another look when you have a chance. Thank you! |
src/graphql/graphql-api.php
Outdated
*/ | ||
public static function transform_block_format( $block ) { | ||
// Generate a unique ID for the block. | ||
$block['id'] = Relay::toGlobalId( 'ID', wp_unique_id() ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first argument to this function should the type name of the object in question, so BlockData
in this case. This is what ensures uniqueness across the graph. Also, since wp_unique_id
just increments integers, you should probably also incorporate the post ID. Otherwise, you risk collisions in client caches which will be really hard to diagnose. (I've seen this happen and it's bewildering when a paragraph from post 2 shows up in post 1).
$block['id'] = Relay::toGlobalId( 'ID', wp_unique_id() ); | |
$block['id'] = Relay::toGlobalId( 'BlockData', sprintf( "%d:%d", $post->ID, wp_unique_id() ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for clarifying this @chriszarate! Changed in 6e2c367.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All my feedback has been addressed, but I'll leave it to Bistro to approve :)
I'll merge this into our release branch, and we can cut a release later this week via that. Thanks for the amazing feedback @chriszarate, and @smithjw1. |
Description
This PR is meant add a GraphQL API to the Block Data API. It assumes that you have WP-GraphQL installed, and uses a filter to tweak the output of the
ContentParser
to generate the GraphQL response.In addition, it uses the
wp_unique_id
function to generate ids for the blocks so that the blocks can be correlated together. Theinner_blocks
are therefore able to be flattened entirely, thereby bypassing the nested depth problem in graphQL.This is in the draft status to get initial feedback, as well as to add some tests and guard rails around the GraphQL API.
Resolves #49
Steps to Test