Skip to content

Commit

Permalink
Nested entry GraphQL support
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonkelly committed Feb 13, 2024
1 parent a3419ea commit d4fc4f2
Show file tree
Hide file tree
Showing 13 changed files with 445 additions and 34 deletions.
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;
}
}
30 changes: 23 additions & 7 deletions src/gql/resolvers/elements/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,36 @@ public static function prepareQuery(mixed $source, array $arguments, ?string $fi
$query = EntryElement::find();
$pairs = GqlHelper::extractAllowedEntitiesFromSchema('read');

if (!isset($pairs['sections'])) {
if (!isset($pairs['sections']) && !isset($pairs['nestedentryfields'])) {
return ElementCollection::empty();
}

$sectionUids = array_flip($pairs['sections']);
$sectionIds = [];
$condition = ['or'];

foreach (Craft::$app->getEntries()->getAllSections() as $section) {
if (isset($sectionUids[$section->uid])) {
$sectionIds[] = $section->id;
if (isset($pairs['sections'])) {
$entriesService = Craft::$app->getEntries();
$sectionIds = array_filter(array_map(
fn(string $uid) => $entriesService->getSectionByUid($uid)?->id,
$pairs['sections'],
));
if (!empty($sectionIds)) {
$condition[] = ['in', 'entries.sectionId', $sectionIds];
}
}

$query->andWhere(['in', 'entries.sectionId', $sectionIds]);
if (isset($pairs['nestedentryfields'])) {
$fieldsService = Craft::$app->getFields();
$types = array_flip($fieldsService->getNestedEntryFieldTypes());
$fieldIds = array_filter(array_map(function(string $uid) use ($fieldsService, $types) {
$field = $fieldsService->getFieldByUid($uid);
return $field && isset($types[$field::class]) ? $field->id : null;
}, $pairs['nestedentryfields']));
if (!empty($fieldIds)) {
$condition[] = ['in', 'entries.fieldId', $fieldIds];
}
}

$query->andWhere($condition);

// If not, get the prepared element query
} else {
Expand Down
Loading

0 comments on commit d4fc4f2

Please sign in to comment.