Skip to content
Commits on Source (20)
# JSON API Extras
This module provides extra functionality on top of JSON API. You should not need this module to get an spec compliant JSON API, this module is to customize the output of JSON API.
This module provides extra functionality on top of JSON API. You should not
need this module to get an spec compliant JSON API, this module is to
customize the output of JSON API.
This module adds the following features:
......@@ -11,4 +13,5 @@ This module adds the following features:
- Lets you remove fields from the JSON API output.
TODO:
* Auto calculate the dependency of the provider of the entity type and bundles in the configuration entity.
* Auto calculate the dependency of the provider of the entity type and
bundles in the configuration entity.
{
"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"
}
}
<?php
/**
* @file
* Module implementation file.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
......@@ -10,7 +15,7 @@ function jsonapi_extras_help($route_name, RouteMatchInterface $route_match) {
case 'entity.jsonapi_resource_config.collection':
$output = '';
$output .= '<p>' . t('The following table shows the list of JSON API resources available.') . '</p>';
$output .= '<p>' . t('Use the overwrite operation to overwrite a resource\'s configuration. You can revert back to the default configuration using the revert operation.') . '</p>';
$output .= '<p>' . t("Use the overwrite operation to overwrite a resource's configuration. You can revert back to the default configuration using the revert operation.") . '</p>';
return $output;
}
}
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:
......@@ -14,18 +6,18 @@ services:
- '@entity_type.manager'
- '@plugin.manager.resource_field_enhancer'
tags:
- { name: normalizer, priority: 25 }
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 25 }
serializer.normalizer.entity.jsonapi_extras:
class: Drupal\jsonapi_extras\Normalizer\ContentEntityNormalizer
arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
tags:
- { name: normalizer, priority: 22 }
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 22 }
serializer.normalizer.config_entity.jsonapi_extras:
class: Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer
arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
tags:
- { name: normalizer, priority: 22 }
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 22 }
plugin.manager.resource_field_enhancer:
class: Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
......
......@@ -83,9 +83,24 @@ class JsonapiResourceConfig extends ConfigEntityBase {
*/
public function calculateDependencies() {
parent::calculateDependencies();
$id = explode('--',$this->id);
$id = explode('--', $this->id);
$typeManager = $this->entityTypeManager();
$dependency = $typeManager->getDefinition($id[0])->getBundleConfigDependency($id[1]);
$this->addDependency($dependency['type'], $dependency['name']);
}
/**
* {@inheritdoc}
*/
protected function urlRouteParameters($rel) {
$uri_route_parameters = parent::urlRouteParameters($rel);
// The add-form route depends on entity_type_id and bundle.
if (in_array($rel, ['add-form'])) {
$parameters = explode('--', $this->id);
$uri_route_parameters['entity_type_id'] = $parameters[0];
$uri_route_parameters['bundle'] = $parameters[1];
}
return $uri_route_parameters;
}
}
<?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 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;
}
}
......@@ -2,14 +2,42 @@
namespace Drupal\jsonapi_extras\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\ProxyClass\Routing\RouteBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure JSON API settings for this site.
*/
class JsonapiExtrasSettingsForm extends ConfigFormBase {
protected $routerBuilder;
/**
* Constructs a \Drupal\system\ConfigFormBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\ProxyClass\Routing\RouteBuilder $router_builder
* The router builder to rebuild menus after saving config entity.
*/
public function __construct(ConfigFactoryInterface $config_factory, RouteBuilder $router_builder) {
parent::__construct($config_factory);
$this->routerBuilder = $router_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('router.builder')
);
}
/**
* {@inheritdoc}
*/
......@@ -64,7 +92,7 @@ class JsonapiExtrasSettingsForm extends ConfigFormBase {
->save();
// Rebuild the router.
\Drupal::service('router.builder')->setRebuildNeeded();
$this->routerBuilder->setRebuildNeeded();
parent::submitForm($form, $form_state);
}
......
......@@ -73,6 +73,8 @@ class JsonapiResourceConfigForm extends EntityForm {
protected $request;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
......@@ -93,7 +95,9 @@ class JsonapiResourceConfigForm extends EntityForm {
* @param \Drupal\Core\Config\ImmutableConfig $config
* The config instance.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
* The typed config manager.
*/
public function __construct(EntityTypeBundleInfoInterface $bundle_info, ResourceTypeRepository $resource_type_repository, EntityFieldManager $field_manager, EntityTypeRepositoryInterface $entity_type_repository, ResourceFieldEnhancerManager $enhancer_manager, ImmutableConfig $config, Request $request, TypedConfigManagerInterface $typed_config_manager) {
$this->bundleInfo = $bundle_info;
......@@ -133,22 +137,18 @@ class JsonapiResourceConfigForm extends EntityForm {
$entity_type_id = $this->request->get('entity_type_id');
$bundle = $this->request->get('bundle');
/** @var JsonapiResourceConfig $entity */
/** @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity */
$entity = $this->getEntity();
$resource_id = $entity->get('id');
// If we are editing an entity we don't want the Entity Type and Bundle
// picker, that info is locked.
if (!$entity_type_id || !$bundle) {
if ($resource_id) {
list($entity_type_id, $bundle) = explode('--', $resource_id);
$form['#title'] = $this->t('Edit %label resource config', ['%label' => $resource_id]);
}
else {
list($entity_type_id, $bundle) = $this->buildEntityTypeBundlePicker($form, $form_state);
if (!$entity_type_id) {
return $form;
}
if (!$resource_id) {
// We can't build the form without an entity type and bundle.
throw new \InvalidArgumentException('Unable to load entity type or bundle for the overrides form.');
}
list($entity_type_id, $bundle) = explode('--', $resource_id);
$form['#title'] = $this->t('Edit %label resource config', ['%label' => $resource_id]);
}
if ($entity_type_id && $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle)) {
......@@ -211,19 +211,6 @@ class JsonapiResourceConfigForm extends EntityForm {
$form_state->setRedirectUrl($resource_config->urlInfo('collection'));
}
/**
* Implements callback for Ajax event on entity type or bundle selection.
*
* @param array $form
* From render array.
*
* @return array
* Color selection section of the form.
*/
public function bundleCallback(array &$form) {
return $form['bundle_wrapper'];
}
/**
* Builds the part of the form that contains the overrides.
*
......@@ -245,7 +232,8 @@ class JsonapiResourceConfigForm extends EntityForm {
}, $this->fieldManager->getFieldDefinitions($entity_type_id, $bundle));
}
else {
$field_names = array_keys($entity_type->getKeys());
$field_names = array_keys($entity_type->getPropertiesToExport());
array_unshift($field_names, $entity_type->getKey('id'));
}
$overrides_form['overrides']['entity'] = [
......@@ -308,12 +296,12 @@ class JsonapiResourceConfigForm extends EntityForm {
$markup = '';
$markup .= '<dl>';
$markup .= '<dt>' . t('Disabled') . '</dt>';
$markup .= '<dd>' . t('Check this if you want to disable this field completely. Disabling required fields will cause problems when writing to the resource.') . '</dd>';
$markup .= '<dt>' . t('Alias') . '</dt>';
$markup .= '<dd>' . t('Overrides the field name with a custom name. Example: Change "field_tags" to "tags".') . '</dd>';
$markup .= '<dt>' . t('Enhancer') . '</dt>';
$markup .= '<dd>' . t('Select an enhancer to manipulate the public output coming in and out.') . '</dd>';
$markup .= '<dt>' . $this->t('Disabled') . '</dt>';
$markup .= '<dd>' . $this->t('Check this if you want to disable this field completely. Disabling required fields will cause problems when writing to the resource.') . '</dd>';
$markup .= '<dt>' . $this->t('Alias') . '</dt>';
$markup .= '<dd>' . $this->t('Overrides the field name with a custom name. Example: Change "field_tags" to "tags".') . '</dd>';
$markup .= '<dt>' . $this->t('Enhancer') . '</dt>';
$markup .= '<dd>' . $this->t('Select an enhancer to manipulate the public output coming in and out.') . '</dd>';
$markup .= '</dl>';
$overrides_form['overrides']['fields']['info'] = [
'#markup' => $markup,
......@@ -346,7 +334,7 @@ class JsonapiResourceConfigForm extends EntityForm {
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var JsonapiResourceConfig $entity */
/** @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig $entity */
$entity = parent::buildEntity($form, $form_state);
// Trim slashes from path.
......@@ -444,76 +432,6 @@ class JsonapiResourceConfigForm extends EntityForm {
return $overrides_form;
}
/**
* Build the entity picker widget and return the entity type and bundle IDs.
*
* @param array $form
* The form passed by reference to update it.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the form.
*
* @return array
* The entity types ID and the bundle ID.
*/
protected function buildEntityTypeBundlePicker(array &$form, FormStateInterface $form_state) {
$form['_entity_type_id'] = [
'#title' => $this->t('Entity Type'),
'#type' => 'select',
'#options' => $this->entityTypeRepository->getEntityTypeLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => TRUE,
'#ajax' => [
'callback' => '::bundleCallback',
'wrapper' => 'bundle-wrapper',
],
];
if (isset($parameter['entity_type_id'])) {
$form['_entity_type_id'] = [
'#type' => 'hidden',
'#value' => $parameter['entity_type_id'],
];
}
$form['bundle_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'bundle-wrapper'],
];
if (!$entity_type_id = $form_state->getValue('_entity_type_id')) {
return [$entity_type_id, NULL];
}
$has_bundles = (bool) $this->entityTypeManager
->getDefinition($entity_type_id)->getBundleEntityType();
if ($has_bundles) {
$bundles = [];
$bundle_info = $this->bundleInfo->getBundleInfo($entity_type_id);
foreach ($bundle_info as $bundle_id => $info) {
$bundles[$bundle_id] = $info['translatable'] ? $this->t($info['label']) : $info['label'];
}
$form['bundle_wrapper']['_bundle_id'] = [
'#type' => 'select',
'#empty_option' => $this->t('- Select -'),
'#title' => $this->t('Bundle'),
'#options' => $bundles,
'#required' => TRUE,
'#ajax' => [
'callback' => '::bundleCallback',
'wrapper' => 'bundle-wrapper',
],
];
}
else {
$form['bundle_wrapper']['_bundle_id'] = [
'#type' => 'hidden',
'#value' => $entity_type_id,
];
}
$bundle = $has_bundles
? $form_state->getValue('_bundle_id')
: $entity_type_id;
return [$entity_type_id, $bundle];
}
/**
* AJAX callback to get the form settings for the enhancer for a field.
*
......
......@@ -4,11 +4,8 @@ namespace Drupal\jsonapi_extras;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -35,9 +32,10 @@ class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
/**
* Constructs new JsonapiResourceConfigListBuilder.
*
* @param EntityTypeInterface $entity_type
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage.
* @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository $resource_type_repository
* The JSON API configurable resource type repository.
* @param \Drupal\Core\Config\ImmutableConfig $config
......@@ -112,7 +110,7 @@ class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
],
'#attached' => [
'library' => [
'jsonapi_extras/admin'
'jsonapi_extras/admin',
],
],
];
......@@ -130,19 +128,19 @@ class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
],
'#attributes' => [
'class' => [
'jsonapi-resources-table'
'jsonapi-resources-table',
],
],
'#attached' => [
'library' => [
'jsonapi_extras/admin',
]
],
],
];
}
$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();
......@@ -216,5 +214,4 @@ class JsonapiResourceConfigListBuilder extends ConfigEntityListBuilder {
return $list;
}
}
......@@ -61,7 +61,7 @@ class FieldItemNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = array()) {
public function normalize($object, $format = NULL, array $context = []) {
// First get the regular output.
$normalized_output = $this->subject->normalize($object, $format, $context);
// Then detect if there is any enhancer to be applied here.
......
......@@ -2,7 +2,6 @@
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
use Drupal\schemata_json_schema\Normalizer\jsonapi\FieldDefinitionNormalizer as SchemataJsonSchemaFieldDefinitionNormalizer;
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
......
......@@ -18,7 +18,9 @@ class SchemataSchemaNormalizer extends SchemataJsonSchemaSchemataSchemaNormalize
protected $resourceTypeRepository;
/**
* @param ResourceTypeRepository $resource_type_repository
* Constructs a SchemataSchemaNormalizer object.
*
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
* A resource repository.
*/
public function __construct(ResourceTypeRepository $resource_type_repository) {
......
......@@ -5,6 +5,8 @@ namespace Drupal\jsonapi_extras\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* The constraint object.
*
* @Constraint(
* id = "jsonapi_extras__duplicate_field",
* label = @Translation("Duplicate field", context = "Validation")
......@@ -12,6 +14,11 @@ use Symfony\Component\Validator\Constraint;
*/
class DuplicateFieldConstraint extends Constraint {
/**
* The error message for the constraint.
*
* @var string
*/
public $message = 'The override must be unique.';
}
......@@ -7,10 +7,13 @@ use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* The validator.
*/
class DuplicateFieldConstraintValidator extends ConstraintValidator {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
......@@ -29,7 +32,7 @@ class DuplicateFieldConstraintValidator extends ConstraintValidator {
$resourceFields = $entity_data['resourceFields'];
$overrides = [];
// Get the field values
// Get the field values.
foreach ($resourceFields as $field => $data) {
// Only get the overridden fields.
if ($data['fieldName'] != $data['publicName']) {
......@@ -49,7 +52,8 @@ class DuplicateFieldConstraintValidator extends ConstraintValidator {
->addViolation();
}
}
// Now compare the overrides with the default names to validate no dupes exist.
// Now compare the overrides with the default names to validate no dupes
// exist.
foreach ($overrides as $field => $override) {
if (array_key_exists($override, $resourceFields)) {
$this->context->buildViolation($constraint->message)
......@@ -58,7 +62,7 @@ class DuplicateFieldConstraintValidator extends ConstraintValidator {
}
}
// Validate URL and resource type
// Validate URL and resource type.
$resource_types = $this->entityTypeManager
->getStorage('jsonapi_resource_config')
->loadByProperties(['disabled' => FALSE]);
......@@ -68,7 +72,10 @@ class DuplicateFieldConstraintValidator extends ConstraintValidator {
}
if ($resource_type->get('resourceType') == $entity_data['resourceType']) {
$this->context->buildViolation('There is already resource (@name) with this resource type.', ['@name' => $resource_type->id()])
$this->context->buildViolation(
'There is already resource (@name) with this resource type.',
['@name' => $resource_type->id()]
)
->atPath('resourceType')
->addViolation();
}
......
......@@ -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' => [
......
......@@ -28,6 +28,8 @@ class ConfigurableResourceType extends ResourceType {
protected $enhancerManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
......@@ -49,7 +51,12 @@ class ConfigurableResourceType extends ResourceType {
* The configuration factory.
*/
public function __construct($entity_type_id, $bundle, $deserialization_target_class, JsonapiResourceConfig $resource_config, ResourceFieldEnhancerManager $enhancer_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($entity_type_id, $bundle, $deserialization_target_class);
parent::__construct(
$entity_type_id,
$bundle,
$deserialization_target_class,
(bool) $resource_config->get('disabled')
);
$this->jsonapiResourceConfig = $resource_config;
$this->enhancerManager = $enhancer_manager;
......@@ -104,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.
*
......
......@@ -7,8 +7,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
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;
/**
* Provides a repository of JSON API configurable resource types.
......@@ -30,23 +30,50 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
protected $enhancerManager;
/**
* @var ConfigFactoryInterface
* The bundle manager.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* A list of all resource types.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType[]
*/
protected $resourceTypes;
/**
* A list of only enabled resource types.
*
* @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType[]
*/
protected $enabledResourceTypes;
/**
* A list of all resource configuration entities.
*
* @var \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[]
*/
protected $resourceConfigs;
/**
* {@inheritdoc}
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager, EntityRepositoryInterface $entity_repository, ResourceFieldEnhancerManager $enhancer_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($entity_type_manager, $bundle_manager);
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager, EntityFieldManagerInterface $entity_field_manager, EntityRepositoryInterface $entity_repository, ResourceFieldEnhancerManager $enhancer_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($entity_type_manager, $bundle_manager, $entity_field_manager);
$this->entityRepository = $entity_repository;
$this->enhancerManager = $enhancer_manager;
$this->configFactory = $config_factory;
$this->entityFieldManager = $entity_field_manager;
$this->bundleManager = $bundle_manager;
}
/**
......@@ -54,78 +81,92 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
*/
public function all() {
if (!$this->all) {
$this->all = $this->getResourceTypes(FALSE);
foreach ($this->getEntityTypeBundleTuples() as $tuple) {
list($entity_type_id, $bundle) = $tuple;
$resource_config_id = sprintf('%s--%s', $entity_type_id, $bundle);
$this->all[] = new ConfigurableResourceType(
$entity_type_id,
$bundle,
$this->entityTypeManager->getDefinition($entity_type_id)->getClass(),
$this->getResourceConfig($resource_config_id),
$this->enhancerManager,
$this->configFactory
);
}
foreach ($this->all as $resource_type) {
$relatable_resource_types = $this->calculateRelatableResourceTypes($resource_type);
$resource_type->setRelatableResourceTypes($relatable_resource_types);
}
}
return $this->all;
}
/**
* {@inheritdoc}
* Get a single resource configuration entity by its ID.
*
* @param string $resource_config_id
* The configuration entity ID.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function get($entity_type_id, $bundle) {
if (empty($entity_type_id)) {
throw new PreconditionFailedHttpException('Server error. The current route is malformed.');
}
protected function getResourceConfig($resource_config_id) {
$resource_configs = $this->getResourceConfigs();
return isset($resource_configs[$resource_config_id]) ?
$resource_configs[$resource_config_id] :
new NullJsonapiResourceConfig([], '');
}
foreach ($this->getResourceTypes() as $resource) {
if ($resource->getEntityTypeId() == $entity_type_id && $resource->getBundle() == $bundle) {
return $resource;
/**
* Load all resource configuration entities.
*
* @return \Drupal\jsonapi_extras\Entity\JsonapiResourceConfig[]
* The resource config entities.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
function getResourceConfigs() {
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);
}
return NULL;
return $this->resourceConfigs;
}
/**
* Returns an array of resource types.
*
* @param bool $include_disabled
* TRUE to included disabled resource types.
* Entity type ID and bundle iterator.
*
* @return array
* An array of resource types.
* A list of entity type ID and bundle tuples.
*/
public function getResourceTypes($include_disabled = TRUE) {
if (isset($this->resourceTypes)) {
return $this->resourceTypes;
}
protected function getEntityTypeBundleTuples() {
$entity_type_ids = array_keys($this->entityTypeManager->getDefinitions());
$resource_types = [];
$resource_config_ids = [];
foreach ($entity_type_ids as $entity_type_id) {
$bundles = array_keys($this->bundleManager->getBundleInfo($entity_type_id));
$resource_config_ids = array_merge($resource_config_ids, array_map(function ($bundle) use ($entity_type_id) {
return sprintf('%s--%s', $entity_type_id, $bundle);
}, $bundles));
}
$resource_configs = $this->entityTypeManager->getStorage('jsonapi_resource_config')->loadMultiple($resource_config_ids);
foreach ($entity_type_ids as $entity_type_id) {
// For each entity type return as many tuples as bundles.
return array_reduce($entity_type_ids, function ($carry, $entity_type_id) {
$bundles = array_keys($this->bundleManager->getBundleInfo($entity_type_id));
$current_types = array_map(function ($bundle) use ($entity_type_id, $include_disabled, $resource_configs) {
$resource_config_id = sprintf('%s--%s', $entity_type_id, $bundle);
$resource_config = isset($resource_configs[$resource_config_id]) ? $resource_configs[$resource_config_id] : new NullJsonapiResourceConfig([], '');
if (!$include_disabled && $resource_config->get('disabled')) {
return NULL;
}
return new ConfigurableResourceType(
$entity_type_id,
$bundle,
$this->entityTypeManager->getDefinition($entity_type_id)->getClass(),
$resource_config,
$this->enhancerManager,
$this->configFactory
);
// Get all the tuples for the current entity type.
$tuples = array_map(function ($bundle) use ($entity_type_id) {
return [$entity_type_id, $bundle];
}, $bundles);
$resource_types = array_merge($resource_types, $current_types);
}
// Append the tuples to the aggregated list.
return array_merge($carry, $tuples);
}, []);
}
$this->resourceTypes = array_filter($resource_types);
return $this->resourceTypes;
/**
* {@inheritdoc}
*/
public function getPathPrefix() {
return $this->configFactory
->get('jsonapi_extras.settings')
->get('path_prefix');
}
}
......@@ -3,7 +3,10 @@
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;
......@@ -13,6 +16,8 @@ use Drupal\user\Entity\User;
use Symfony\Component\Routing\Route;
/**
* The test class for the main functionality.
*
* @group jsonapi_extras
*/
class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
......@@ -26,15 +31,59 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
*/
protected function setUp() {
parent::setUp();
// Add vocabs field to the tags.
$this->createEntityReferenceField(
'taxonomy_term',
'tags',
'vocabs',
'Vocabularies',
'taxonomy_vocabulary',
'default',
[
'target_bundles' => [
'tags' => 'taxonomy_vocabulary',
],
'auto_create' => TRUE,
],
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);
$config->set('include_count', TRUE);
$config->save(TRUE);
$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']);
}
/**
* {@inheritdoc}
*
* Appends the 'application/vnd.api+json' if there's no Accept header.
*/
protected function drupalGet($path, array $options = [], array $headers = []) {
if (empty($headers['Accept']) && empty($headers['accept'])) {
$headers['Accept'] = 'application/vnd.api+json';
......@@ -51,21 +100,27 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
// Make the link for node/3 to point to an entity.
$this->nodes[3]->field_link->setValue(['uri' => 'entity:node/' . $this->nodes[2]->id()]);
$this->nodes[3]->save();
$this->nodes[40]->uid->set(0, 1);
$this->nodes[40]->save();
// 1. Make sure the api root is under '/api' and not '/jsonapi'.
/** @var \Symfony\Component\Routing\RouteCollection $route_collection */
$route_collection = \Drupal::service('router.route_provider')->getRoutesByPattern('/api');
$this->assertInstanceOf(Route::class, $route_collection->get('jsonapi.resource_list'));
$route_collection = \Drupal::service('router.route_provider')
->getRoutesByPattern('/api');
$this->assertInstanceOf(
Route::class, $route_collection->get('jsonapi.resource_list')
);
$this->drupalGet('/jsonapi');
$this->assertSession()->statusCodeEquals(404);
// 2. Make sure the count is included in collections. This also tests the overridden paths.
// 2. Make sure the count is included in collections. This also tests the
// overridden paths.
$output = Json::decode($this->drupalGet('/api/articles'));
$this->assertSame($num_articles, (int) $output['meta']['count']);
$this->assertSession()->statusCodeEquals(200);
// 3. Check disabled resources.
$this->drupalGet('/api/action/action');
$this->drupalGet('/api/taxonomy_vocabulary/taxonomy_vocabulary');
$this->assertSession()->statusCodeEquals(404);
// 4. Check renamed fields.
......@@ -73,25 +128,96 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertArrayHasKey('contentType', $output['data']['relationships']);
$this->assertSame('contentTypes', $output['data']['relationships']['contentType']['data']['type']);
$output = Json::decode($this->drupalGet('/api/contentTypes/' . $this->nodes[0]->type->entity->uuid()));
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertSame('article', $output['data']['attributes']['machineName']);
// 5. Check disabled fields.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[1]->uuid()));
$this->assertArrayNotHasKey('uuid', $output['data']['attributes']);
// 6. Test the field enhancers: DateTimeEnhancer
// 6. Test the field enhancers: DateTimeEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[2]->uuid()));
$timestamp = \DateTime::createFromFormat('Y-m-d\TH:i:sO', $output['data']['attributes']['createdAt'])
->format('U');
$this->assertSame((int) $timestamp, $this->nodes[2]->getCreatedTime());
// 7. Test the field enhancers: UuidLinkEnhancer
// 7. Test the field enhancers: UuidLinkEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[3]->uuid()));
$expected_link = 'entity:node/article/' . $this->nodes[2]->uuid();
$this->assertSame($expected_link, $output['data']['attributes']['link']['uri']);
// 8. Test the field enhancers: SingleNestedEnhancer
// 8. Test the field enhancers: SingleNestedEnhancer.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[3]->uuid()));
$this->assertInternalType('string', $output['data']['attributes']['body']);
// 9. Test the related endpoint.
// This tests the overridden resource name, the overridden field names and
// the disabled fields.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[4]->uuid() . '/contentType'));
$this->assertArrayNotHasKey('type', $output['data']['attributes']);
$this->assertSame('article', $output['data']['attributes']['machineName']);
$this->assertSame('contentTypes', $output['data']['type']);
$this->assertArrayNotHasKey('uuid', $output['data']['attributes']);
// 10. Test the relationships endpoint.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[4]->uuid() . '/relationships/contentType'));
$this->assertSame('contentTypes', $output['data']['type']);
$this->assertArrayHasKey('id', $output['data']);
// 11. Test the related endpoint on a multiple cardinality relationship.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[5]->uuid() . '/tags'));
$this->assertCount(count($this->nodes[5]->get('field_tags')->getValue()), $output['data']);
$this->assertSame('taxonomy_term--tags', $output['data'][0]['type']);
// 12. Test the relationships endpoint.
$output = Json::decode($this->drupalGet('/api/articles/' . $this->nodes[5]->uuid() . '/relationships/tags'));
$this->assertCount(count($this->nodes[5]->get('field_tags')->getValue()), $output['data']);
$this->assertArrayHasKey('id', $output['data'][0]);
// 13. Test a disabled related resource of single cardinality.
$this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/vid');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/relationships/vid');
$this->assertSession()->statusCodeEquals(404);
// 14. Test a disabled related resource of multiple cardinality.
$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->assertTrue(empty($output['data']));
$output = Json::decode($this->drupalGet('/api/taxonomy_term/tags/' . $this->tags[0]->uuid() . '/relationships/vocabs'));
$this->assertTrue(empty($output['data']));
// 15. Test included resource.
$output = Json::decode($this->drupalGet(
'/api/articles/' . $this->nodes[6]->uuid(),
['query' => ['include' => 'owner']]
));
$this->assertSame('user--user', $output['included'][0]['type']);
// 16. Test disabled included resources.
$output = Json::decode($this->drupalGet(
'/api/taxonomy_term/tags/' . $this->tags[0]->uuid(),
['query' => ['include' => 'vocabs,vid']]
));
$this->assertArrayNotHasKey('included', $output);
// 17. Test nested filters with renamed field.
$output = Json::decode($this->drupalGet(
'/api/articles',
[
'query' => [
'filter' => [
'owner.name' => [
'value' => User::load(1)->getAccountName(),
],
],
],
]
));
// There is only one article for the admin.
$this->assertSame($this->nodes[40]->uuid(), $output['data'][0]['id']);
}
/**
......@@ -107,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' => [
......@@ -137,14 +261,30 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
'auth' => [$this->user->getUsername(), $this->user->pass_raw],
'headers' => ['Content-Type' => 'application/vnd.api+json'],
]);
$created_response = Json::decode($response->getBody()->getContents());
$created_response = Json::decode((string) $response->getBody());
$this->assertEquals(201, $response->getStatusCode());
$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'];
$relationships_url = Url::fromUserInput('/api/articles/' . $uuid . '/relationships/tags');
$body = [
'data' => [
['type' => 'taxonomy_term--tags', 'id' => $this->tags[2]->uuid()]
],
];
$response = $this->request('POST', $relationships_url, [
'body' => Json::encode($body),
'auth' => [$this->user->getUsername(), $this->user->pass_raw],
'headers' => ['Content-Type' => 'application/vnd.api+json'],
]);
$created_response = Json::decode((string) $response->getBody());
$this->assertCount(3, $created_response['data']);
}
/**
......@@ -153,10 +293,10 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
protected static function overrideResources() {
// Disable the user resource.
JsonapiResourceConfig::create([
'id' => 'action--action',
'id' => 'taxonomy_vocabulary--taxonomy_vocabulary',
'disabled' => TRUE,
'path' => 'action/action',
'resourceType' => 'action--action',
'path' => 'taxonomy_vocabulary/taxonomy_vocabulary',
'resourceType' => 'taxonomy_vocabulary--taxonomy_vocabulary',
])->save();
// Override paths and fields in the articles resource.
JsonapiResourceConfig::create([
......@@ -291,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',
......@@ -324,27 +473,15 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
'path' => 'contentTypes',
'resourceType' => 'contentTypes',
'resourceFields' => [
'id' => [
'fieldName' => 'id',
'type' => [
'fieldName' => 'type',
'publicName' => 'machineName',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'label' => [
'fieldName' => 'label',
'publicName' => 'label',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
'revision' => [
'fieldName' => 'revision',
'publicName' => 'revision',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'bundle' => [
'fieldName' => 'bundle',
'publicName' => 'bundle',
'status' => [
'fieldName' => 'status',
'publicName' => 'isEnabled',
'enhancer' => ['id' => ''],
'disabled' => FALSE,
],
......@@ -354,12 +491,6 @@ class JsonExtrasApiFunctionalTest extends JsonApiFunctionalTestBase {
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'default_langcode' => [
'fieldName' => 'default_langcode',
'publicName' => 'default_langcode',
'enhancer' => ['id' => ''],
'disabled' => TRUE,
],
'uuid' => [
'fieldName' => 'uuid',
'publicName' => 'uuid',
......