diff --git a/inc/Config/QueryContext/HttpQueryContext.php b/inc/Config/QueryContext/HttpQueryContext.php index f41610f1..2abfcec1 100644 --- a/inc/Config/QueryContext/HttpQueryContext.php +++ b/inc/Config/QueryContext/HttpQueryContext.php @@ -37,6 +37,10 @@ class HttpQueryContext implements QueryContextInterface, HttpQueryContextInterfa 'type' => 'array', 'required' => false, ], + 'transform' => [ + 'type' => 'function', + 'required' => false, + ], ], ], ], diff --git a/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index 90fb5261..820e6364 100644 --- a/inc/Editor/DataBinding/BlockBindings.php +++ b/inc/Editor/DataBinding/BlockBindings.php @@ -152,10 +152,45 @@ private static function apply_query_input_overrides( array $query_input, array $ return array_merge( $query_input, $overrides ); } - public static function execute_query( array $block_context, string $operation_name ): array|null { + /** + * Transform the query input for a block binding before executing the query if + * a transform function is provided. This allows the query input to be + * transformed in some way before the query is executed. This runs after the + * query input overrides have been applied. + */ + private static function transform_query_input( + array $query_input, + object $query_config + ): array { + $transformed_query_input = []; + + foreach ( $query_config->input_schema as $query_input_key => $query_input_schema ) { + if ( + isset( $query_input_schema['transform'] ) && + is_callable( $query_input_schema['transform'] ) + ) { + $transformed_query_input[ $query_input_key ] = $query_input_schema['transform']( + $query_input + ); + } + } + + return array_merge( $query_input, $transformed_query_input ); + } + + private static function get_query_input( array $block_context, object $query_config ): array { $block_name = $block_context['blockName']; $query_input = $block_context['queryInput']; $overrides = $block_context['queryInputOverrides'] ?? []; + + $query_input = self::apply_query_input_overrides( $query_input, $overrides, $block_name ); + $query_input = self::transform_query_input( $query_input, $query_config ); + + return $query_input; + } + + public static function execute_query( array $block_context, string $operation_name ): array|null { + $block_name = $block_context['blockName']; $block_config = ConfigStore::get_configuration( $block_name ); if ( null === $block_config ) { @@ -164,7 +199,7 @@ public static function execute_query( array $block_context, string $operation_na try { $query_config = $block_config['queries']['__DISPLAY__']; - $query_input = self::apply_query_input_overrides( $query_input, $overrides, $block_name ); + $query_input = self::get_query_input( $block_context, $query_config ); $query_runner = $query_config->get_query_runner(); $query_results = $query_runner->execute( $query_input ); diff --git a/tests/inc/Editor/DataBinding/BlockBindingsTest.php b/tests/inc/Editor/DataBinding/BlockBindingsTest.php new file mode 100644 index 00000000..c5064364 --- /dev/null +++ b/tests/inc/Editor/DataBinding/BlockBindingsTest.php @@ -0,0 +1,380 @@ + [ + 'name' => 'Test Input Field', + 'type' => 'string', + ], + ]; + + private const MOCK_OUTPUT_SCHEMA = [ + 'is_collection' => false, + 'mappings' => [ + 'output_field' => [ + 'name' => 'Output Field', + 'type' => 'string', + 'path' => '$.output_field', + ], + ], + ]; + private const MOCK_OUTPUT_FIELD_NAME = 'output_field'; + private const MOCK_OUTPUT_FIELD_VALUE = 'Test Output Value'; + private const MOCK_OUTPUT_QUERY_RESULTS = [ + 'is_collection' => false, + 'results' => [ + [ + 'result' => [ + self::MOCK_OUTPUT_FIELD_NAME => [ + 'value' => self::MOCK_OUTPUT_FIELD_VALUE, + ], + ], + ], + ], + ]; + + protected function tearDown(): void { + parent::tearDown(); + Mockery::close(); + MockWordPressFunctions::reset(); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_with_no_config(): void { + /** + * Mock the ConfigStore to return null. + */ + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( null ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [], + ]; + + $query_results = BlockBindings::execute_query( $block_context, 'test-operation' ); + + /** + * Assert that the query results are null as no configuration was found. + */ + $this->assertNull( $query_results ); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_returns_query_results(): void { + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 'Test Output Value' ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + ]; + + $mock_block_config = [ + 'queries' => [ + '__DISPLAY__' => new MockQueryContext( + $mock_qr, + self::MOCK_INPUT_SCHEMA, + self::MOCK_OUTPUT_SCHEMA, + ), + ], + ]; + + /** + * Mock the ConfigStore to return the block configuration. + */ + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $query_results = BlockBindings::execute_query( $block_context, self::MOCK_OPERATION_NAME ); + $this->assertSame( $query_results, self::MOCK_OUTPUT_QUERY_RESULTS ); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_with_overrides(): void { + /** + * Set the query var to an override value. + */ + MockWordPressFunctions::set_query_var( 'test_input_field', 'override_value' ); + + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( self::MOCK_OUTPUT_FIELD_NAME, self::MOCK_OUTPUT_FIELD_VALUE ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + 'queryInputOverrides' => [ + 'test_input_field' => [ + 'type' => 'url', + 'display' => '/test_input_field/{test_input_field}', + ], + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + 'overrides' => [ + [ + 'target' => 'test_target', + 'type' => 'url', + ], + ], + ], + ]; + + $mock_block_config = [ + 'queries' => [ + '__DISPLAY__' => new MockQueryContext( + $mock_qr, + $input_schema, + self::MOCK_OUTPUT_SCHEMA, + ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $query_results = BlockBindings::execute_query( $block_context, self::MOCK_OPERATION_NAME ); + + $this->assertSame( $query_results, self::MOCK_OUTPUT_QUERY_RESULTS ); + + /** + * Assert that the query runner received the correct input after overrides were applied. + */ + $this->assertSame( $mock_qr->getLastExecuteCallInput(), [ + 'test_input_field' => 'override_value', + ] ); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_with_query_input_transformations(): void { + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 'Test Output Value' ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + 'transform' => function ( array $data ): string { + return $data['test_input_field'] . ' transformed'; + }, + ], + ]; + + $mock_block_config = [ + 'queries' => [ + '__DISPLAY__' => new MockQueryContext( + $mock_qr, + $input_schema, + self::MOCK_OUTPUT_SCHEMA, + ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $query_results = BlockBindings::execute_query( $block_context, self::MOCK_OPERATION_NAME ); + $this->assertSame( $query_results, self::MOCK_OUTPUT_QUERY_RESULTS ); + + /** + * Assert that the query runner received the correct input after transformations were applied. + */ + $this->assertSame( $mock_qr->getLastExecuteCallInput(), [ + 'test_input_field' => 'test_value transformed', + ] ); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_with_query_input_transformed_with_multiple_inputs(): void { + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 'Test Output Value' ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + 'another_input_field' => 'another_value', + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + 'transform' => function ( array $data ): string { + return $data['test_input_field'] . ' ' . $data['another_input_field']; + }, + ], + 'another_input_field' => [ + 'name' => 'Another Input Field', + 'type' => 'string', + ], + ]; + + $mock_block_config = [ + 'queries' => [ + '__DISPLAY__' => new MockQueryContext( + $mock_qr, + $input_schema, + self::MOCK_OUTPUT_SCHEMA, + ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $query_results = BlockBindings::execute_query( $block_context, self::MOCK_OPERATION_NAME ); + $this->assertSame( $query_results, self::MOCK_OUTPUT_QUERY_RESULTS ); + + /** + * Assert that the query runner received the correct input after transformations were applied. + */ + $this->assertSame( $mock_qr->getLastExecuteCallInput(), [ + 'test_input_field' => 'test_value another_value', + 'another_input_field' => 'another_value', + ] ); + } + + /** + * @runInSeparateProcess + */ + public function test_execute_query_with_query_input_transformations_and_overrides(): void { + /** + * Set the query var to an override value. + */ + MockWordPressFunctions::set_query_var( 'test_input_field', 'override_value' ); + + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 'Test Output Value' ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + 'queryInputOverrides' => [ + 'test_input_field' => [ + 'type' => 'url', + 'display' => '/test_input_field/{test_input_field}', + ], + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + 'transform' => function ( array $data ): string { + return $data['test_input_field'] . ' transformed'; + }, + ], + ]; + + $mock_block_config = [ + 'queries' => [ + '__DISPLAY__' => new MockQueryContext( + $mock_qr, + $input_schema, + self::MOCK_OUTPUT_SCHEMA, + ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $query_results = BlockBindings::execute_query( $block_context, self::MOCK_OPERATION_NAME ); + $this->assertSame( $query_results, self::MOCK_OUTPUT_QUERY_RESULTS ); + + /** + * Assert that the query runner received the correct input after transformations and overrides were applied. + */ + $this->assertSame( $mock_qr->getLastExecuteCallInput(), [ + 'test_input_field' => 'override_value transformed', + ] ); + } +} diff --git a/tests/inc/Mocks/MockQueryContext.php b/tests/inc/Mocks/MockQueryContext.php new file mode 100644 index 00000000..0b9cc72d --- /dev/null +++ b/tests/inc/Mocks/MockQueryContext.php @@ -0,0 +1,26 @@ +mock_qr; + } +} diff --git a/tests/inc/Mocks/MockQueryRunner.php b/tests/inc/Mocks/MockQueryRunner.php index bcdc26b0..5c4ade77 100644 --- a/tests/inc/Mocks/MockQueryRunner.php +++ b/tests/inc/Mocks/MockQueryRunner.php @@ -6,6 +6,7 @@ class MockQueryRunner implements QueryRunnerInterface { private $query_results = []; + private $execute_call_inputs = []; public function addResult( $field, $result ) { if ( $result instanceof \WP_Error ) { @@ -26,6 +27,11 @@ public function addResult( $field, $result ) { } public function execute( array $input_variables ): array|\WP_Error { + array_push( $this->execute_call_inputs, $input_variables ); return array_shift( $this->query_results ); } + + public function getLastExecuteCallInput(): array|null { + return end( $this->execute_call_inputs ) ?? null; + } } diff --git a/tests/inc/Mocks/MockWordPressFunctions.php b/tests/inc/Mocks/MockWordPressFunctions.php new file mode 100644 index 00000000..dbcb86a1 --- /dev/null +++ b/tests/inc/Mocks/MockWordPressFunctions.php @@ -0,0 +1,19 @@ +