Skip to content
Commits on Source (7)
{
"name": "drupal/jsonapi_extras",
"description": "JSON API Extras provides a means to override and provide limited configurations to the default zero-configuration implementation provided by the JSON API module.",
"type": "drupal-module",
"license": "GPL-2.0+",
"authors": [
{
"name": "Mateu Aguiló Bosch",
"email": "mateu.aguilo.bosch@gmail.com"
}
],
"require": {
"drupal/jsonapi": "^1.12"
}
}
services:
route_subscriber.alter_jsonapi:
class: Drupal\jsonapi_extras\EventSubscriber\JsonApiExtrasRouteAlterSubscriber
arguments:
- '@jsonapi.resource_type.repository'
- '@config.factory'
tags:
- { name: event_subscriber }
serializer.normalizer.field_item.jsonapi_extras:
class: Drupal\jsonapi_extras\Normalizer\FieldItemNormalizer
arguments:
......
<?php
namespace Drupal\jsonapi_extras\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Subscriber for overwrite JSON API routes.
*/
class JsonApiExtrasRouteAlterSubscriber implements EventSubscriberInterface {
/**
* The configuration object factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The JSON API resource repository.
*
* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
*/
protected $resourceTypeRepository;
/**
* JsonApiExtrasRouteAlterSubscriber constructor.
*
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
* The JSON API resource repository.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration object factory.
*/
public function __construct(ResourceTypeRepository $resource_type_repository, ConfigFactoryInterface $config_factory) {
$this->resourceTypeRepository = $resource_type_repository;
$this->configFactory = $config_factory;
}
/**
* Alters select routes to update the route path.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The event to process.
*/
public function onRoutingRouteAlterSetPaths(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
$prefix = $this->configFactory
->get('jsonapi_extras.settings')
->get('path_prefix');
// Overwrite the entry point.
$path = sprintf('/%s', $prefix);
$collection->get('jsonapi.resource_list')
->setPath($path);
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
foreach ($this->resourceTypeRepository->all() as $resource_type) {
// Overwrite routes.
$paths = $this->getPathsForResourceType($resource_type, $prefix);
foreach ($paths as $route_name => $path) {
$collection->get($route_name)->setPath($path);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetPaths'];
return $events;
}
/**
* Returns paths for a resource type.
*
* @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type
* The ConfigurableResourceType entity.
* @param string $prefix
* The path prefix.
*
* @return array
* An array of route paths.
*/
protected function getPathsForResourceType(ConfigurableResourceType $resource_type, $prefix) {
$entity_type_id = $resource_type->getEntityTypeId();
$bundle_id = $resource_type->getBundle();
// Callback to build the route name.
$build_route_name = function ($key) use ($resource_type) {
return sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $key);
};
// Base path.
$base_path = sprintf('/%s/%s/%s', $prefix, $entity_type_id, $bundle_id);
if (($resource_config = $resource_type->getJsonapiResourceConfig()) && ($config_path = $resource_config->get('path'))) {
$base_path = sprintf('/%s/%s', $prefix, $config_path);
}
$paths = [];
$paths[$build_route_name('collection')] = $base_path;
$paths[$build_route_name('individual')] = sprintf('%s/{%s}', $base_path, $entity_type_id);
$paths[$build_route_name('related')] = sprintf('%s/{%s}/{related}', $base_path, $entity_type_id);
$paths[$build_route_name('relationship')] = sprintf('%s/{%s}/relationships/{related}', $base_path, $entity_type_id);
return $paths;
}
}
......@@ -140,7 +140,7 @@ class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
}
$prefix = $this->config->get('path_prefix');
foreach ($this->resourceTypeRepository->getResourceTypes(TRUE) as $resource_type) {
foreach ($this->resourceTypeRepository->all() as $resource_type) {
/** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
$entity_type_id = $resource_type->getEntityTypeId();
$bundle = $resource_type->getBundle();
......
......@@ -69,9 +69,9 @@ class SingleNestedEnhancer extends ResourceFieldEnhancerBase {
* {@inheritdoc}
*/
public function getSettingsForm(array $resource_field_info) {
$settings = empty($resource_field_info['settings'])
$settings = empty($resource_field_info['enhancer']['settings'])
? $this->getConfiguration()
: $resource_field_info['settings'];
: $resource_field_info['enhancer']['settings'];
return [
'path' => [
......
......@@ -111,6 +111,21 @@ class ConfigurableResourceType extends ResourceType {
->get('include_count');
}
/**
* {@inheritdoc}
*/
public function getPath() {
$resource_config = $this->getJsonapiResourceConfig();
if (!$resource_config) {
return parent::getPath();
}
$config_path = $resource_config->get('path');
if (!$config_path) {
return parent::getPath();
}
return $config_path;
}
/**
* Get the resource field configuration.
*
......
......@@ -5,10 +5,8 @@ namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
......@@ -59,6 +57,13 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
*/
protected $enabledResourceTypes;
/**
* A list of all resource configuration entities.
*
* @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[]
*/
protected $resourceConfigs;
/**
* {@inheritdoc}
*/
......@@ -76,46 +81,10 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
*/
public function all() {
if (!$this->all) {
$this->all = $this->getResourceTypes(FALSE);
}
return $this->all;
}
/**
* {@inheritdoc}
*/
public function get($entity_type_id, $bundle) {
if (empty($entity_type_id)) {
throw new PreconditionFailedHttpException('Server error. The current route is malformed.');
}
foreach ($this->getResourceTypes(FALSE) as $resource) {
if ($resource->getEntityTypeId() == $entity_type_id && $resource->getBundle() == $bundle) {
return $resource;
}
}
return NULL;
}
/**
* Returns an array of resource types.
*
* @param bool $include_disabled
* TRUE to included disabled resource types.
*
* @return array
* An array of resource types.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function getResourceTypes($include_disabled = TRUE) {
if (!isset($this->resourceTypes)) {
$this->resourceTypes = [];
foreach ($this->getEntityTypeBundleTuples() as $tuple) {
list($entity_type_id, $bundle) = $tuple;
$resource_config_id = sprintf('%s--%s', $entity_type_id, $bundle);
$this->resourceTypes[] = new ConfigurableResourceType(
$this->all[] = new ConfigurableResourceType(
$entity_type_id,
$bundle,
$this->entityTypeManager->getDefinition($entity_type_id)->getClass(),
......@@ -124,22 +93,12 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
$this->configFactory
);
}
foreach ($this->resourceTypes as $resource_type) {
foreach ($this->all as $resource_type) {
$relatable_resource_types = $this->calculateRelatableResourceTypes($resource_type);
$resource_type->setRelatableResourceTypes($relatable_resource_types);
}
}
if (!isset($this->enabledResourceTypes) && !$include_disabled) {
$this->enabledResourceTypes = $this->filterOutDisabledResourceTypes(
$this->resourceTypes,
$this->getResourceConfigs()
);
}
return ($include_disabled) ?
$this->resourceTypes :
$this->enabledResourceTypes;
return $this->all;
}
/**
......@@ -168,39 +127,17 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
function getResourceConfigs() {
$resource_config_ids = [];
foreach ($this->getEntityTypeBundleTuples() as $tuple) {
list($entity_type_id, $bundle) = $tuple;
$resource_config_ids[] = sprintf('%s--%s', $entity_type_id, $bundle);
if (!$this->resourceConfigs) {
$resource_config_ids = [];
foreach ($this->getEntityTypeBundleTuples() as $tuple) {
list($entity_type_id, $bundle) = $tuple;
$resource_config_ids[] = sprintf('%s--%s', $entity_type_id, $bundle);
}
$this->resourceConfigs = $this->entityTypeManager
->getStorage('jsonapi_resource_config')
->loadMultiple($resource_config_ids);
}
/** @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[] $resource_configs */
$resource_configs = $this->entityTypeManager
->getStorage('jsonapi_resource_config')
->loadMultiple($resource_config_ids);
return $resource_configs;
}
/**
* Takes a list of resource types and removes the disabled from it.
*
* @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
* The list of resource types including disabled ones.
* @param \Drupal\Core\Entity\EntityInterface[] $resource_configs
* The configuration entities that accompany the resource types.
*
* @return \Drupal\jsonapi\ResourceType\ResourceType[]
* The list of enabled resource types.
*/
protected function filterOutDisabledResourceTypes($resource_types, $resource_configs) {
return array_filter($resource_types, function (ResourceType $resource_type) use ($resource_configs) {
$resource_config_id = sprintf(
'%s--%s',
$resource_type->getEntityTypeId(),
$resource_type->getBundle()
);
$resource_config = isset($resource_configs[$resource_config_id]) ? $resource_configs[$resource_config_id] : new NullJsonapiResourceConfig([], '');
return !$resource_config->get('disabled');
});
return $this->resourceConfigs;
}
/**
......@@ -223,4 +160,13 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
}, []);
}
/**
* {@inheritdoc}
*/
public function getPathPrefix() {
return $this->configFactory
->get('jsonapi_extras.settings')
->get('path_prefix');
}
}
......@@ -5,6 +5,8 @@ namespace Drupal\Tests\jsonapi_extras\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
......@@ -46,6 +48,26 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
FieldStorageConfig::create([
'field_name' => 'field_timestamp',
'entity_type' => 'node',
'type' => 'timestamp',
'settings' => [],
'cardinality' => 1,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_timestamp',
'label' => 'Timestamp',
'entity_type' => 'node',
'bundle' => 'article',
'required' => FALSE,
'settings' => [],
'description' => '',
]);
$field_config->save();
$config = \Drupal::configFactory()->getEditable('jsonapi_extras.settings');
$config->set('path_prefix', 'api');
$config->set('include_count', TRUE);
......@@ -53,6 +75,8 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), ['access jsonapi resource list']);
static::overrideResources();
$this->resetAll();
$role = $this->user->get('roles')[0]->entity;
$this->grantPermissions($role, ['administer nodes', 'administer site configuration']);
}
/**
......@@ -161,9 +185,9 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
$this->tags[1]->vocabs->set(0, 'tags');
$this->tags[1]->save();
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/vocabs'));
$this->assertEmpty($output['data']);
$this->assertTrue(empty($output['data']));
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/relationships/vocabs'));
$this->assertEmpty($output['data']);
$this->assertTrue(empty($output['data']));
// 15. Test included resource.
$output = Json::decode($this->drupalGet(
......@@ -209,11 +233,9 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
'attributes' => [
'langcode' => 'en',
'title' => 'My custom title',
'isPublished' => '1',
'isPromoted' => '1',
'default_langcode' => '1',
'body' => 'Custom value',
'updatedAt' => '2017-12-23T08:45:17+0100',
'timestamp' => '2017-12-23T08:45:17+0100',
],
'relationships' => [
'contentType' => [
......@@ -244,9 +266,9 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
$this->assertArrayHasKey('internalId', $created_response['data']['attributes']);
$this->assertCount(2, $created_response['data']['relationships']['tags']['data']);
$this->assertSame($created_response['data']['links']['self'], $response->getHeader('Location')[0]);
$date = new \DateTime($body['data']['attributes']['updatedAt']);
$date = new \DateTime($body['data']['attributes']['timestamp']);
$created_node = Node::load($created_response['data']['attributes']['internalId']);
$this->assertSame((int) $date->format('U'), (int) $created_node->getChangedTime());
$this->assertSame((int) $date->format('U'), (int) $created_node->get('field_timestamp')->value);
// 2. Successful relationships PATCH.
$uuid = $created_response['data']['id'];
......@@ -409,6 +431,15 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
'enhancer' => ['id' => 'uuid_link'],
'disabled' => FALSE,
],
'field_timestamp' => [
'fieldName' => 'field_timestamp',
'publicName' => 'timestamp',
'enhancer' => [
'id' => 'date_time',
'settings' => ['dateTimeFormat' => 'Y-m-d\TH:i:sO'],
],
'disabled' => FALSE,
],
'comment' => [
'fieldName' => 'comment',
'publicName' => 'comment',
......