This commit is contained in:
Kilian Hofmann
2021-06-01 19:55:55 +02:00
parent 052cbe3038
commit d8c489c714
328 changed files with 36645 additions and 0 deletions
@@ -0,0 +1,224 @@
<?php
namespace Nuwave\Lighthouse\WhereConstraints;
use GraphQL\Error\Error;
use Illuminate\Support\Str;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
class WhereConstraintsDirective extends BaseDirective implements ArgBuilderDirective, ArgManipulator, DefinedDirective
{
const NAME = 'whereConstraints';
const INVALID_COLUMN_MESSAGE = 'Column names may contain only alphanumerics or underscores, and may not begin with a digit.';
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return self::NAME;
}
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
"""
Add a dynamically client-controlled WHERE constraint to a fields query.
The argument it is defined on may have any name but **must** be
of the input type `WhereConstraints`.
"""
directive @whereConstraints(
"""
Restrict the allowed column names to a well-defined list.
This improves introspection capabilities and security.
"""
columns: [String!]
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
SDL;
}
/**
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $whereConstraints
* @param bool $nestedOr
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $whereConstraints, bool $nestedOr = false)
{
if ($andConnectedConstraints = $whereConstraints['AND'] ?? null) {
$builder->whereNested(
function ($builder) use ($andConnectedConstraints): void {
foreach ($andConnectedConstraints as $constraint) {
$this->handleBuilder($builder, $constraint);
}
}
);
}
if ($orConnectedConstraints = $whereConstraints['OR'] ?? null) {
$builder->whereNested(
function ($builder) use ($orConnectedConstraints): void {
foreach ($orConnectedConstraints as $constraint) {
$this->handleBuilder($builder, $constraint, true);
}
}
);
}
if ($notConnectedConstraints = $whereConstraints['NOT'] ?? null) {
$builder->whereNested(
function ($builder) use ($notConnectedConstraints): void {
foreach ($notConnectedConstraints as $constraint) {
$this->handleBuilder($builder, $constraint);
}
},
'not'
);
}
if ($column = $whereConstraints['column'] ?? null) {
if (! array_key_exists('value', $whereConstraints)) {
throw new Error(
self::missingValueForColumn($column)
);
}
if (! \Safe\preg_match('/^(?![0-9])[A-Za-z0-9_-]*$/', $column)) {
throw new Error(
self::INVALID_COLUMN_MESSAGE
);
}
$where = $nestedOr
? 'orWhere'
: 'where';
$builder->{$where}(
$column,
$whereConstraints['operator'],
$whereConstraints['value']
);
}
return $builder;
}
public static function missingValueForColumn(string $column): string
{
return "Did not receive a value to match the WhereConstraints for column {$column}.";
}
/**
* Manipulate the AST.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @return \Nuwave\Lighthouse\Schema\AST\DocumentAST
*/
public function manipulateArgDefinition(DocumentAST &$documentAST, InputValueDefinitionNode &$argDefinition, FieldDefinitionNode &$parentField, ObjectTypeDefinitionNode &$parentType)
{
$allowedColumns = $this->directiveArgValue('columns');
if (! $allowedColumns) {
return $documentAST;
}
$restrictedWhereConstraintsName = $this->restrictedWhereConstraintsName($argDefinition, $parentField);
$argDefinition->type = PartialParser::namedType($restrictedWhereConstraintsName);
$allowedColumnsEnumName = $this->allowedColumnsEnumName($argDefinition, $parentField);
return $documentAST
->setTypeDefinition(
WhereConstraintsServiceProvider::createWhereConstraintsInputType(
$restrictedWhereConstraintsName,
"Dynamic WHERE constraints for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.",
$allowedColumnsEnumName
)
)
->setTypeDefinition(
$this->createAllowedColumnsEnum($argDefinition, $parentField, $allowedColumns, $allowedColumnsEnumName)
);
}
/**
* Create the name for the Enum that holds the allowed columns.
*
* @example FieldNameArgNameColumn
*
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @return string
*/
protected function allowedColumnsEnumName(InputValueDefinitionNode &$argDefinition, FieldDefinitionNode &$parentField): string
{
return Str::studly($parentField->name->value)
.Str::studly($argDefinition->name->value)
.'Column';
}
/**
* Create the name for the restricted WhereConstraints input.
*
* @example FieldNameArgNameWhereConstraints
*
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @return string
*/
protected function restrictedWhereConstraintsName(InputValueDefinitionNode &$argDefinition, FieldDefinitionNode &$parentField): string
{
return Str::studly($parentField->name->value)
.Str::studly($argDefinition->name->value)
.'WhereConstraints';
}
/**
* Create the Enum that holds the allowed columns.
*
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @param string[] $allowedColumns
* @param string $allowedColumnsEnumName
* @return \GraphQL\Language\AST\EnumTypeDefinitionNode
*/
public function createAllowedColumnsEnum(
InputValueDefinitionNode &$argDefinition,
FieldDefinitionNode &$parentField,
array $allowedColumns,
string $allowedColumnsEnumName
): EnumTypeDefinitionNode {
$enumValues = array_map(
function (string $columnName): string {
return
strtoupper(
Str::snake($columnName)
)
.' @enum(value: "'.$columnName.'")';
},
$allowedColumns
);
$enumDefinition = "\"Allowed column names for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.\"\n"
."enum $allowedColumnsEnumName {\n";
foreach ($enumValues as $enumValue) {
$enumDefinition .= "$enumValue\n";
}
$enumDefinition .= '}';
return PartialParser::enumTypeDefinition($enumDefinition);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Nuwave\Lighthouse\WhereConstraints;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
class WhereConstraintsServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @param \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory $directiveFactory
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function boot(DirectiveFactory $directiveFactory, Dispatcher $dispatcher): void
{
$directiveFactory->addResolved(
WhereConstraintsDirective::NAME,
WhereConstraintsDirective::class
);
$dispatcher->listen(
ManipulateAST::class,
function (ManipulateAST $manipulateAST): void {
$manipulateAST->documentAST
->setTypeDefinition(
static::createWhereConstraintsInputType(
'WhereConstraints',
'Dynamic WHERE constraints for queries.',
'String'
)
)
->setTypeDefinition(
PartialParser::scalarTypeDefinition('
scalar Mixed @scalar(class: "MLL\\\GraphQLScalars\\\Mixed")
')
);
}
);
}
public static function createWhereConstraintsInputType(string $name, string $description, string $columnType): InputObjectTypeDefinitionNode
{
return PartialParser::inputObjectTypeDefinition("
input $name {
column: $columnType
operator: Operator = EQ
value: Mixed
AND: [$name!]
OR: [$name!]
NOT: [$name!]
}
");
}
}