Skip to content
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

Improve query registration and schema validation #243

Merged
merged 46 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e2ee0f4
SCHEMA: Add new Types class to provide formal schema types
chriszarate Dec 11, 2024
cafcda9
SCHEMA: Update Validator class to use new formal types
chriszarate Dec 11, 2024
6932818
SCHEMA: Update Sanitizer class to use new formal types
chriszarate Dec 11, 2024
45c31bc
SCHEMA: Provide schema store for plugin code
chriszarate Dec 11, 2024
3b4ca53
SCHEMA: Add FieldFormatter class for ad hoc formatting
chriszarate Dec 16, 2024
fe70032
SCHEMA: Update HttpDataSource to use new schema validation
chriszarate Dec 12, 2024
644166d
SCHEMA: Update HttpQuery to use new schema validation
chriszarate Dec 12, 2024
894e1ca
SCHEMA: Update QueryRunner to use updated HttpQuery class
chriszarate Dec 19, 2024
1530334
SCHEMA: Update primitive types used in blocks
chriszarate Dec 11, 2024
f62fea4
SERVICE CONFIG: Update DataSourceCrud to use service configs instead …
chriszarate Dec 12, 2024
effc3cf
SERVICE CONFIG: Update DataSource frontend to use new service config …
chriszarate Dec 16, 2024
779349a
SERVICE CONFIG: Update HttpSettings to use new service config
chriszarate Dec 18, 2024
f10aa4a
BLOCK CONFIG: Register block using a config array with schema validation
chriszarate Dec 12, 2024
14cc2e2
BLOCK CONFIG: Update ConfigStore to use new block config
chriszarate Dec 12, 2024
5855046
BLOCK CONFIG: Update BlockBindings to use the new block config
chriszarate Dec 12, 2024
9a02650
BLOCK CONFIG: Update BlockPatterns to use the new block config
chriszarate Dec 12, 2024
2f4ee57
BLOCK CONFIG: Update BlockRegistration to use the new block config
chriszarate Dec 12, 2024
7d608b3
BLOCK CONFIG: Update QueryOverrides to use new block configuration
chriszarate Dec 13, 2024
74832e6
BLOCK CONFIG: Remove unused output type from client-side block types
chriszarate Dec 12, 2024
4129252
INTEGRATIONS: Update ExampleApi to use new interfaces
chriszarate Dec 11, 2024
3288031
INTEGRATIONS: Remove GenericHttpDataSource in favor of HttpDataSource
chriszarate Dec 12, 2024
13187d5
INTEGRATIONS: Update Airtable integration to use new interfaces
chriszarate Dec 8, 2024
820b195
INTEGRATIONS: Update GitHubDataSource to use new interfaces
chriszarate Dec 11, 2024
1993e2c
INTEGRATIONS: Update GoogleSheetsDataSource to use new interfaces
chriszarate Dec 12, 2024
4b3d848
INTEGRATIONS: Update SalesforceB2C integration to use new interfaces
chriszarate Dec 12, 2024
b7052cb
INTEGRATIONS: Update Shopify integration to use new interfaces
chriszarate Dec 11, 2024
8512c1d
INTEGRATIONS: Update Block Data API integration to use new interfaces
chriszarate Dec 11, 2024
56b3b87
EXAMPLES: Update Airtable Elden Ring example to use new interfaces
chriszarate Dec 11, 2024
9456aab
EXAMPLES: Update Airtable Conference Event example to use new interfaces
chriszarate Dec 12, 2024
8a229e8
EXAMPLES: Update Art Institute example to use new interfaces
chriszarate Dec 12, 2024
2619144
EXAMPLES: Update GitHub example to use new interfaces
chriszarate Dec 11, 2024
6715285
EXAMPLES: Update Google Sheets example to use new interfaces
chriszarate Dec 9, 2024
3634c3b
EXAMPLES: Update Shopify example
chriszarate Dec 12, 2024
acf96a1
EXAMPLES: Update Zip code example to use new interfaces
chriszarate Dec 8, 2024
d294c80
DOCS: Update docs and docblocks
chriszarate Dec 18, 2024
6071c3a
TESTS: TracksAnalyticsTest
chriszarate Dec 11, 2024
c63c311
CLEANUP: Add display_name to all map_service_config methods
chriszarate Dec 18, 2024
84e5b8f
BUGFIX: Use first member of arrays masquerading as primitives
chriszarate Dec 17, 2024
0399689
BUGFIX: Retrieve HTML response from GitHub API when loading file
chriszarate Dec 18, 2024
4da6fde
DOCS: Improve block pattern example
chriszarate Dec 19, 2024
802fdcf
DOCS: Improve data source example
chriszarate Dec 19, 2024
94e0f63
DOCS: Improve QueryRunner documentation
chriszarate Dec 19, 2024
9545dd9
DOCS: Explain object caching mechanism
chriszarate Dec 19, 2024
2fb45d6
DOCS: Fix formatting
chriszarate Dec 19, 2024
5060870
DOCS: Update workflows and query docs per feedback
chriszarate Dec 19, 2024
ef24f6d
BUGFIX: Ensure unique key
chriszarate Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[![Launch in WordPress Playground](https://img.shields.io/badge/Launch%20in%20WordPress%20Playground-blue?style=for-the-badge)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/Automattic/remote-data-blocks/trunk/blueprint.json)

[Launch the plugin in WordPress Playground](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/Automattic/remote-data-blocks/trunk/blueprint.json) and explore. An example API ("Conference Events") is included, or visit Settings > Remote Data Blocks to add your own. Visit the [workflows guide](docs/workflows/index.md) to dive in.
[Launch the plugin in WordPress Playground](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/Automattic/remote-data-blocks/trunk/blueprint.json) and explore. An example API ("Conference Event") is included, or visit Settings > Remote Data Blocks to add your own. Visit the [workflows guide](docs/workflows/index.md) to dive in.

## Installation

Expand Down
10 changes: 6 additions & 4 deletions docs/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ Remote data blocks are custom blocks, but they are created and registered by our

## Data sources and queries

Each remote data block is associated with a **data source** and a **query** that defines how data is fetched, processed, and displayed. Simple data sources and queries can be configured via the plugin's settings screen, while others may require custom PHP code (see [extending](../extending/index.md)).
Each remote data block is associated with at least one **query** that defines how data is fetched, processed, and displayed. Queries delegate some logic to a **data source**, which can be reused by multiple queries.

Simple data sources and queries can be configured via the plugin's settings screen, while others may require custom PHP code (see [extending](../extending/index.md)).

## Data fetching

Data fetching is handled by the plugin and wraps `wp_remote_request`. When a request to your site resolves to one or more remote data blocks, the remote data will be fetched and potentially cached by our plugin. Multiple requests for the same data will be deduped, even if the requests are not cacheable.
Data fetching is handled by the plugin and wraps `wp_remote_request`. When a request to your site resolves to one or more remote data blocks, the remote data will be fetched and potentially cached by our plugin. Multiple requests for the same data within a single page load will be deduped, even if the requests are not cacheable.

### Caching

The plugin offers a caching layer for optimal performance and to help avoid rate limiting from remote data sources. If your WordPress environment has configured a [persistent object cache](https://developer.wordpress.org/reference/classes/wp_object_cache/#persistent-cache-plugins), it will be used. Otherwise, the plugin will utilize in-memory (per-request) caching. Deploying to production without a persistent object cache is not recommended.
The plugin offers a caching layer for optimal performance and to help avoid rate limiting from remote data sources. If your WordPress environment has configured a [persistent object cache](https://developer.wordpress.org/reference/classes/wp_object_cache/#persistent-cache-plugins), it will be used. Otherwise, the plugin will utilize in-memory (per-page-load) caching. Deploying to production without a persistent object cache is not recommended.

The default TTL for all cache objects is 60 seconds, but can be adjusted by extending the query class and [overriding the `get_cache_ttl` method](../extending/query.md#get_cache_ttl).
The default TTL for all cache objects is 60 seconds, but it can be [configured per query or per request](../extending/query.md#get_cache_ttl).

## Theming

Expand Down
19 changes: 11 additions & 8 deletions docs/extending/block-patterns.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Block patterns

Patterns allow you to represent your remote data if different ways. By default, the plugin registers a unstyled block pattern that you can use out of the box. You can create additional patterns in the WordPress Dashboard or programmatically using the `register_remote_data_block_pattern` function.
Patterns allow you to represent your remote data if different ways. By default, the plugin registers a unstyled block pattern that you can use out of the box. You can create additional patterns in the WordPress Dashboard or programmatically by passing a `patterns` property to your block options.

Example:

Expand All @@ -18,11 +18,14 @@ Example:
```

```php
function register_your_block_pattern() {
$block_name = 'Your Custom Block';
$block_pattern = file_get_contents( '/path/to/your-pattern.html' );

register_remote_data_block_pattern( $block_name, 'Pattern Title', $block_pattern );
}
add_action( 'init', 'YourNamespace\\register_your_block_pattern', 10, 0 );
register_remote_data_block( [
'title' => 'My Remote Data Block',
'queries' => [ /* ... */ ],
'patterns' => [
[
'title' => 'My Pattern',
'content' => file_get_contents( __DIR__ . '/my-pattern.html' ),
],
],
] );
```
43 changes: 38 additions & 5 deletions docs/extending/block-registration.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
# Block registration

Use the `register_remote_data_block` function to register your block and associate it with your query and data source.
Use the `register_remote_data_block` function to register your block and associate it with your query and data source. This example:

1. Creates a data source
2. Associates the data source with a query
3. Defines the output schema of a query, which tells the plugin how to map the query response to blocks.
4. Registers a remote data block.

```php
function register_your_custom_block() {
$block_name = 'Your Custom Block';
$your_data_source = new YourCustomDataSource();
$your_query = new YourCustomQuery( $your_data_source );
$data_source = HttpDataSource::from_array( [
'service_config' => [
'__version' => 1,
'display_name' => 'Example API',
'endpoint' => 'https://api.example.com/',
],
] );

$display_query = HttpQuery::from_array( [
'display_name' => 'Example Query',
'data_source' => $data_source,
'output_schema' => [
'type' => [
'id => [
'name' => 'ID',
'path' => '$.id',
'type' => 'id',
],
'title' => [
'name' => 'Title',
'path' => '$.title',
'type' => 'string',
],
],
],
] );

register_remote_data_block( $block_name, $your_query );
register_remote_data_block( [
'title' => 'My Block',
'queries' => [
'display' => $display_query,
],
] );
chriszarate marked this conversation as resolved.
Show resolved Hide resolved
}
add_action( 'init', 'YourNamespace\\register_your_custom_block', 10, 0 );
```
56 changes: 19 additions & 37 deletions docs/extending/data-source.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,41 @@
# Data source

A data source defines basic reusable properties of an API and is required to define a [query](query.md).

## DataSourceInterface

At its simplest, a data source implements `DataSourceInterface` and describes itself with the following methods:

- `get_display_name(): string`: Return the display name of the data source.
- `get_image_url(): string|null`: Optionally, return an image URL that can represent the data source in UI.

## HttpDataSource

The `HttpDataSource` class implements `DataSourceInterface` and provides common reusable properties of an HTTP API:

- `get_endpoint(): string`: Returns the base URL of the API endpoint. This can be overridden by a query.
- `get_request_headers(): array`: Returns an associative array of HTTP headers to be sent with each request. This is a common place to set authentication headers such as `Authorization`. This array can be extended or overridden by a query.
A data source defines basic reusable properties of an API and is used by a [query](query.md) to reduce boilerplate. It allows helps this plugin represent your data source in the plugin settings screen and other UI.

## Example

Most HTTP-powered APIs can be represented by defining a class that extends `HttpDataSource`. Here's an example of a data source for US ZIP code data:
Most HTTP-powered APIs can be represented by defining a class that extends `HttpDataSource`. Here's an example of a data source for an example HTTP API:

```php
class ZipCodeDataSource extends HttpDataSource {
public function get_display_name(): string {
return 'US ZIP codes';
}

public function get_endpoint(): string {
return 'https://api.zippopotam.us/us/';
}

public function get_request_headers(): array|WP_Error {
return [
$data_source = HttpDataSource::from_array( [
'service_config' => [
'__version' => 1,
'display_name' => 'Example API',
'endpoint' => 'https://api.example.com/',
'request_headers' => [
'Content-Type' => 'application/json',
];
}
}
'X-Api-Key': MY_API_KEY_CONSTANT,
],
],
] );
```

The logic to fetch data from the API is defined in a [query](query.md).
The configuration array passed to `from_array` is very flexible, so it's usually not necessary to extend `HttpDataSource`, but you can do so if you need to add custom behavior.

## Custom data source
## Custom data sources

APIs that do not use HTTP as transport may require a custom data source. Implement `DataSourceInterface` and provide methods that define reusable properties of your API. The actual implementation of your transport will likely be provided via a [custom query runner](./query-runner.md).
For APIs that use non-HTTP transports, you can also implement `DataSourceInterface` and provide methods that define reusable properties of your API. The actual implementation of your transport will need to be provided by a [custom query runner](./query-runner.md).

Here is an example of a data source for a WebDAV server:
Here is a theoretical example of a data source for a WebDAV server:

```php
class WebDavFilesDataSource implements DataSourceInterface {
public function get_display_name(): string {
return 'WebDAV Files';
return 'My WebDAV Files';
}

public function get_image_url(): null {
return null;
public function get_image_url(): string {
return 'https://example.com/webdav-icon.png';
}

public function get_webdav_root(): string {
Expand Down
6 changes: 3 additions & 3 deletions docs/extending/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ add_filter( 'remote_data_blocks_register_example_block', '__return_false' );
Filter the allowed URL schemes for this request. By default, only HTTPS is allowed, but it might be useful to relax this restriction in local environments.

```php
function custom_allowed_url_schemes( array $allowed_url_schemes, HttpQueryContext $query_context ): array {
function custom_allowed_url_schemes( array $allowed_url_schemes, HttpQueryInterface $query ): array {
// Modify the allowed URL schemes.
return $allowed_url_schemes;
}
Expand All @@ -48,7 +48,7 @@ add_filter( 'remote_data_blocks_allowed_url_schemes', 'custom_allowed_url_scheme
Filter the request details (method, options, url) before the HTTP request is dispatched.

```php
function custom_request_details( array $request_details, HttpQueryContext $query_context, array $input_variables ): array {
function custom_request_details( array $request_details, HttpQueryInterface $query, array $input_variables ): array {
// Modify the request details.
return $request_details;
}
Expand All @@ -60,7 +60,7 @@ add_filter( 'remote_data_blocks_request_details', 'custom_request_details', 10,
Filter the query response metadata, which are available as bindings for field shortcodes. In most cases, it is better to provide a custom query class and override the `get_response_metadata` method but this filter is available in case that is not possible.

```php
function custom_query_response_metadata( array $metadata, HttpQueryContext $query_context, array $input_variables ): array {
function custom_query_response_metadata( array $metadata, HttpQueryInterface $query, array $input_variables ): array {
// Modify the response metadata.
return $metadata;
}
Expand Down
13 changes: 6 additions & 7 deletions docs/extending/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ Data sources and queries can be configured on the settings screen, but sometimes

Here's a short overview of how data flows through the plugin when a post with a remote data block is rendered:

1. WordPress core loads the post content, parses the blocks, and recognizes that a paragraph block has a [block binding](https://make.wordpress.org/core/2024/03/06/new-feature-the-block-bindings-api/).
1. WordPress core loads the post content, parses the blocks, and recognizes that a paragraph block has a [block binding](../concepts/block-bindings.md).
2. WordPress core calls the block binding callback function: `BlockBindings::get_value()`.
3. The callback function inspects the paragraph block. Using the block context supplied by the parent remote data block, it determines which query to execute.
4. The query runner is loaded: `$query->get_query_runner()`.
5. The query runner executes the query: `$query_runner->execute()`.
6. Various properties of the query are requested by the query runner, including the endpoint, request headers, request method, and request body. Some of these properties are delegated to the data source (`$query->get_data_source()`).
7. The query is dispatched and the response data is inspected, formatted into a consistent shape, and returned to the block binding callback function.
8. The callback function extracts the requested field from the response data and returns it to WordPress core for rendering.
3. The callback function inspects the paragraph block. Using the block context supplied by the parent remote data block, it determines which [query](./query.md) to execute.
4. The query is executed: `$query->execute()` (usually by delegating to a [query runner](./query-runner.md)).
5. Various properties of the query are requested by the query runner, including the endpoint, request headers, request method, and request body. Some of these properties are delegated to the data source (`$query->get_data_source()`).
6. The query is dispatched and the response data is inspected, formatted into a consistent shape, and returned to the block binding callback function.
7. The callback function extracts the requested field from the response data and returns it to WordPress core for rendering.

## Customization

Expand Down
34 changes: 13 additions & 21 deletions docs/extending/query-runner.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
# Query runner

A query runner executes a query and processes the results. The default `QueryRunner` used by the [`HttpQueryContext` class](query.md#HttpQueryContext) is designed to work with most APIs that transact over HTTP and return JSON, but you may want to provide a custom query runner if:
A query runner executes a query and processes the results. The default `QueryRunner` used by the [`HttpQuery` class](query.md) is designed to work with most APIs that transact over HTTP and return JSON, but you may want to provide a custom query runner if:

- Your API does not respond with JSON or requires custom deserialization logic.
- Your API uses a non-HTTP transport.
- You want to implement custom processing of the response data that is not possible with the provided filters.

## QueryRunner
## Custom QueryRunner for HTTP queries

If your API transacts over HTTP and you want to customize the query runner, consider extending the `QueryRunner` class and overriding select methods.
If your API transacts over HTTP and you want to customize the query runner, consider extending the `QueryRunner` class and providing an instance to your query via the `query_runner` option. Here are the methods:

### execute( array $input_variables ): array|WP_Error
### execute( HttpQueryInterface $query, array $input_variables ): array|WP_Error

The `execute` method executes the query and returns the parsed data. The input variables for the current request are provided as an associative array (`[ $var_name => $value ]`).

### get_request_details( array $input_variables ): array|WP_Error
### deserialize_response( string $raw_response_data, array $input_variables ): mixed

By default, the `deserialize_response` assumes a JSON string and deserializes it using `json_decode`. Override this method to provide custom deserialization logic.

### get_request_details( HttpQueryInterface $query, array $input_variables ): array|WP_Error

The `get_request_details` method extracts and validates the request details provided by the query. The input variables for the current request are provided as an associative array (`[ $var_name => $value ]`). The return value is an associative array that provides the HTTP method, request options, origin, and URI.

### get_raw_response_data( array $input_variables ): array|WP_Error
### get_raw_response_data( HttpQueryInterface $query, array $input_variables ): array|WP_Error

The `get_raw_response_data` method dispatches the HTTP request and assembles the raw (pre-processed) response data. The input variables for the current request are provided as an associative array (`[ $var_name => $value ]`). The return value is an associative array that provides the response metadata and the raw response data.

### get_response_metadata( array $response_metadata, array $query_results ): array
### get_response_metadata( HttpQueryInterface $query, array $response_metadata, array $query_results ): array

The `get_response_metadata` method returns the response metadata for the query, which are available as bindings for [field shortcodes](field-shortcodes.md).

### map_fields( string|array|object|null $response_data, bool $is_collection ): ?array

The `map_fields` method maps fields from the API response data, adhering to the output schema defined by the query.

### get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string

The `get_field_value` method computes the field value based on the field type. Overriding this method can be useful if you have custom field types and want to format the value in a specific way (e.g., a custom date format).
## Custom query execution

## QueryRunnerInterface

If you want to implement a query runner from scratch, `QueryRunnerInterface` requires only a single method, `execute`:

### execute( array $input_variables ): array

The `execute` method executes the query and returns the parsed data. The input variables for the current request are provided as an associative array (`[ $var_name => $value ]`).
If your API uses a non-HTTP transport or you want full control over query execution, you should implement your own query that implements `QueryInterface` and provides a custom `execute` method.
Loading
Loading