Skip to content

Commit

Permalink
Merge pull request #14366 from craftcms/feature/nested-entry-gql
Browse files Browse the repository at this point in the history
Nested entry GraphQL support
  • Loading branch information
brandonkelly authored Feb 13, 2024
2 parents a3419ea + 9d1f60b commit 3554168
Show file tree
Hide file tree
Showing 16 changed files with 468 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
- User queries now have an `authorOf` param.
- Nested addresses are now cached by their field ID, and address queries now register cache tags based on their `field` and `fieldId` params.
- Nested entries are now cached by their field ID, and entry queries now register cache tags based on their `field` and `fieldId` params.
- GraphQL schemas can now include queries and mutations for nested entries (e.g. within Matrix or CKEditor fields) directly. ([#14366](https://github.com/craftcms/cms/pull/14366))
- Added the `fieldId`, `fieldHandle`, `ownerId`, and `sortOrder` entry GraphQL fields. ([#14366](https://github.com/craftcms/cms/pull/14366))
- Entries’ GraphQL type names are now formatted as `<entryTypeHandle>_Entry`, and are no longer prefixed with their section’s handle. (That goes for Matrix-nested entries as well.)
- Entries now have `author` and `authorIds` GraphQL field.
- Matrix fields’ GraphQL mutation types now expect nested entries to be defined by an `entries` field rather than `blocks`.
Expand Down Expand Up @@ -359,7 +361,9 @@
- Added `craft\services\Entries::refreshEntryTypes()`.
- Added `craft\services\Entries::saveSection()`.
- Added `craft\services\Fields::$fieldContext`, which replaces `craft\services\Content::$fieldContext`.
- Added `craft\services\Fields::EVENT_REGISTER_NESTED_ENTRY_FIELD_TYPES`.
- Added `craft\services\Fields::getAllLayouts()`.
- Added `craft\services\Fields::getNestedEntryFieldTypes()`.
- Added `craft\services\Gql::defineContentArgumentsForFieldLayouts()`.
- Added `craft\services\Gql::defineContentArgumentsForFields()`.
- Added `craft\services\Gql::getOrSetContentArguments()`.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
- Restored the “Preview” and “Edit Image” buttons on asset image previews. ([#14333](https://github.com/craftcms/cms/discussions/14333))
- Improved the accessibility of the global sidebar. ([#14335](https://github.com/craftcms/cms/pull/14335))
- Improved the contrast of the Dev Mode caution tape indicator. ([#14336](https://github.com/craftcms/cms/pull/14336))
- GraphQL schemas can now include queries and mutations for nested entries (e.g. within Matrix or CKEditor fields) directly. ([#14366](https://github.com/craftcms/cms/pull/14366))
- Added the `fieldId`, `fieldHandle`, `ownerId`, and `sortOrder` entry GraphQL fields. ([#14366](https://github.com/craftcms/cms/pull/14366))
- Nested addresses are now cached by their field ID, and address queries now register cache tags based on their `field` and `fieldId` params.
- Nested entries are now cached by their field ID, and entry queries now register cache tags based on their `field` and `fieldId` params.
- Added `craft\services\Fields::EVENT_REGISTER_NESTED_ENTRY_FIELD_TYPES`.
- Added `craft\services\Fields::getNestedEntryFieldTypes()`.
- `craft\behaviors\SessionBehavior::setSuccess()` and `getSuccess()` use the `success` flash key now, rather than `notice`. ([#14345](https://github.com/craftcms/cms/pull/14345))
- Exception response data no longer includes an `error` key with the exception message. `message` should be used instead. ([#14346](https://github.com/craftcms/cms/pull/14346))
- Fixed an error that occurred when adding a new block to a Matrix field with an overridden handle. ([#14350](https://github.com/craftcms/cms/issues/14350))
Expand Down
20 changes: 20 additions & 0 deletions src/gql/arguments/elements/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ public static function getArguments(): array
'type' => Type::listOf(QueryArgument::getType()),
'description' => 'Narrows the query results based on the sections the entries belong to, per the sections’ IDs.',
],
'field' => [
'name' => 'field',
'type' => Type::listOf(Type::string()),
'description' => 'Narrows the query results based on the field the entries are contained by.',
],
'fieldId' => [
'name' => 'fieldId',
'type' => Type::listOf(QueryArgument::getType()),
'description' => 'Narrows the query results based on the field the entries are contained by, per the fields’ IDs.',
],
'primaryOwnerId' => [
'name' => 'primaryOwnerId',
'type' => Type::listOf(QueryArgument::getType()),
'description' => 'Narrows the query results based on the primary owner element of the entries, per the owners’ IDs.',
],
'ownerId' => [
'name' => 'ownerId',
'type' => Type::listOf(QueryArgument::getType()),
'description' => 'Narrows the query results based on the owner element of the entries, per the owners’ IDs.',
],
'type' => [
'name' => 'type',
'type' => Type::listOf(Type::string()),
Expand Down
38 changes: 38 additions & 0 deletions src/gql/arguments/mutations/NestedEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\gql\arguments\mutations;

use GraphQL\Type\Definition\Type;

/**
* Class NestedEntry
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.0.0
*/
class NestedEntry extends Entry
{
/**
* @inheritdoc
*/
public static function getArguments(): array
{
return array_merge(parent::getArguments(), [
'ownerId' => [
'name' => 'ownerId',
'type' => Type::id(),
'description' => 'The entry’s owner ID.',
],
'sortOrder' => [
'name' => 'sortOrder',
'type' => Type::int(),
'description' => 'The entry’s sort order.',
],
]);
}
}
25 changes: 23 additions & 2 deletions src/gql/interfaces/elements/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,36 @@ public static function getFieldDefinitions(): array
],
'sectionId' => [
'name' => 'sectionId',
'type' => Type::nonNull(Type::int()),
'type' => Type::int(),
'description' => 'The ID of the section that contains the entry.',
],
'sectionHandle' => [
'name' => 'sectionHandle',
'type' => Type::nonNull(Type::string()),
'type' => Type::string(),
'description' => 'The handle of the section that contains the entry.',
'complexity' => Gql::singleQueryComplexity(),
],
'fieldId' => [
'name' => 'fieldId',
'type' => Type::int(),
'description' => 'The ID of the field that contains the entry.',
],
'fieldHandle' => [
'name' => 'fieldHandle',
'type' => Type::string(),
'description' => 'The handle of the field that contains the entry.',
'complexity' => Gql::singleQueryComplexity(),
],
'ownerId' => [
'name' => 'ownerId',
'type' => Type::int(),
'description' => 'The ID of the entry’s owner elementt.',
],
'sortOrder' => [
'name' => 'sortOrder',
'type' => Type::int(),
'description' => 'The entry’s position within the field that contains it.',
],
'typeId' => [
'name' => 'typeId',
'type' => Type::nonNull(Type::int()),
Expand Down
87 changes: 87 additions & 0 deletions src/gql/mutations/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
namespace craft\gql\mutations;

use Craft;
use craft\base\ElementContainerFieldInterface;
use craft\gql\arguments\mutations\Draft as DraftMutationArguments;
use craft\gql\arguments\mutations\Entry as EntryMutationArguments;
use craft\gql\arguments\mutations\NestedEntry;
use craft\gql\arguments\mutations\Structure as StructureArguments;
use craft\gql\base\ElementMutationResolver;
use craft\gql\base\Mutation;
Expand Down Expand Up @@ -62,6 +64,35 @@ public static function getMutations(): array
}
}

$fieldsService = Craft::$app->getFields();
foreach ($fieldsService->getNestedEntryFieldTypes() as $type) {
foreach ($fieldsService->getFieldsByType($type) as $field) {
/** @var ElementContainerFieldInterface $field */
$scope = "nestedentryfields.$field->uid";
$canCreate = Gql::canSchema($scope, 'create');
$canSave = Gql::canSchema($scope, 'save');

if ($canCreate || $canSave) {
// Create a mutation for each entry type
foreach ($field->getFieldLayoutProviders() as $provider) {
if ($provider instanceof EntryTypeModel) {
foreach (static::createSaveMutationsForField($field, $provider, $canSave) as $mutation) {
$mutationList[$mutation['name']] = $mutation;
}
}
}
}

if (!$createDraftMutations && $canSave) {
$createDraftMutations = true;
}

if (!$createDeleteMutation && Gql::canSchema($scope, 'delete')) {
$createDeleteMutation = true;
}
}
}

if ($createDeleteMutation || $createDraftMutations) {
$resolver = Craft::createObject(EntryMutationResolver::class);

Expand Down Expand Up @@ -205,4 +236,60 @@ public static function createSaveMutations(

return $mutations;
}

/**
* Create the per-entry-type save mutations for a nested entry field.
*
* @param ElementContainerFieldInterface $field
* @param EntryTypeModel $entryType
* @param bool $createSaveDraftMutation
* @return array
* @throws InvalidConfigException
*/
public static function createSaveMutationsForField(
ElementContainerFieldInterface $field,
EntryTypeModel $entryType,
bool $createSaveDraftMutation,
): array {
$mutations = [];

$entryMutationArguments = NestedEntry::getArguments();
$draftMutationArguments = DraftMutationArguments::getArguments();
$generatedType = EntryType::generateType($entryType);

/** @var EntryMutationResolver $resolver */
$resolver = Craft::createObject(EntryMutationResolver::class);
$resolver->setResolutionData('entryType', $entryType);
$resolver->setResolutionData('field', $field);

static::prepareResolver($resolver, $entryType->getCustomFields());

$description = sprintf('Save a “%s” entry in the “%s” %s field.', $entryType->name, $field->name, $field::displayName());
$draftDescription = sprintf('Save a “%s” entry draft in the “%s” %s field.', $entryType->name, $field->name, $field::displayName());

$contentFields = $resolver->getResolutionData(ElementMutationResolver::CONTENT_FIELD_KEY);
$entryMutationArguments = array_merge($entryMutationArguments, $contentFields);
$draftMutationArguments = array_merge($draftMutationArguments, $contentFields);

$mutations[] = [
'name' => "save_{$field->handle}Field_{$entryType->handle}_Entry",
'description' => $description,
'args' => $entryMutationArguments,
'resolve' => [$resolver, 'saveEntry'],
'type' => $generatedType,
];

// This gets created only if allowed to save entries
if ($createSaveDraftMutation) {
$mutations[] = [
'name' => "save_{$field->handle}_{$entryType->handle}_Draft",
'description' => $draftDescription,
'args' => $draftMutationArguments,
'resolve' => [$resolver, 'saveEntry'],
'type' => $generatedType,
];
}

return $mutations;
}
}
88 changes: 77 additions & 11 deletions src/gql/queries/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use craft\gql\GqlEntityRegistry;
use craft\gql\interfaces\elements\Entry as EntryInterface;
use craft\gql\resolvers\elements\Entry as EntryResolver;
use craft\gql\types\elements\Entry as EntryGqlType;
use craft\gql\types\generators\EntryType as EntryTypeGenerator;
use craft\helpers\ArrayHelper;
use craft\helpers\Gql as GqlHelper;
Expand All @@ -37,6 +38,15 @@ public static function getQueries(bool $checkToken = true): array
return [];
}

/** @var EntryGqlType[] $entryTypeGqlTypes */
$entryTypeGqlTypes = array_map(
fn(EntryType $entryType) => EntryTypeGenerator::generateType($entryType),
ArrayHelper::index(
GqlHelper::getSchemaContainedEntryTypes(),
fn(EntryType $entryType) => $entryType->id
),
);

return [
'entries' => [
'type' => Type::listOf(EntryInterface::getType()),
Expand All @@ -59,26 +69,20 @@ public static function getQueries(bool $checkToken = true): array
'description' => 'This query is used to query for a single entry.',
'complexity' => GqlHelper::singleQueryComplexity(),
],
...self::getSectionLevelFields(),
...self::sectionLevelFields($entryTypeGqlTypes),
...self::nestedEntryFieldLevelFields($entryTypeGqlTypes),
];
}

/**
* Return the query fields for section level queries.
*
* @param EntryGqlType[] $entryTypeGqlTypes
* @return array
* @throws InvalidConfigException
*/
protected static function getSectionLevelFields(): array
private static function sectionLevelFields(array $entryTypeGqlTypes): array
{
$entryTypeGqlTypes = array_map(
fn(EntryType $entryType) => EntryTypeGenerator::generateType($entryType),
ArrayHelper::index(
GqlHelper::getSchemaContainedEntryTypes(),
fn(EntryType $entryType) => $entryType->id
),
);

$gqlTypes = [];

foreach (GqlHelper::getSchemaContainedSections() as $section) {
Expand All @@ -101,7 +105,13 @@ protected static function getSectionLevelFields(): array

// Unset unusable arguments
$arguments = EntryArguments::getArguments();
unset($arguments['section'], $arguments['sectionId']);
unset(
$arguments['section'],
$arguments['sectionId'],
$arguments['field'],
$arguments['fieldId'],
$arguments['ownerId'],
);

// Create the section query field
$sectionQueryType = [
Expand All @@ -120,4 +130,60 @@ protected static function getSectionLevelFields(): array

return $gqlTypes;
}

/**
* Return the query fields for nested entry field queries.
*
* @param EntryGqlType[] $entryTypeGqlTypes
* @return array
* @throws InvalidConfigException
*/
private static function nestedEntryFieldLevelFields(array $entryTypeGqlTypes): array
{
$gqlTypes = [];

foreach (GqlHelper::getSchemaContainedNestedEntryFields() as $field) {
$typeName = "{$field->handle}NestedEntriesQuery";
$fieldQueryType = GqlEntityRegistry::getEntity($typeName);

if (!$fieldQueryType) {
$entryTypesInField = [];

// Loop through the entry types and create further queries
foreach ($field->getFieldLayoutProviders() as $provider) {
if ($provider instanceof EntryType && isset($entryTypeGqlTypes[$provider->id])) {
$entryTypesInField[] = $entryTypeGqlTypes[$provider->id];
}
}

if (empty($entryTypesInField)) {
continue;
}

// Unset unusable arguments
$arguments = EntryArguments::getArguments();
unset(
$arguments['section'],
$arguments['sectionId'],
$arguments['field'],
$arguments['fieldId'],
);

// Create the query field
$fieldQueryType = [
'name' => "{$field->handle}FieldEntries",
'args' => $arguments,
'description' => sprintf('Entries within the “%s” %s field.', $field->name, $field::displayName()),
'type' => Type::listOf(GqlHelper::getUnionType("{$field->handle}FieldEntryUnion", $entryTypesInField)),
// Enforce the section argument and set the source to `null`, to enforce a new element query.
'resolve' => fn($source, array $arguments, $context, ResolveInfo $resolveInfo) =>
EntryResolver::resolve(null, $arguments + ['field' => $field->handle], $context, $resolveInfo),
];
}

$gqlTypes[$field->handle] = $fieldQueryType;
}

return $gqlTypes;
}
}
Loading

0 comments on commit 3554168

Please sign in to comment.