Commit 3c609b27 authored by catch's avatar catch

Issue #3025372 by gabesullice, cantrellnm, aleevas, ravi.shankar, bander2,...

Issue #3025372 by gabesullice, cantrellnm, aleevas, ravi.shankar, bander2, blainelang, Wim Leers: [regression] Impossible to filter for resources with an empty relationship object in JSON:API 2.x

(cherry picked from commit 413fc69d)
parent d8fee2b2
......@@ -256,13 +256,16 @@ public static function resolveInternalIncludePath(ResourceType $resource_type, a
* The JSON:API resource type from which to resolve the field name.
* @param string $external_field_name
* The public field name to map to a Drupal field name.
* @param string $operator
* (optional) The operator of the condition for which the path should be
* resolved.
*
* @return string
* The mapped field name.
*
* @throws \Drupal\Core\Http\Exception\CacheableBadRequestHttpException
*/
public function resolveInternalEntityQueryPath(ResourceType $resource_type, $external_field_name) {
public function resolveInternalEntityQueryPath(ResourceType $resource_type, $external_field_name, $operator = NULL) {
$cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']);
if (empty($external_field_name)) {
throw new CacheableBadRequestHttpException($cacheability, 'No field name was provided for the filter.');
......@@ -355,7 +358,10 @@ public function resolveInternalEntityQueryPath(ResourceType $resource_type, $ext
// If there are no remaining path parts, the process is finished unless
// the field has multiple properties, in which case one must be specified.
if (empty($parts)) {
if ($property_specifier_needed) {
// If the operator is asserting the presence or absence of a
// relationship entirely, it does not make sense to require a property
// specifier.
if ($property_specifier_needed && (!$at_least_one_entity_reference_field || !in_array($operator, ['IS NULL', 'IS NOT NULL'], TRUE))) {
$possible_specifiers = array_map(function ($specifier) use ($at_least_one_entity_reference_field) {
return $at_least_one_entity_reference_field && $specifier !== 'id' ? "meta.$specifier" : $specifier;
}, $candidate_property_names);
......
......@@ -157,7 +157,8 @@ public static function createFromQueryParameter($parameter, ResourceType $resour
foreach ($expanded as &$filter_item) {
if (isset($filter_item[static::CONDITION_KEY][EntityCondition::PATH_KEY])) {
$unresolved = $filter_item[static::CONDITION_KEY][EntityCondition::PATH_KEY];
$filter_item[static::CONDITION_KEY][EntityCondition::PATH_KEY] = $field_resolver->resolveInternalEntityQueryPath($resource_type, $unresolved);
$operator = $filter_item[static::CONDITION_KEY][EntityCondition::OPERATOR_KEY];
$filter_item[static::CONDITION_KEY][EntityCondition::PATH_KEY] = $field_resolver->resolveInternalEntityQueryPath($resource_type, $unresolved, $operator);
}
}
return new static(static::buildEntityConditionGroup($expanded));
......
......@@ -918,6 +918,62 @@ public function testMapFieldTypeNormalizationFromIssue3040590() {
$this->assertSame(['foo' => 'bar'], $data['data'][0]['attributes']['data']);
}
/**
* Ensure filtering for entities with empty entity reference fields works.
*
* @see https://www.drupal.org/project/jsonapi/issues/3025372
*/
public function testEmptyRelationshipFilteringFromIssue3025372() {
// Set up data model.
$this->drupalCreateContentType(['type' => 'folder']);
$this->createEntityReferenceField(
'node',
'folder',
'field_parent_folder',
NULL,
'node',
'default',
[
'target_bundles' => ['folder'],
],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
$this->rebuildAll();
// Create data.
$node = Node::create([
'title' => 'root folder',
'type' => 'folder',
]);
$node->save();
// Test.
$user = $this->drupalCreateUser(['access content']);
$url = Url::fromRoute('jsonapi.node--folder.collection');
$request_options = [
RequestOptions::HEADERS => [
'Content-Type' => 'application/vnd.api+json',
'Accept' => 'application/vnd.api+json',
],
RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
];
$response = $this->request('GET', $url, $request_options);
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
$this->assertSame($node->uuid(), Json::decode((string) $response->getBody())['data'][0]['id']);
$response = $this->request('GET', $url->setOption('query', [
'filter[test][condition][path]' => 'field_parent_folder',
'filter[test][condition][operator]' => 'IS NULL',
]), $request_options);
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
$this->assertSame($node->uuid(), Json::decode((string) $response->getBody())['data'][0]['id']);
$response = $this->request('GET', $url->setOption('query', [
'filter[test][condition][path]' => 'field_parent_folder',
'filter[test][condition][operator]' => 'IS NOT NULL',
]), $request_options);
$this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
$this->assertEmpty(Json::decode((string) $response->getBody())['data']);
}
/**
* Tests that the response still has meaningful error messages.
*/
......
......@@ -413,7 +413,7 @@ public function testCreateFromQueryParameterNested() {
*/
protected function getFieldResolverMock(ResourceType $resource_type) {
$field_resolver = $this->prophesize(FieldResolver::class);
$field_resolver->resolveInternalEntityQueryPath($resource_type, Argument::any())->willReturnArgument(1);
$field_resolver->resolveInternalEntityQueryPath($resource_type, Argument::any(), Argument::any())->willReturnArgument(1);
return $field_resolver->reveal();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment