Skip to content

Commit

Permalink
Merge pull request #486 from EdwinDayot/fix/custom-query-relation-fie…
Browse files Browse the repository at this point in the history
…ld-on-interface

Fixed the custom query not being handled by interface's relations
  • Loading branch information
mfn committed Nov 26, 2019
2 parents 021a202 + 72c6a5b commit a213321
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 100 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CHANGELOG
- Fix validation rules for non-null list of non-null objects [\#511 / crissi](https://github.com/rebing/graphql-laravel/pull/511/files)
- Add morph type to returned models [\#503 / crissi](https://github.com/rebing/graphql-laravel/pull/503)
- Querying same field multiple times causes an error (e.g. via fragments) [\#537 / edgarsn](https://github.com/rebing/graphql-laravel/pull/537)
- Fixed the custom query not being handled by interface's relations [\#486 / EdwinDayot](https://github.com/rebing/graphql-laravel/pull/486)
### Changed
- Switch Code Style handling from StyleCI to PHP-CS Fixer [\#502 / crissi](https://github.com/rebing/graphql-laravel/pull/502)
- Implemented [ClientAware](https://webonyx.github.io/graphql-php/error-handling/#default-error-formatting) interface on integrated exceptions [\#530 / georgeboot](https://github.com/rebing/graphql-laravel/pull/530)
Expand Down
15 changes: 15 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,21 @@ class HumanType extends GraphQLType
}
```

#### Supporting custom queries on interface relations

If an interface contains a relation with a custom query, it's required to implement `public function types()` returning an array of `GraphQL::type()`, i.e. all the possible types it may resolve to (quite similar as it works for unions) so that it works correctly with `SelectFields`.

Based on the previous code example, the method would look like:
```php
public function types(): array
{
return[
GraphQL::type('Human'),
GraphQL::type('Droid'),
];
}
```

#### Sharing Interface fields

Since you often have to repeat many of the field definitons of the Interface in the concrete types, it makes sense to share the definitions of the Interface.
Expand Down
26 changes: 23 additions & 3 deletions src/Support/InterfaceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ protected function getTypeResolver(): ?Closure
};
}

protected function getTypesResolver(): ?Closure
{
if (! method_exists($this, 'types')) {
return null;
}

$resolver = [$this, 'types'];

return function () use ($resolver): array {
$args = func_get_args();

return call_user_func_array($resolver, $args);
};
}

/**
* Get the attributes from the container.
*
Expand All @@ -34,9 +49,14 @@ public function getAttributes(): array
{
$attributes = parent::getAttributes();

$resolver = $this->getTypeResolver();
if ($resolver) {
$attributes['resolveType'] = $resolver;
$resolverType = $this->getTypeResolver();
if ($resolverType) {
$attributes['resolveType'] = $resolverType;
}

$resolverTypes = $this->getTypesResolver();
if ($resolverTypes) {
$attributes['types'] = $resolverTypes;
}

return $attributes;
Expand Down
260 changes: 183 additions & 77 deletions src/Support/SelectFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Arr;
use RuntimeException;

class SelectFields
Expand Down Expand Up @@ -114,6 +116,16 @@ public static function getSelectableFieldsAndRelations(
};
}

private static function getTableNameFromParentType(GraphqlType $parentType): ?string
{
return isset($parentType->config['model']) ? app($parentType->config['model'])->getTable() : null;
}

private static function getPrimaryKeyFromParentType(GraphqlType $parentType): ?string
{
return isset($parentType->config['model']) ? app($parentType->config['model'])->getKeyName() : null;
}

/**
* Get the selects and withs from the given fields
* and recurse if necessary.
Expand Down Expand Up @@ -159,6 +171,12 @@ protected static function handleFields(
continue;
}

$parentTypeUnwrapped = $parentType;

if ($parentTypeUnwrapped instanceof WrappingType) {
$parentTypeUnwrapped = $parentTypeUnwrapped->getWrappedType(true);
}

// First check if the field is even accessible
$canSelect = self::validateField($fieldObject, $queryArgs);
if ($canSelect === true) {
Expand All @@ -170,7 +188,10 @@ protected static function handleFields(

// Pagination
if (is_a($parentType, config('graphql.pagination_type', PaginationType::class))) {
self::handleFields($queryArgs, $field, $fieldObject->config['type']->getWrappedType(), $select, $with, $ctx);
/* @var GraphqlType $fieldType */
$fieldType = $fieldObject->config['type'];
self::handleFields($queryArgs, $field, $fieldType->getWrappedType(), $select,
$with, $ctx);
}
// With
elseif (is_array($field['fields']) && $queryable) {
Expand All @@ -180,53 +201,18 @@ protected static function handleFields(
$relationsKey = $fieldObject->config['alias'] ?? $key;
$relation = call_user_func([app($parentType->config['model']), $relationsKey]);

// Add the foreign key here, if it's a 'belongsTo'/'belongsToMany' relation
if (method_exists($relation, 'getForeignKey')) {
$foreignKey = $relation->getForeignKey();
} elseif (method_exists($relation, 'getQualifiedForeignPivotKeyName')) {
$foreignKey = $relation->getQualifiedForeignPivotKeyName();
} else {
$foreignKey = $relation->getQualifiedForeignKeyName();
}

$foreignKey = $parentTable ? ($parentTable.'.'.preg_replace('/^'.preg_quote($parentTable, '/').'\./', '', $foreignKey)) : $foreignKey;

if (is_a($relation, MorphTo::class)) {
$foreignKeyType = $relation->getMorphType();
$foreignKeyType = $parentTable ? ($parentTable.'.'.$foreignKeyType) : $foreignKeyType;

if (! in_array($foreignKey, $select)) {
$select[] = $foreignKey;
}

if (! in_array($foreignKeyType, $select)) {
$select[] = $foreignKeyType;
}
} elseif (is_a($relation, BelongsTo::class)) {
if (! in_array($foreignKey, $select)) {
$select[] = $foreignKey;
}
}
// If 'HasMany', then add it in the 'with'
elseif ((is_a($relation, HasMany::class) || is_a($relation, MorphMany::class) || is_a($relation, HasOne::class) || is_a($relation, MorphOne::class))
&& ! array_key_exists($foreignKey, $field)) {
$segments = explode('.', $foreignKey);
$foreignKey = end($segments);
if (! array_key_exists($foreignKey, $field)) {
$field['fields'][$foreignKey] = self::ALWAYS_RELATION_KEY;
}

if (is_a($relation, MorphMany::class) || is_a($relation, MorphOne::class)) {
$field['fields'][$relation->getMorphType()] = self::ALWAYS_RELATION_KEY;
}
}
self::handleRelation($select, $relation, $parentTable, $field);

// New parent type, which is the relation
$newParentType = $parentType->getField($key)->config['type'];

self::addAlwaysFields($fieldObject, $field, $parentTable, true);

$with[$relationsKey] = self::getSelectableFieldsAndRelations($queryArgs, $field, $newParentType, $customQuery, false, $ctx);
$with[$relationsKey] = self::getSelectableFieldsAndRelations($queryArgs, $field, $newParentType,
$customQuery, false, $ctx);
} elseif (is_a($parentTypeUnwrapped, \GraphQL\Type\Definition\InterfaceType::class)) {
self::handleInterfaceFields($queryArgs, $field, $parentTypeUnwrapped, $select, $with, $ctx,
$fieldObject, $key, $customQuery);
} else {
self::handleFields($queryArgs, $field, $fieldObject->config['type'], $select, $with, $ctx);
}
Expand Down Expand Up @@ -261,6 +247,40 @@ protected static function handleFields(
}
}

private static function isMongodbInstance(GraphqlType $parentType): bool
{
$mongoType = 'Jenssegers\Mongodb\Eloquent\Model';

return isset($parentType->config['model']) ? app($parentType->config['model']) instanceof $mongoType : false;
}

/**
* @param string|Expression $field
* @param array $select Passed by reference, adds further fields to select
* @param string|null $parentTable
* @param bool $forRelation
*/
protected static function addFieldToSelect($field, array &$select, ?string $parentTable, bool $forRelation): void
{
if ($field instanceof Expression) {
$select[] = $field;

return;
}

if ($forRelation && ! array_key_exists($field, $select['fields'])) {
$select['fields'][$field] = [
'args' => [],
'fields' => true,
];
} elseif (! $forRelation && ! in_array($field, $select)) {
$field = $parentTable ? ($parentTable.'.'.$field) : $field;
if (! in_array($field, $select)) {
$select[] = $field;
}
}
}

/**
* Check the privacy status, if it's given.
*
Expand Down Expand Up @@ -325,6 +345,52 @@ private static function isQueryable(array $fieldObject): bool
return ($fieldObject['is_relation'] ?? true) === true;
}

/**
* @param array $select
* @param mixed $relation
* @param string|null $parentTable
* @param array $field
*/
private static function handleRelation(array &$select, $relation, ?string $parentTable, &$field): void
{
// Add the foreign key here, if it's a 'belongsTo'/'belongsToMany' relation
if (method_exists($relation, 'getForeignKey')) {
$foreignKey = $relation->getForeignKey();
} elseif (method_exists($relation, 'getQualifiedForeignPivotKeyName')) {
$foreignKey = $relation->getQualifiedForeignPivotKeyName();
} else {
$foreignKey = $relation->getQualifiedForeignKeyName();
}
$foreignKey = $parentTable ? ($parentTable.'.'.preg_replace('/^'.preg_quote($parentTable, '/').'\./',
'', $foreignKey)) : $foreignKey;
if (is_a($relation, MorphTo::class)) {
$foreignKeyType = $relation->getMorphType();
$foreignKeyType = $parentTable ? ($parentTable.'.'.$foreignKeyType) : $foreignKeyType;
if (! in_array($foreignKey, $select)) {
$select[] = $foreignKey;
}
if (! in_array($foreignKeyType, $select)) {
$select[] = $foreignKeyType;
}
} elseif (is_a($relation, BelongsTo::class)) {
if (! in_array($foreignKey, $select)) {
$select[] = $foreignKey;
}
} // If 'HasMany', then add it in the 'with'
elseif ((is_a($relation, HasMany::class) || is_a($relation, MorphMany::class) || is_a($relation,
HasOne::class) || is_a($relation, MorphOne::class))
&& ! array_key_exists($foreignKey, $field)) {
$segments = explode('.', $foreignKey);
$foreignKey = end($segments);
if (! array_key_exists($foreignKey, $field)) {
$field['fields'][$foreignKey] = self::ALWAYS_RELATION_KEY;
}
if (is_a($relation, MorphMany::class) || is_a($relation, MorphOne::class)) {
$field['fields'][$relation->getMorphType()] = self::ALWAYS_RELATION_KEY;
}
}
}

/**
* Add selects that are given by the 'always' attribute.
*
Expand Down Expand Up @@ -354,47 +420,87 @@ protected static function addAlwaysFields(
}

/**
* @param string|Expression $field
* @param array $select Passed by reference, adds further fields to select
* @param string|null $parentTable
* @param bool $forRelation
* @param array $queryArgs
* @param array $field
* @param GraphqlType $parentType
* @param array $select
* @param array $with
* @param mixed $ctx
* @param FieldDefinition $fieldObject
* @param string $key
* @param Closure|null $customQuery
*/
protected static function addFieldToSelect($field, array &$select, ?string $parentTable, bool $forRelation): void
{
if ($field instanceof Expression) {
$select[] = $field;

return;
}

if ($forRelation && ! array_key_exists($field, $select['fields'])) {
$select['fields'][$field] = [
'args' => [],
'fields' => true,
];
} elseif (! $forRelation && ! in_array($field, $select)) {
$field = $parentTable ? ($parentTable.'.'.$field) : $field;
if (! in_array($field, $select)) {
$select[] = $field;
protected static function handleInterfaceFields(
array $queryArgs,
array $field,
GraphqlType $parentType,
array &$select,
array &$with,
$ctx,
FieldDefinition $fieldObject,
string $key,
?Closure $customQuery
) {
$relationsKey = Arr::get($fieldObject->config, 'alias', $key);

$with[$relationsKey] = function ($query) use (
$queryArgs,
$field,
$parentType,
&$select,
$ctx,
$customQuery,
$key,
$fieldObject
) {
$parentTable = self::isMongodbInstance($parentType) ? null : self::getTableNameFromParentType($parentType);

self::handleRelation($select, $query, $parentTable, $field);

// New parent type, which is the relation
try {
if (method_exists($parentType, 'getField')) {
$newParentType = $parentType->getField($key)->config['type'];
$customQuery = $parentType->getField($key)->config['query'] ?? $customQuery;
} else {
return $query;
}
} catch (InvariantViolation $e) {
return $query;
}
}
}

private static function getPrimaryKeyFromParentType(GraphqlType $parentType): ?string
{
return isset($parentType->config['model']) ? app($parentType->config['model'])->getKeyName() : null;
}
self::addAlwaysFields($fieldObject, $field, $parentTable, true);

// Find the type of the current relation by comparing table names
if (isset($parentType->config['types'])) {
$typesFiltered = array_filter(
$parentType->config['types'](),
function (GraphqlType $type) use ($query) {
/* @var Relation $query */
return app($type->config['model'])->getTable() === $query->getParent()->getTable();
});
$typesFiltered = array_values($typesFiltered ?? []);

if (count($typesFiltered) === 1) {
/* @var GraphqlType $type */
$type = $typesFiltered[0];
$relationField = $type->getField($key);
$newParentType = $relationField->config['type'];
// If a custom query is available on the selected type, it should replace the interface's one
$customQuery = $relationField->config['query'] ?? $customQuery;
}
}

private static function getTableNameFromParentType(GraphqlType $parentType): ?string
{
return isset($parentType->config['model']) ? app($parentType->config['model'])->getTable() : null;
}
if ($newParentType instanceof WrappingType) {
$newParentType = $newParentType->getWrappedType(true);
}

private static function isMongodbInstance(GraphqlType $parentType): bool
{
$mongoType = 'Jenssegers\Mongodb\Eloquent\Model';
/** @var callable $callable */
$callable = self::getSelectableFieldsAndRelations($queryArgs, $field, $newParentType, $customQuery,
false, $ctx);

return isset($parentType->config['model']) ? app($parentType->config['model']) instanceof $mongoType : false;
return $callable($query);
};
}

public function getSelect(): array
Expand Down
Loading

0 comments on commit a213321

Please sign in to comment.