From 86fe284a31363c8276964dc315d6620fac20ac01 Mon Sep 17 00:00:00 2001 From: spawnia Date: Tue, 2 Mar 2021 23:37:36 +0100 Subject: [PATCH 01/10] Allow constraining allowed relations in @whereConditions --- .../WhereConditionsBaseDirective.php | 27 ++++++++++--------- .../WhereConditionsServiceProvider.php | 7 ++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/WhereConditions/WhereConditionsBaseDirective.php b/src/WhereConditions/WhereConditionsBaseDirective.php index 174df2e3dc..93ebadded0 100644 --- a/src/WhereConditions/WhereConditionsBaseDirective.php +++ b/src/WhereConditions/WhereConditionsBaseDirective.php @@ -140,20 +140,21 @@ public function manipulateArgDefinition( $argDefinition->type = Parser::namedType($restrictedWhereConditionsName); $allowedColumnsEnumName = $this->generateColumnsEnum($documentAST, $argDefinition, $parentField, $parentType); - $documentAST - ->setTypeDefinition( - WhereConditionsServiceProvider::createWhereConditionsInputType( - $restrictedWhereConditionsName, - "Dynamic WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", - $allowedColumnsEnumName - ) + $documentAST->setTypeDefinition( + WhereConditionsServiceProvider::createWhereConditionsInputType( + $restrictedWhereConditionsName, + "Dynamic WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", + $allowedColumnsEnumName ) - ->setTypeDefinition( - WhereConditionsServiceProvider::createHasConditionsInputType( - $restrictedWhereConditionsName, - "Dynamic HAS conditions for WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`." - ) - ); + ); + + $documentAST->setTypeDefinition( + WhereConditionsServiceProvider::createHasConditionsInputType( + $restrictedWhereConditionsName, + "Dynamic HAS conditions for WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", + $this->directiveArgValue('relations') + ) + ); } else { $argDefinition->type = Parser::namedType(WhereConditionsServiceProvider::DEFAULT_WHERE_CONDITIONS); } diff --git a/src/WhereConditions/WhereConditionsServiceProvider.php b/src/WhereConditions/WhereConditionsServiceProvider.php index 5a17fa7a41..97625bdde7 100644 --- a/src/WhereConditions/WhereConditionsServiceProvider.php +++ b/src/WhereConditions/WhereConditionsServiceProvider.php @@ -105,8 +105,13 @@ public static function createWhereConditionsInputType(string $name, string $desc ); } - public static function createHasConditionsInputType(string $name, string $description): InputObjectTypeDefinitionNode + /** + * @param array $relations + */ + public static function createHasConditionsInputType(string $name, string $description, array $relations): InputObjectTypeDefinitionNode { + // TODO turn $relations into scalar, perhaps like + // scalar {$name}Relation @scalar(regex: "^{implode('|', $relations)}$") $hasRelationInputName = $name.self::DEFAULT_WHERE_RELATION_CONDITIONS; $defaultHasAmount = self::DEFAULT_HAS_AMOUNT; From caff9d7eff51ee5897b834b047bb5ff10d5b26a5 Mon Sep 17 00:00:00 2001 From: spawnia Date: Tue, 2 Mar 2021 23:38:01 +0100 Subject: [PATCH 02/10] Allow constraining allowed relations in @whereConditions --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6043b1abb3..f46e718ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ You can find and compare releases at the [GitHub release page](https://github.co - Add `GraphQLContext` to `StartExecution` event - Add `connect` and `disconnect` operations in nested mutations for HasMany and MorphMany relations https://github.com/nuwave/lighthouse/pull/1730 +- Allow constraining allowed relations in `@whereConditions` and `@whereHasConditions` ### Changed From f1f9518d7d820ddfaa6dc0cc61eb52896c62be4e Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 30 Dec 2021 13:16:37 +0100 Subject: [PATCH 03/10] clean up tests --- .../WhereConditionsDirectiveTest.php | 304 +++++++++++++++--- 1 file changed, 260 insertions(+), 44 deletions(-) diff --git a/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php b/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php index 053d54e7d1..996f904668 100644 --- a/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php +++ b/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php @@ -15,37 +15,6 @@ class WhereConditionsDirectiveTest extends DBTestCase { - protected $schema = /** @lang GraphQL */ ' - type User { - id: ID! - name: String - email: String - } - - type Post { - id: ID! - title: String - body: String - parent: Post @belongsTo - } - - type Query { - posts(where: _ @whereConditions): [Post!]! @all - users(where: _ @whereConditions): [User!]! @all - whitelistedColumns( - where: _ @whereConditions(columns: ["id", "camelCase"]) - ): [User!]! @all - enumColumns( - where: _ @whereConditions(columnsEnum: "UserColumn") - ): [User!]! @all - } - - enum UserColumn { - ID @enum(value: "id") - NAME @enum(value: "name") - } - '; - protected function getPackageProviders($app): array { return array_merge( @@ -56,6 +25,16 @@ protected function getPackageProviders($app): array public function testDefaultsToWhereEqual(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 2)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -74,6 +53,16 @@ public function testDefaultsToWhereEqual(): void public function testOverwritesTheOperator(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 3)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -93,6 +82,16 @@ public function testOverwritesTheOperator(): void public function testOperatorIn(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -123,6 +122,16 @@ public function testOperatorIn(): void public function testOperatorIsNull(): void { + $this->schema = /** @lang GraphQL */' + type Post { + id: ID! + } + + type Query { + posts(where: _ @whereConditions): [Post!]! @all + } + '; + factory(Post::class)->create([ 'body' => null, ]); @@ -154,6 +163,16 @@ public function testOperatorIsNull(): void public function testOperatorNotNull(): void { + $this->schema = /** @lang GraphQL */' + type Post { + id: ID! + } + + type Query { + posts(where: _ @whereConditions): [Post!]! @all + } + '; + factory(Post::class)->create([ 'body' => null, ]); @@ -185,6 +204,16 @@ public function testOperatorNotNull(): void public function testOperatorNotBetween(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -215,6 +244,16 @@ public function testOperatorNotBetween(): void public function testAddsNestedAnd(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 3)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -243,6 +282,16 @@ public function testAddsNestedAnd(): void public function testAddsNestedOr(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -292,6 +341,16 @@ public function testAddsNestedOr(): void public function testAddsNestedAndOr(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -339,6 +398,16 @@ public function testAddsNestedAndOr(): void public function testHasMixed(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 9)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -423,6 +492,16 @@ public function testHasMixed(): void public function testHasRelation(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -467,6 +546,16 @@ public function testHasRelation(): void public function testHasAmount(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -513,6 +602,16 @@ public function testHasAmount(): void public function testHasOperator(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -560,6 +659,16 @@ public function testHasOperator(): void public function testHasCondition(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -605,6 +714,16 @@ public function testHasCondition(): void public function testHasRecursive(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 7)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -651,6 +770,16 @@ public function testHasRecursive(): void public function testHasNested(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 5)->create()->each(function ($user) { $user->posts()->saveMany(factory(Post::class, 2)->create()); }); @@ -707,6 +836,16 @@ public function testHasNested(): void public function testRejectsInvalidColumnName(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + $this->graphQL(/** @lang GraphQL */ ' { users( @@ -727,11 +866,27 @@ public function testRejectsInvalidColumnName(): void public function testQueriesEmptyStrings(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 3)->create(); - $userNamedEmptyString = factory(User::class)->create([ - 'name' => '', - ]); + /** @var \Tests\Utils\Models\User $userNamedEmptyString */ + $userNamedEmptyString = factory(User::class)->make(); + $userNamedEmptyString->name = ''; + $userNamedEmptyString->save(); + + /** @var \Tests\Utils\Models\User $userNamedNull */ + $userNamedNull = factory(User::class)->make(); + $userNamedNull->name = null; + $userNamedNull->save(); $this->graphQL(/** @lang GraphQL */ ' { @@ -742,7 +897,6 @@ public function testQueriesEmptyStrings(): void } ) { id - name } } ')->assertJson([ @@ -750,7 +904,6 @@ public function testQueriesEmptyStrings(): void 'users' => [ [ 'id' => $userNamedEmptyString->id, - 'name' => $userNamedEmptyString->name, ], ], ], @@ -759,11 +912,27 @@ public function testQueriesEmptyStrings(): void public function testQueryForNull(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + factory(User::class, 3)->create(); - $userNamedNull = factory(User::class)->create([ - 'name' => null, - ]); + /** @var \Tests\Utils\Models\User $userNamedEmptyString */ + $userNamedEmptyString = factory(User::class)->make(); + $userNamedEmptyString->name = ''; + $userNamedEmptyString->save(); + + /** @var \Tests\Utils\Models\User $userNamedNull */ + $userNamedNull = factory(User::class)->make(); + $userNamedNull->name = null; + $userNamedNull->save(); $this->graphQL(/** @lang GraphQL */ ' { @@ -774,7 +943,6 @@ public function testQueryForNull(): void } ) { id - name } } ')->assertJson([ @@ -782,7 +950,6 @@ public function testQueryForNull(): void 'users' => [ [ 'id' => $userNamedNull->id, - 'name' => $userNamedNull->name, ], ], ], @@ -791,6 +958,16 @@ public function testQueryForNull(): void public function testRequiresAValueForAColumn(): void { + $this->schema = /** @lang GraphQL */' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + $this->graphQL(/** @lang GraphQL */ ' { users( @@ -806,6 +983,18 @@ public function testRequiresAValueForAColumn(): void public function testOnlyAllowsWhitelistedColumns(): void { + $this->schema = /** @lang GraphQL */ ' + type User { + id: ID! + } + + type Query { + whitelistedColumns( + where: _ @whereConditions(columns: ["id", "camelCase"]) + ): [User!]! @all + } + '; + factory(User::class)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -866,6 +1055,23 @@ public function testOnlyAllowsWhitelistedColumns(): void public function testUseColumnEnumsArg(): void { + $this->schema = /** @lang GraphQL */ ' + type User { + id: ID! + } + + type Query { + enumColumns( + where: _ @whereConditions(columnsEnum: "UserColumn") + ): [User!]! @all + } + + enum UserColumn { + ID @enum(value: "id") + NAME @enum(value: "name") + } + '; + factory(User::class)->create(); $this->graphQL(/** @lang GraphQL */ ' @@ -892,7 +1098,17 @@ enumColumns( public function testIgnoreNullCondition(): void { - factory(User::class)->create(); + $this->schema = /** @lang GraphQL */ ' + type User { + id: ID! + } + + type Query { + users(where: _ @whereConditions): [User!]! @all + } + '; + + $user = factory(User::class)->create(); $this->graphQL(/** @lang GraphQL */ ' { @@ -906,7 +1122,7 @@ public function testIgnoreNullCondition(): void 'data' => [ 'users' => [ [ - 'id' => '1', + 'id' => "{$user->id}", ], ], ], @@ -962,7 +1178,7 @@ public function testHandler(): void type User { id: ID! } - + type Query { users(where: _ @whereConditions( columns: ["name"], From 618f1b8b6209c5c70ce7a9b3cceed3996e45dc88 Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 30 Dec 2021 13:27:05 +0100 Subject: [PATCH 04/10] implement --- docs/4/api-reference/directives.md | 6 +- docs/4/eloquent/complex-where-conditions.md | 8 +- docs/5/api-reference/directives.md | 8 +- docs/5/digging-deeper/ordering.md | 2 +- docs/5/eloquent/complex-where-conditions.md | 8 +- docs/master/api-reference/directives.md | 8 +- docs/master/digging-deeper/ordering.md | 2 +- .../eloquent/complex-where-conditions.md | 8 +- src/OrderBy/OrderByDirective.php | 8 +- src/Support/Traits/GeneratesColumnsEnum.php | 43 +++---- src/Support/Traits/GeneratesRelationsEnum.php | 105 +++++++++++++++ .../WhereConditionsBaseDirective.php | 24 ++-- .../WhereConditionsDirective.php | 18 ++- .../WhereConditionsServiceProvider.php | 16 +-- .../WhereHasConditionsDirective.php | 4 +- .../WhereConditionsDirectiveTest.php | 121 +++++++++++++----- 16 files changed, 285 insertions(+), 104 deletions(-) create mode 100644 src/Support/Traits/GeneratesRelationsEnum.php diff --git a/docs/4/api-reference/directives.md b/docs/4/api-reference/directives.md index e2dd2437e7..a3289385e3 100644 --- a/docs/4/api-reference/directives.md +++ b/docs/4/api-reference/directives.md @@ -1908,8 +1908,8 @@ directive @orderBy( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String @@ -1956,7 +1956,7 @@ enum SortOrder { } ``` -If you want to re-use a list of allowed columns, you can define your own enumeration type and use the `columnsEnum` argument instead of `columns`. +If you want to re-use a list of allowed columns, you can define your own enum type and use the `columnsEnum` argument instead of `columns`. Here's an example of how you could define it in your schema: ```graphql diff --git a/docs/4/eloquent/complex-where-conditions.md b/docs/4/eloquent/complex-where-conditions.md index 2fecb8b7f3..215764863c 100644 --- a/docs/4/eloquent/complex-where-conditions.md +++ b/docs/4/eloquent/complex-where-conditions.md @@ -41,8 +41,8 @@ directive @whereConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String @@ -220,8 +220,8 @@ directive @whereHasConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/docs/5/api-reference/directives.md b/docs/5/api-reference/directives.md index d941dded58..c997abfa4e 100644 --- a/docs/5/api-reference/directives.md +++ b/docs/5/api-reference/directives.md @@ -2006,8 +2006,8 @@ directive @orderBy( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. """ @@ -2064,8 +2064,8 @@ input OrderByRelation { columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/docs/5/digging-deeper/ordering.md b/docs/5/digging-deeper/ordering.md index 175141ef5a..dc42febb3d 100644 --- a/docs/5/digging-deeper/ordering.md +++ b/docs/5/digging-deeper/ordering.md @@ -67,7 +67,7 @@ You may pass more than one sorting option to add a secondary ordering. ### Reuse Columns Enum -To re-use a list of allowed columns, define your own enumeration type and use the `columnsEnum` argument instead of `columns`: +To re-use a list of allowed columns, define your own enum type and use the `columnsEnum` argument instead of `columns`: ```graphql type Query { diff --git a/docs/5/eloquent/complex-where-conditions.md b/docs/5/eloquent/complex-where-conditions.md index 06c950253c..5f7a9fd9ba 100644 --- a/docs/5/eloquent/complex-where-conditions.md +++ b/docs/5/eloquent/complex-where-conditions.md @@ -41,8 +41,8 @@ directive @whereConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String @@ -251,8 +251,8 @@ directive @whereHasConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index d941dded58..c997abfa4e 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -2006,8 +2006,8 @@ directive @orderBy( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. """ @@ -2064,8 +2064,8 @@ input OrderByRelation { columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/docs/master/digging-deeper/ordering.md b/docs/master/digging-deeper/ordering.md index 175141ef5a..dc42febb3d 100644 --- a/docs/master/digging-deeper/ordering.md +++ b/docs/master/digging-deeper/ordering.md @@ -67,7 +67,7 @@ You may pass more than one sorting option to add a secondary ordering. ### Reuse Columns Enum -To re-use a list of allowed columns, define your own enumeration type and use the `columnsEnum` argument instead of `columns`: +To re-use a list of allowed columns, define your own enum type and use the `columnsEnum` argument instead of `columns`: ```graphql type Query { diff --git a/docs/master/eloquent/complex-where-conditions.md b/docs/master/eloquent/complex-where-conditions.md index 06c950253c..5f7a9fd9ba 100644 --- a/docs/master/eloquent/complex-where-conditions.md +++ b/docs/master/eloquent/complex-where-conditions.md @@ -41,8 +41,8 @@ directive @whereConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String @@ -251,8 +251,8 @@ directive @whereHasConditions( columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/src/OrderBy/OrderByDirective.php b/src/OrderBy/OrderByDirective.php index 1ebe2cb329..0c1cc0ce2d 100644 --- a/src/OrderBy/OrderByDirective.php +++ b/src/OrderBy/OrderByDirective.php @@ -40,8 +40,8 @@ public static function definition(): string columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. """ @@ -98,8 +98,8 @@ enum OrderByDirection { columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/src/Support/Traits/GeneratesColumnsEnum.php b/src/Support/Traits/GeneratesColumnsEnum.php index 31d46c2748..81b1d813d5 100644 --- a/src/Support/Traits/GeneratesColumnsEnum.php +++ b/src/Support/Traits/GeneratesColumnsEnum.php @@ -24,12 +24,12 @@ trait GeneratesColumnsEnum */ protected function hasAllowedColumns(): bool { - $hasColumns = ! is_null($this->directiveArgValue('columns')); - $hasColumnsEnum = ! is_null($this->directiveArgValue('columnsEnum')); + $hasColumns = null !== $this->directiveArgValue('columns'); + $hasColumnsEnum = null !== $this->directiveArgValue('columnsEnum'); if ($hasColumns && $hasColumnsEnum) { throw new DefinitionException( - 'The @' . $this->name() . ' directive can only have one of the following arguments: `columns`, `columnsEnum`.' + "The @{$this->name()} directive can only have one of the following arguments: `columns`, `columnsEnum`." ); } @@ -37,7 +37,7 @@ protected function hasAllowedColumns(): bool } /** - * Generate the enumeration type for the list of allowed columns. + * Generate the enum type for the list of allowed columns. * * @return string the name of the used enum */ @@ -49,28 +49,27 @@ protected function generateColumnsEnum( ): string { $columnsEnum = $this->directiveArgValue('columnsEnum'); - if (! is_null($columnsEnum)) { + if (null !== $columnsEnum) { return $columnsEnum; } $allowedColumnsEnumName = ASTHelper::qualifiedArgType($argDefinition, $parentField, $parentType) . 'Column'; - $documentAST - ->setTypeDefinition( - static::createAllowedColumnsEnum( - $argDefinition, - $parentField, - $parentType, - $this->directiveArgValue('columns'), - $allowedColumnsEnumName - ) - ); + $documentAST->setTypeDefinition( + static::createAllowedColumnsEnum( + $argDefinition, + $parentField, + $parentType, + $this->directiveArgValue('columns'), + $allowedColumnsEnumName + ) + ); return $allowedColumnsEnumName; } /** - * Create the Enum that holds the allowed columns. + * Create the enum that holds the allowed columns. * * @param array $allowedColumns */ @@ -83,11 +82,11 @@ protected function createAllowedColumnsEnum( ): EnumTypeDefinitionNode { $enumValues = array_map( function (string $columnName): string { - return - strtoupper( - Str::snake($columnName) - ) - . ' @enum(value: "' . $columnName . '")'; + $key = strtoupper( + Str::snake($columnName) + ); + + return "{$key} @enum(value: \"{$columnName}\")"; }, $allowedColumns ); @@ -96,7 +95,7 @@ function (string $columnName): string { return Parser::enumTypeDefinition(/** @lang GraphQL */ <<name->value}.{$parentField->name->value}.{$argDefinition->name->value}." -enum $allowedColumnsEnumName { +enum {$allowedColumnsEnumName} { {$enumValuesString} } GRAPHQL diff --git a/src/Support/Traits/GeneratesRelationsEnum.php b/src/Support/Traits/GeneratesRelationsEnum.php new file mode 100644 index 0000000000..5579ed79df --- /dev/null +++ b/src/Support/Traits/GeneratesRelationsEnum.php @@ -0,0 +1,105 @@ +directiveArgValue('relations'); + $hasRelationsEnum = null !== $this->directiveArgValue('relationsEnum'); + + if ($hasRelations && $hasRelationsEnum) { + throw new DefinitionException( + "The @{$this->name()} directive can only have one of the following arguments: `relations`, `relationsEnum`." + ); + } + + return $hasRelations || $hasRelationsEnum; + } + + /** + * Generate the enum type for the list of allowed relations. + * + * @return string the name of the used enum + */ + protected function generateRelationsEnum( + DocumentAST &$documentAST, + InputValueDefinitionNode &$argDefinition, + FieldDefinitionNode &$parentField, + ObjectTypeDefinitionNode &$parentType + ): string { + $relationsEnum = $this->directiveArgValue('relationsEnum'); + + if (null !== $relationsEnum) { + return $relationsEnum; + } + + $allowedRelationsEnumName = ASTHelper::qualifiedArgType($argDefinition, $parentField, $parentType) . 'Relation'; + + $documentAST->setTypeDefinition( + static::createAllowedRelationsEnum( + $argDefinition, + $parentField, + $parentType, + $this->directiveArgValue('relations'), + $allowedRelationsEnumName + ) + ); + + return $allowedRelationsEnumName; + } + + /** + * Create the enum that holds the allowed relations. + * + * @param array $allowedRelations + */ + protected function createAllowedRelationsEnum( + InputValueDefinitionNode &$argDefinition, + FieldDefinitionNode &$parentField, + ObjectTypeDefinitionNode &$parentType, + array $allowedRelations, + string $allowedRelationsEnumName + ): EnumTypeDefinitionNode { + $enumValues = array_map( + function (string $relationName): string { + $separatedRelations = str_replace('.', '__', $relationName); + $key = strtoupper( + Str::snake($separatedRelations) + ); + + return "{$key} @enum(value: \"{$relationName}\")"; + }, + $allowedRelations + ); + + $enumValuesString = implode("\n", $enumValues); + + return Parser::enumTypeDefinition(/** @lang GraphQL */ <<name->value}.{$parentField->name->value}.{$argDefinition->name->value}." +enum {$allowedRelationsEnumName} { + {$enumValuesString} +} +GRAPHQL + ); + } +} diff --git a/src/WhereConditions/WhereConditionsBaseDirective.php b/src/WhereConditions/WhereConditionsBaseDirective.php index 229e1cca78..c49a96722c 100644 --- a/src/WhereConditions/WhereConditionsBaseDirective.php +++ b/src/WhereConditions/WhereConditionsBaseDirective.php @@ -12,10 +12,12 @@ use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; use Nuwave\Lighthouse\Support\Contracts\ArgManipulator; use Nuwave\Lighthouse\Support\Traits\GeneratesColumnsEnum; +use Nuwave\Lighthouse\Support\Traits\GeneratesRelationsEnum; abstract class WhereConditionsBaseDirective extends BaseDirective implements ArgBuilderDirective, ArgManipulator { use GeneratesColumnsEnum; + use GeneratesRelationsEnum; /** * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder the builder used to resolve the field @@ -36,24 +38,30 @@ public function manipulateArgDefinition( FieldDefinitionNode &$parentField, ObjectTypeDefinitionNode &$parentType ): void { - if ($this->hasAllowedColumns()) { - $restrictedWhereConditionsName = ASTHelper::qualifiedArgType($argDefinition, $parentField, $parentType) . $this->generatedInputSuffix(); - $argDefinition->type = Parser::namedType($restrictedWhereConditionsName); - $allowedColumnsEnumName = $this->generateColumnsEnum($documentAST, $argDefinition, $parentField, $parentType); + $hasAllowedColumns = $this->hasAllowedColumns(); + $hasAllowedRelations = $this->hasAllowedRelations(); + + if ($hasAllowedColumns || $hasAllowedRelations) { + $qualifiedWhereConditionsName = ASTHelper::qualifiedArgType($argDefinition, $parentField, $parentType) . $this->generatedInputSuffix(); + $argDefinition->type = Parser::namedType($qualifiedWhereConditionsName); $documentAST->setTypeDefinition( WhereConditionsServiceProvider::createWhereConditionsInputType( - $restrictedWhereConditionsName, + $qualifiedWhereConditionsName, "Dynamic WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", - $allowedColumnsEnumName + $hasAllowedColumns + ? $this->generateColumnsEnum($documentAST, $argDefinition, $parentField, $parentType) + : 'String' ) ); $documentAST->setTypeDefinition( WhereConditionsServiceProvider::createHasConditionsInputType( - $restrictedWhereConditionsName, + $qualifiedWhereConditionsName, "Dynamic HAS conditions for WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", - $this->directiveArgValue('relations') + $hasAllowedRelations + ? $this->generateRelationsEnum($documentAST, $argDefinition, $parentField, $parentType) + : 'String' ) ); } else { diff --git a/src/WhereConditions/WhereConditionsDirective.php b/src/WhereConditions/WhereConditionsDirective.php index 67ac9c239e..82757f47c2 100644 --- a/src/WhereConditions/WhereConditionsDirective.php +++ b/src/WhereConditions/WhereConditionsDirective.php @@ -19,12 +19,26 @@ public static function definition(): string columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String + """ + Restrict the allowed relation names to a well-defined list. + This improves introspection capabilities and security. + Mutually exclusive with the `relationsEnum` argument. + """ + relations: [String!] + + """ + Use an existing enum type to restrict the allowed relations to a well-defined list. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `relations` argument. + """ + relationsEnum: String + """ Reference a method that applies the client given conditions to the query builder. diff --git a/src/WhereConditions/WhereConditionsServiceProvider.php b/src/WhereConditions/WhereConditionsServiceProvider.php index 2813bad7bb..ac0050f3fb 100644 --- a/src/WhereConditions/WhereConditionsServiceProvider.php +++ b/src/WhereConditions/WhereConditionsServiceProvider.php @@ -48,7 +48,8 @@ function (ManipulateAST $manipulateAST): void { ->setTypeDefinition( static::createHasConditionsInputType( static::DEFAULT_WHERE_CONDITIONS, - 'Dynamic HAS conditions for WHERE condition queries.' + 'Dynamic HAS conditions for WHERE condition queries.', + 'String' ) ) ->setTypeDefinition( @@ -102,22 +103,15 @@ public static function createWhereConditionsInputType(string $name, string $desc ); } - /** - * @param array $relations - */ - public static function createHasConditionsInputType(string $name, string $description, array $relations): InputObjectTypeDefinitionNode + public static function createHasConditionsInputType(string $name, string $description, string $relationType): InputObjectTypeDefinitionNode { - // TODO turn $relations into scalar, perhaps like - // scalar {$name}Relation @scalar(regex: "^{implode('|', $relations)}$") $hasRelationInputName = $name . self::DEFAULT_WHERE_RELATION_CONDITIONS; $defaultHasAmount = self::DEFAULT_HAS_AMOUNT; /** @var \Nuwave\Lighthouse\WhereConditions\Operator $operator */ $operator = app(Operator::class); - $operatorName = Parser::enumTypeDefinition( - $operator->enumDefinition() - ) + $operatorName = Parser::enumTypeDefinition($operator->enumDefinition()) ->name ->value; $operatorDefault = $operator->defaultHasOperator(); @@ -126,7 +120,7 @@ public static function createHasConditionsInputType(string $name, string $descri "$description" input $hasRelationInputName { "The relation that is checked." - relation: String! + relation: $relationType! "The comparison operator to test against the amount." operator: $operatorName = $operatorDefault diff --git a/src/WhereConditions/WhereHasConditionsDirective.php b/src/WhereConditions/WhereHasConditionsDirective.php index a0bd36da61..feaed0cddb 100644 --- a/src/WhereConditions/WhereHasConditionsDirective.php +++ b/src/WhereConditions/WhereHasConditionsDirective.php @@ -33,8 +33,8 @@ public static function definition(): string columns: [String!] """ - Use an existing enumeration type to restrict the allowed columns to a predefined list. - This allowes you to re-use the same enum for multiple fields. + Use an existing enum type to restrict the allowed columns to a well-defined list. + This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String diff --git a/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php b/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php index 996f904668..427070f14f 100644 --- a/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php +++ b/tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php @@ -25,7 +25,7 @@ protected function getPackageProviders($app): array public function testDefaultsToWhereEqual(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -53,7 +53,7 @@ public function testDefaultsToWhereEqual(): void public function testOverwritesTheOperator(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -82,7 +82,7 @@ public function testOverwritesTheOperator(): void public function testOperatorIn(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -122,7 +122,7 @@ public function testOperatorIn(): void public function testOperatorIsNull(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type Post { id: ID! } @@ -163,7 +163,7 @@ public function testOperatorIsNull(): void public function testOperatorNotNull(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type Post { id: ID! } @@ -204,7 +204,7 @@ public function testOperatorNotNull(): void public function testOperatorNotBetween(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -244,7 +244,7 @@ public function testOperatorNotBetween(): void public function testAddsNestedAnd(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -282,7 +282,7 @@ public function testAddsNestedAnd(): void public function testAddsNestedOr(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -341,7 +341,7 @@ public function testAddsNestedOr(): void public function testAddsNestedAndOr(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -398,7 +398,7 @@ public function testAddsNestedAndOr(): void public function testHasMixed(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -492,7 +492,7 @@ public function testHasMixed(): void public function testHasRelation(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -546,7 +546,7 @@ public function testHasRelation(): void public function testHasAmount(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -602,7 +602,7 @@ public function testHasAmount(): void public function testHasOperator(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -659,7 +659,7 @@ public function testHasOperator(): void public function testHasCondition(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -714,7 +714,7 @@ public function testHasCondition(): void public function testHasRecursive(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -770,7 +770,7 @@ public function testHasRecursive(): void public function testHasNested(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -836,7 +836,7 @@ public function testHasNested(): void public function testRejectsInvalidColumnName(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -866,7 +866,7 @@ public function testRejectsInvalidColumnName(): void public function testQueriesEmptyStrings(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -912,7 +912,7 @@ public function testQueriesEmptyStrings(): void public function testQueryForNull(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -958,7 +958,7 @@ public function testQueryForNull(): void public function testRequiresAValueForAColumn(): void { - $this->schema = /** @lang GraphQL */' + $this->schema = /** @lang GraphQL */ ' type User { id: ID! } @@ -981,15 +981,15 @@ public function testRequiresAValueForAColumn(): void ')->assertGraphQLErrorMessage(SQLOperator::missingValueForColumn('no_value')); } - public function testOnlyAllowsWhitelistedColumns(): void + public function testWellDefinedColumns(): void { $this->schema = /** @lang GraphQL */ ' type User { id: ID! } - + type Query { - whitelistedColumns( + users( where: _ @whereConditions(columns: ["id", "camelCase"]) ): [User!]! @all } @@ -999,7 +999,7 @@ public function testOnlyAllowsWhitelistedColumns(): void $this->graphQL(/** @lang GraphQL */ ' { - whitelistedColumns( + users( where: { column: ID value: 1 @@ -1010,7 +1010,7 @@ public function testOnlyAllowsWhitelistedColumns(): void } ')->assertJson([ 'data' => [ - 'whitelistedColumns' => [ + 'users' => [ [ 'id' => 1, ], @@ -1018,7 +1018,7 @@ public function testOnlyAllowsWhitelistedColumns(): void ], ]); - $expectedEnumName = 'QueryWhitelistedColumnsWhereColumn'; + $expectedEnumName = 'QueryUsersWhereColumn'; $enum = $this->introspectType($expectedEnumName); $this->assertNotNull($enum); @@ -1029,7 +1029,7 @@ public function testOnlyAllowsWhitelistedColumns(): void [ 'kind' => 'ENUM', 'name' => $expectedEnumName, - 'description' => 'Allowed column names for Query.whitelistedColumns.where.', + 'description' => 'Allowed column names for Query.users.where.', 'fields' => null, 'inputFields' => null, 'interfaces' => null, @@ -1053,19 +1053,80 @@ public function testOnlyAllowsWhitelistedColumns(): void ); } + public function testWellDefinedRelations(): void + { + $this->schema = /** @lang GraphQL */ ' + type User { + id: ID! + } + + type Query { + users( + where: _ @whereConditions(relations: ["posts", "posts.comments", "alternateConnections.user", "alternate.connectionsUser"]) + ): [User!]! @all + } + '; + + $expectedEnumName = 'QueryUsersWhereRelation'; + $enum = $this->introspectType($expectedEnumName); + + $this->assertNotNull($enum); + /** @var array $enum */ + + // TODO: Replace with dms/phpunit-arraysubset-asserts when we require PHPUnit 9 + PHP 7.3 + $this->assertSame( + [ + 'kind' => 'ENUM', + 'name' => $expectedEnumName, + 'description' => 'Allowed relation names for Query.users.where.', + 'fields' => null, + 'inputFields' => null, + 'interfaces' => null, + 'enumValues' => [ + [ + 'name' => 'POSTS', + 'description' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'POSTS__COMMENTS', + 'description' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'ALTERNATE_CONNECTIONS__USER', + 'description' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'ALTERNATE__CONNECTIONS_USER', + 'description' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + ], + 'possibleTypes' => null, + ], + $enum + ); + } + public function testUseColumnEnumsArg(): void { $this->schema = /** @lang GraphQL */ ' type User { id: ID! } - + type Query { enumColumns( where: _ @whereConditions(columnsEnum: "UserColumn") ): [User!]! @all } - + enum UserColumn { ID @enum(value: "id") NAME @enum(value: "name") @@ -1102,7 +1163,7 @@ public function testIgnoreNullCondition(): void type User { id: ID! } - + type Query { users(where: _ @whereConditions): [User!]! @all } From a01b8ca909ab5a01f7952c5df3ceb88219525322 Mon Sep 17 00:00:00 2001 From: spawnia Date: Thu, 30 Dec 2021 13:31:31 +0100 Subject: [PATCH 05/10] cleanup --- CHANGELOG.md | 2 +- docs/5/api-reference/directives.md | 2 +- docs/master/api-reference/directives.md | 2 +- src/OrderBy/OrderByDirective.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7d4374a1..3428ae0a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ You can find and compare releases at the [GitHub release page](https://github.co ### Added -- Allow constraining allowed relations in `@whereConditions` and `@whereHasConditions` +- Allow constraining allowed relations in `@whereConditions` and `@whereHasConditions` https://github.com/nuwave/lighthouse/pull/1896 ## v5.33.0 diff --git a/docs/5/api-reference/directives.md b/docs/5/api-reference/directives.md index c997abfa4e..9624242821 100644 --- a/docs/5/api-reference/directives.md +++ b/docs/5/api-reference/directives.md @@ -2052,7 +2052,7 @@ Options for the `relations` argument on `@orderBy`. """ input OrderByRelation { """ - TODO: description + Name of the relation. """ relation: String! diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index c997abfa4e..9624242821 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -2052,7 +2052,7 @@ Options for the `relations` argument on `@orderBy`. """ input OrderByRelation { """ - TODO: description + Name of the relation. """ relation: String! diff --git a/src/OrderBy/OrderByDirective.php b/src/OrderBy/OrderByDirective.php index 0c1cc0ce2d..bd0d3d77c5 100644 --- a/src/OrderBy/OrderByDirective.php +++ b/src/OrderBy/OrderByDirective.php @@ -86,7 +86,7 @@ enum OrderByDirection { """ input OrderByRelation { """ - TODO: description + Name of the relation. """ relation: String! From 3e8bb1c38c1b14151b4a55ed5726e08b62b44a36 Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 3 Jan 2022 21:07:49 +0100 Subject: [PATCH 06/10] document --- CHANGELOG.md | 2 +- docs/3/api-reference/directives.md | 4 +- docs/4/api-reference/directives.md | 4 +- docs/4/eloquent/complex-where-conditions.md | 24 +- docs/5/api-reference/directives.md | 8 +- docs/5/eloquent/complex-where-conditions.md | 22 +- docs/master/api-reference/directives.md | 8 +- .../eloquent/complex-where-conditions.md | 255 ++++++++---------- src/OrderBy/OrderByDirective.php | 8 +- .../WhereConditionsBaseDirective.php | 4 +- .../WhereConditionsDirective.php | 8 +- .../WhereConditionsServiceProvider.php | 1 + .../WhereHasConditionsDirective.php | 5 +- 13 files changed, 155 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3428ae0a28..e9404975ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1189,7 +1189,7 @@ You can find and compare releases at the [GitHub release page](https://github.co a [whereJsonContains filter - Allow using callable classes with `__invoke` when referencing methods in directives and when looking for default resolvers or type resolvers https://github.com/nuwave/lighthouse/issues/882 -- Allow to restrict column names to a well-defined list in `@whereContraints` +- Allow to restrict column names to a well-defined enum in `@whereContraints` and generate definitions for an `Enum` type and an `Input` type that are restricted to the defined columns https://github.com/nuwave/lighthouse/pull/916 - Add test helpers for introspection queries to `MakesGraphQLRequests` https://github.com/nuwave/lighthouse/pull/916 diff --git a/docs/3/api-reference/directives.md b/docs/3/api-reference/directives.md index 9178eb851d..94a567b081 100644 --- a/docs/3/api-reference/directives.md +++ b/docs/3/api-reference/directives.md @@ -2214,7 +2214,7 @@ type Query { ``` This is how you can use it to construct a complex query -that gets actors over age 37 who either have red hair or are at least 150cm. +that gets actors over age 37 who either have red hair or are at least 150 cm. ```graphql { @@ -2227,7 +2227,7 @@ that gets actors over age 37 who either have red hair or are at least 150cm. { column: "type", value: "Actor" } { OR: [ - { column: "haircolour", value: "red" } + { column: "hair_color", value: "red" } { column: "height", operator: GTE, value: 150 } ] } diff --git a/docs/4/api-reference/directives.md b/docs/4/api-reference/directives.md index a3289385e3..62fb88bdb0 100644 --- a/docs/4/api-reference/directives.md +++ b/docs/4/api-reference/directives.md @@ -1900,7 +1900,7 @@ Sort a result list by one or more given columns. """ directive @orderBy( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. If not given, the column names can be passed as a String by clients. Mutually exclusive with the `columnsEnum` argument. @@ -1908,7 +1908,7 @@ directive @orderBy( columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/docs/4/eloquent/complex-where-conditions.md b/docs/4/eloquent/complex-where-conditions.md index 215764863c..688a07230d 100644 --- a/docs/4/eloquent/complex-where-conditions.md +++ b/docs/4/eloquent/complex-where-conditions.md @@ -34,14 +34,14 @@ Add a dynamically client-controlled WHERE condition to a fields query. """ directive @whereConditions( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ @@ -54,7 +54,7 @@ You can apply this directive on any field that performs an Eloquent query: ```graphql type Query { people( - where: _ @whereConditions(columns: ["age", "type", "haircolour", "height"]) + where: _ @whereConditions(columns: ["age", "type", "hair_color", "height"]) ): [Person!]! @all } @@ -63,7 +63,7 @@ type Person { age: Int! height: Int! type: String! - hair_colour: String! + hair_color: String! } ``` @@ -95,7 +95,7 @@ input PeopleWhereWhereConditions { enum PeopleWhereColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + hair_color @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -117,7 +117,7 @@ type Query { enum PersonColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + hair_color @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -127,7 +127,7 @@ Instead of creating enums for the allowed columns, it will simply use the existi It is recommended to either use the `columns` or the `columnsEnum` argument. When you don't define any allowed columns, clients can specify arbitrary column names as a `String`. -This approach should by taken with care, as it carries +This approach should be taken with care, as it carries potential performance and security risks and offers little type safety. A simple query for a person who is exactly 42 years old would look like this: @@ -143,7 +143,7 @@ A simple query for a person who is exactly 42 years old would look like this: Note that the operator defaults to `EQ` (`=`) if not given, so you could also omit it from the previous example and get the same result. -The following query gets actors over age 37 who either have red hair or are at least 150cm: +The following query gets actors over age 37 who either have red hair or are at least 150 cm: ```graphql { @@ -154,7 +154,7 @@ The following query gets actors over age 37 who either have red hair or are at l { column: TYPE, value: "Actor" } { OR: [ - { column: HAIRCOLOUR, value: "red" } + { column: hair_color, value: "red" } { column: HEIGHT, operator: GTE, value: 150 } ] } @@ -174,7 +174,7 @@ query gets people that have no hair and blue-ish eyes: people( where: { AND: [ - { column: HAIRCOLOUR, operator: IS_NULL } + { column: hair_color, operator: IS_NULL } { column: EYES, operator: IN, value: ["blue", "aqua", "turquoise"] } ] } @@ -213,14 +213,14 @@ directive @whereHasConditions( relation: String """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/docs/5/api-reference/directives.md b/docs/5/api-reference/directives.md index 9624242821..4b00674452 100644 --- a/docs/5/api-reference/directives.md +++ b/docs/5/api-reference/directives.md @@ -1998,7 +1998,7 @@ Sort a result list by one or more given columns. """ directive @orderBy( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. Only used when the directive is added on an argument. @@ -2006,7 +2006,7 @@ directive @orderBy( columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. @@ -2057,14 +2057,14 @@ input OrderByRelation { relation: String! """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/docs/5/eloquent/complex-where-conditions.md b/docs/5/eloquent/complex-where-conditions.md index 5f7a9fd9ba..756d41075a 100644 --- a/docs/5/eloquent/complex-where-conditions.md +++ b/docs/5/eloquent/complex-where-conditions.md @@ -34,14 +34,14 @@ Add a dynamically client-controlled WHERE condition to a fields query. """ directive @whereConditions( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ @@ -67,7 +67,7 @@ You can apply this directive on any field that performs an Eloquent query: ```graphql type Query { people( - where: _ @whereConditions(columns: ["age", "type", "haircolour", "height"]) + where: _ @whereConditions(columns: ["age", "type", "hair_color", "height"]) ): [Person!]! @all } @@ -76,7 +76,7 @@ type Person { age: Int! height: Int! type: String! - hair_colour: String! + hair_color: String! } ``` @@ -111,7 +111,7 @@ input QueryPeopleWhereWhereConditions { enum QueryPeopleWhereColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + hair_color @enum(value: "hair_color") HEIGHT @enum(value: "height") } @@ -148,7 +148,7 @@ type Query { enum PersonColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + hair_color @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -174,7 +174,7 @@ A simple query for a person who is exactly 42 years old would look like this: Note that the operator defaults to `EQ` (`=`) if not given, so you could also omit it from the previous example and get the same result. -The following query gets actors over age 37 who either have red hair or are at least 150cm: +The following query gets actors over age 37 who either have red hair or are at least 150 cm: ```graphql { @@ -185,7 +185,7 @@ The following query gets actors over age 37 who either have red hair or are at l { column: TYPE, value: "Actor" } { OR: [ - { column: HAIRCOLOUR, value: "red" } + { column: hair_color, value: "red" } { column: HEIGHT, operator: GTE, value: 150 } ] } @@ -205,7 +205,7 @@ query gets people that have no hair and blue-ish eyes: people( where: { AND: [ - { column: HAIRCOLOUR, operator: IS_NULL } + { column: hair_color, operator: IS_NULL } { column: EYES, operator: IN, value: ["blue", "aqua", "turquoise"] } ] } @@ -244,14 +244,14 @@ directive @whereHasConditions( relation: String """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 9624242821..4b00674452 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -1998,7 +1998,7 @@ Sort a result list by one or more given columns. """ directive @orderBy( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. Only used when the directive is added on an argument. @@ -2006,7 +2006,7 @@ directive @orderBy( columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. @@ -2057,14 +2057,14 @@ input OrderByRelation { relation: String! """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/docs/master/eloquent/complex-where-conditions.md b/docs/master/eloquent/complex-where-conditions.md index 5f7a9fd9ba..56b8835d80 100644 --- a/docs/master/eloquent/complex-where-conditions.md +++ b/docs/master/eloquent/complex-where-conditions.md @@ -5,7 +5,7 @@ Adding query conditions ad-hoc can be cumbersome and limiting when you require manifold ways to filter query results. Lighthouse's `WhereConditions` extension can give advanced query capabilities to clients -and allow them to apply complex, dynamic WHERE conditions to queries. +and allow them to apply complex and dynamic filters. ## Setup @@ -19,55 +19,75 @@ Add the service provider to your `config/app.php`: Install the dependency [mll-lab/graphql-php-scalars](https://github.com/mll-lab/graphql-php-scalars): - composer require mll-lab/graphql-php-scalars:^4 + composer require mll-lab/graphql-php-scalars -## Usage - -You can use this feature through a set of schema directives that enhance fields -with advanced filter capabilities. - -### @whereConditions +## @whereConditions ```graphql """ Add a dynamically client-controlled WHERE condition to a fields query. """ directive @whereConditions( - """ - Restrict the allowed column names to a well-defined list. - This improves introspection capabilities and security. - Mutually exclusive with the `columnsEnum` argument. - """ - columns: [String!] - - """ - Use an existing enum type to restrict the allowed columns to a well-defined list. - This allows you to re-use the same enum for multiple fields. - Mutually exclusive with the `columns` argument. - """ - columnsEnum: String - - """ - Reference a method that applies the client given conditions to the query builder. - - Expected signature: `( - \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, - array $whereConditions - ): void` - - Consists of two parts: a class name and a method name, separated by an `@` symbol. - If you pass only a class name, the method name defaults to `__invoke`. - """ - handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" + """ + Restrict the allowed column names to a well-defined enum. + This improves introspection capabilities and security. + Mutually exclusive with the `columnsEnum` argument. + """ + columns: [String!] + + """ + Use an existing enum type to restrict the allowed columns to a well-defined enum. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `columns` argument. + """ + columnsEnum: String + + """ + Restrict the allowed relation names to a well-defined enum. + This improves introspection capabilities and security. + Mutually exclusive with the `relationsEnum` argument. + """ + relations: [String!] + + """ + Use an existing enum type to restrict the allowed relations to a well-defined enum. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `relations` argument. + """ + relationsEnum: String + + """ + Reference a method that applies the client given conditions to the query builder. + + Expected signature: `( + \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, + array $whereConditions + ): void` + + Consists of two parts: a class name and a method name, separated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" ) on ARGUMENT_DEFINITION ``` -You can apply this directive on any field that performs an Eloquent query: +### Basic usage + +You can apply this directive on any field that performs an Eloquent query. + +It is recommended to use both: +- the `columns` or the `columnsEnum` argument +- the `relations` or the `relationsEnum` argument. + +When you don't define allowed values, clients can specify arbitrary `String` values, +which poses a risk to performance, security and type safety. ```graphql type Query { people( - where: _ @whereConditions(columns: ["age", "type", "haircolour", "height"]) + where: _ @whereConditions( + columns: ["age", "type", "hair_color", "height"] + relations: ["friends", "friends.friends"]) ): [Person!]! @all } @@ -76,17 +96,20 @@ type Person { age: Int! height: Int! type: String! - hair_colour: String! + hair_color: String! + friends: [Person!]! @hasMany } ``` +### Generated code + Lighthouse automatically generates definitions for an `Enum` type and an `Input` type that are restricted to the defined columns, so you do not have to specify them by hand. The blank type named `_` will be changed to the actual type. Here are the types that will be included in the compiled schema: ```graphql -"Dynamic WHERE conditions for the `where` argument on the query `people`." +"Dynamic WHERE conditions for Query.people.where." input QueryPeopleWhereWhereConditions { "The column that is used for the condition." column: QueryPeopleWhereColumn @@ -107,18 +130,18 @@ input QueryPeopleWhereWhereConditions { HAS: QueryPeopleWhereWhereConditionsRelation } -"Allowed column names for the `where` argument on the query `people`." +"Allowed column names for Query.people.where." enum QueryPeopleWhereColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } -"Dynamic HAS conditions for WHERE condition queries." +"Dynamic HAS conditions for Query.people.where." input QueryPeopleWhereWhereConditionsRelation { "The relation that is checked." - relation: String! + relation: QueryPeopleWhereRelation! "The comparison operator to test against the amount." operator: SQLOperator = GTE @@ -129,10 +152,18 @@ input QueryPeopleWhereWhereConditionsRelation { "Additional condition logic." condition: QueryPeopleWhereWhereConditions } + +"Allowed relation names for Query.people.where." +enum QueryPeopleWhereRelation { + FRIENDS @enum(value: "friends") + FRIENDS__FRIENDS @enum(value: "friends.friends") +} ``` +### Reuse existing enum + Alternatively to the `columns` argument, you can also use `columnsEnum` in case you -want to re-use a list of allowed columns. Here's how your schema could look like: +want to re-use an enum of allowed columns. Here's how your schema could look like: ```graphql type Query { @@ -144,11 +175,11 @@ type Query { ): [Person!]! @paginated } -"A custom description for this custom enum." +"Filterable columns of Person." enum PersonColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - HAIRCOLOUR @enum(value: "haircolour") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -156,10 +187,9 @@ enum PersonColumn { Lighthouse will still automatically generate the necessary input types. Instead of creating enums for the allowed columns, it will simply use the existing `PersonColumn` enum. -It is recommended to either use the `columns` or the `columnsEnum` argument. -When you don't define any allowed columns, clients can specify arbitrary column names as a `String`. -This approach should by taken with care, as it carries -potential performance and security risks and offers little type safety. +The same works for `relationColumns`. + +### Example queries A simple query for a person who is exactly 42 years old would look like this: @@ -174,7 +204,7 @@ A simple query for a person who is exactly 42 years old would look like this: Note that the operator defaults to `EQ` (`=`) if not given, so you could also omit it from the previous example and get the same result. -The following query gets actors over age 37 who either have red hair or are at least 150cm: +The following query gets actors over age 37 who either have red hair or are at least 150 cm: ```graphql { @@ -185,7 +215,7 @@ The following query gets actors over age 37 who either have red hair or are at l { column: TYPE, value: "Actor" } { OR: [ - { column: HAIRCOLOUR, value: "red" } + { column: HAIR_COLOR, value: "red" } { column: HEIGHT, operator: GTE, value: 150 } ] } @@ -205,7 +235,7 @@ query gets people that have no hair and blue-ish eyes: people( where: { AND: [ - { column: HAIRCOLOUR, operator: IS_NULL } + { column: HAIR_COLOR, operator: IS_NULL } { column: EYES, operator: IN, value: ["blue", "aqua", "turquoise"] } ] } @@ -226,124 +256,49 @@ This query would retrieve all persons without any condition: } ``` -### @whereHasConditions - -```graphql -""" -Allows clients to filter a query based on the existence of a related model, using -a dynamically controlled `WHERE` condition that applies to the relationship. -""" -directive @whereHasConditions( - """ - The Eloquent relationship that the conditions will be applied to. - - This argument can be omitted if the argument name follows the naming - convention `has{$RELATION}`. For example, if the Eloquent relationship - is named `posts`, the argument name must be `hasPosts`. - """ - relation: String - - """ - Restrict the allowed column names to a well-defined list. - This improves introspection capabilities and security. - Mutually exclusive with the `columnsEnum` argument. - """ - columns: [String!] - - """ - Use an existing enum type to restrict the allowed columns to a well-defined list. - This allows you to re-use the same enum for multiple fields. - Mutually exclusive with the `columns` argument. - """ - columnsEnum: String - - """ - Reference a method that applies the client given conditions to the query builder. - - Expected signature: `( - \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, - array $whereConditions - ): void` - - Consists of two parts: a class name and a method name, separated by an `@` symbol. - If you pass only a class name, the method name defaults to `__invoke`. - """ - handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" -) on ARGUMENT_DEFINITION -``` - -This directive works very similar to [@whereConditions](#whereconditions), except that -the conditions are applied to a relation sub query: - -```graphql -type Query { - people( - hasRole: _ @whereHasConditions(columns: ["name", "access_level"]) - ): [Person!]! @all -} - -type Role { - name: String! - access_level: Int -} -``` - -Again, Lighthouse will auto-generate an `input` and `enum` definition for your query: - -```graphql -"Dynamic WHERE conditions for the `hasRole` argument on the query `people`." -input QueryPeopleHasRoleWhereConditions { - "The column that is used for the condition." - column: QueryPeopleHasRoleColumn - - "The operator that is used for the condition." - operator: SQLOperator = EQ +### Has Relations - "The value that is used for the condition." - value: Mixed - - "A set of conditions that requires all conditions to match." - AND: [QueryPeopleHasRoleWhereConditions!] - - "A set of conditions that requires at least one condition to match." - OR: [QueryPeopleHasRoleWhereConditions!] -} - -"Allowed column names for the `hasRole` argument on the query `people`." -enum QueryPeopleHasRoleColumn { - NAME @enum(value: "name") - ACCESS_LEVEL @enum(value: "access_level") -} -``` - -A simple query for a person who has an access level of at least 5, through one of -their roles, looks like this: +Use the `HAS` clause to filter by relation existence. +This query retrieves all persons that have at least 1 role. ```graphql { - people(hasRole: { column: ACCESS_LEVEL, operator: GTE, value: 5 }) { + people(where: { + HAS: { + relation: ROLE, + amount: 1, + operator: GTE + } + }) { name } } ``` -You can also query for relationship existence without any condition; simply use an empty object as argument value. -This query would retrieve all persons that have a role: +The default values for `amount` and `operator` are included in the previous example, +you can also omit them: ```graphql { - people(hasRole: {}) { + people(where: { HAS: { relation: ROLE } }) { name } } ``` -Just like with the [@whereCondition](../api-reference/directives.md#whereconditions) directive, using `null` as argument value does not have any effect on the query. -This query would retrieve all persons, no matter if they have a role or not: +You can also add additional +This filters people who have an access level of at least 5 through one of their roles: ```graphql { - people(hasRole: null) { + people(where: { + HAS: { + relation: ROLE, + amount: 1, + operator: GTE, + condition: { column: ACCESS_LEVEL, operator: GTE, value: 5 } + } + }) { name } } diff --git a/src/OrderBy/OrderByDirective.php b/src/OrderBy/OrderByDirective.php index bd0d3d77c5..c186b8acd5 100644 --- a/src/OrderBy/OrderByDirective.php +++ b/src/OrderBy/OrderByDirective.php @@ -32,7 +32,7 @@ public static function definition(): string """ directive @orderBy( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. Only used when the directive is added on an argument. @@ -40,7 +40,7 @@ public static function definition(): string columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. Only used when the directive is added on an argument. @@ -91,14 +91,14 @@ enum OrderByDirection { relation: String! """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ diff --git a/src/WhereConditions/WhereConditionsBaseDirective.php b/src/WhereConditions/WhereConditionsBaseDirective.php index c49a96722c..f994a40864 100644 --- a/src/WhereConditions/WhereConditionsBaseDirective.php +++ b/src/WhereConditions/WhereConditionsBaseDirective.php @@ -48,7 +48,7 @@ public function manipulateArgDefinition( $documentAST->setTypeDefinition( WhereConditionsServiceProvider::createWhereConditionsInputType( $qualifiedWhereConditionsName, - "Dynamic WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", + "Dynamic WHERE conditions for Query.{$parentField->name->value}.{$argDefinition->name->value}.", $hasAllowedColumns ? $this->generateColumnsEnum($documentAST, $argDefinition, $parentField, $parentType) : 'String' @@ -58,7 +58,7 @@ public function manipulateArgDefinition( $documentAST->setTypeDefinition( WhereConditionsServiceProvider::createHasConditionsInputType( $qualifiedWhereConditionsName, - "Dynamic HAS conditions for WHERE conditions for the `{$argDefinition->name->value}` argument on the query `{$parentField->name->value}`.", + "Dynamic HAS conditions for Query.{$parentField->name->value}.{$argDefinition->name->value}.", $hasAllowedRelations ? $this->generateRelationsEnum($documentAST, $argDefinition, $parentField, $parentType) : 'String' diff --git a/src/WhereConditions/WhereConditionsDirective.php b/src/WhereConditions/WhereConditionsDirective.php index 82757f47c2..653d38e3ec 100644 --- a/src/WhereConditions/WhereConditionsDirective.php +++ b/src/WhereConditions/WhereConditionsDirective.php @@ -12,28 +12,28 @@ public static function definition(): string """ directive @whereConditions( """ - Restrict the allowed column names to a well-defined list. + Restrict the allowed column names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `columnsEnum` argument. """ columns: [String!] """ - Use an existing enum type to restrict the allowed columns to a well-defined list. + Use an existing enum type to restrict the allowed columns to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `columns` argument. """ columnsEnum: String """ - Restrict the allowed relation names to a well-defined list. + Restrict the allowed relation names to a well-defined enum. This improves introspection capabilities and security. Mutually exclusive with the `relationsEnum` argument. """ relations: [String!] """ - Use an existing enum type to restrict the allowed relations to a well-defined list. + Use an existing enum type to restrict the allowed relations to a well-defined enum. This allows you to re-use the same enum for multiple fields. Mutually exclusive with the `relations` argument. """ diff --git a/src/WhereConditions/WhereConditionsServiceProvider.php b/src/WhereConditions/WhereConditionsServiceProvider.php index ac0050f3fb..da4d8a901e 100644 --- a/src/WhereConditions/WhereConditionsServiceProvider.php +++ b/src/WhereConditions/WhereConditionsServiceProvider.php @@ -116,6 +116,7 @@ public static function createHasConditionsInputType(string $name, string $descri ->value; $operatorDefault = $operator->defaultHasOperator(); + // TODO condition: $name makes no sense at all when columns/columnsEnum are used return Parser::inputObjectTypeDefinition(/** @lang GraphQL */ << Date: Mon, 3 Jan 2022 20:08:42 +0000 Subject: [PATCH 07/10] Prettify docs --- .../eloquent/complex-where-conditions.md | 117 +++++++++--------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/docs/master/eloquent/complex-where-conditions.md b/docs/master/eloquent/complex-where-conditions.md index 56b8835d80..c179a83cae 100644 --- a/docs/master/eloquent/complex-where-conditions.md +++ b/docs/master/eloquent/complex-where-conditions.md @@ -28,46 +28,46 @@ Install the dependency [mll-lab/graphql-php-scalars](https://github.com/mll-lab/ Add a dynamically client-controlled WHERE condition to a fields query. """ directive @whereConditions( - """ - Restrict the allowed column names to a well-defined enum. - This improves introspection capabilities and security. - Mutually exclusive with the `columnsEnum` argument. - """ - columns: [String!] - - """ - Use an existing enum type to restrict the allowed columns to a well-defined enum. - This allows you to re-use the same enum for multiple fields. - Mutually exclusive with the `columns` argument. - """ - columnsEnum: String - - """ - Restrict the allowed relation names to a well-defined enum. - This improves introspection capabilities and security. - Mutually exclusive with the `relationsEnum` argument. - """ - relations: [String!] - - """ - Use an existing enum type to restrict the allowed relations to a well-defined enum. - This allows you to re-use the same enum for multiple fields. - Mutually exclusive with the `relations` argument. - """ - relationsEnum: String - - """ - Reference a method that applies the client given conditions to the query builder. - - Expected signature: `( - \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, - array $whereConditions - ): void` - - Consists of two parts: a class name and a method name, separated by an `@` symbol. - If you pass only a class name, the method name defaults to `__invoke`. - """ - handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" + """ + Restrict the allowed column names to a well-defined enum. + This improves introspection capabilities and security. + Mutually exclusive with the `columnsEnum` argument. + """ + columns: [String!] + + """ + Use an existing enum type to restrict the allowed columns to a well-defined enum. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `columns` argument. + """ + columnsEnum: String + + """ + Restrict the allowed relation names to a well-defined enum. + This improves introspection capabilities and security. + Mutually exclusive with the `relationsEnum` argument. + """ + relations: [String!] + + """ + Use an existing enum type to restrict the allowed relations to a well-defined enum. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `relations` argument. + """ + relationsEnum: String + + """ + Reference a method that applies the client given conditions to the query builder. + + Expected signature: `( + \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, + array $whereConditions + ): void` + + Consists of two parts: a class name and a method name, separated by an `@` symbol. + If you pass only a class name, the method name defaults to `__invoke`. + """ + handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" ) on ARGUMENT_DEFINITION ``` @@ -76,6 +76,7 @@ directive @whereConditions( You can apply this directive on any field that performs an Eloquent query. It is recommended to use both: + - the `columns` or the `columnsEnum` argument - the `relations` or the `relationsEnum` argument. @@ -85,9 +86,11 @@ which poses a risk to performance, security and type safety. ```graphql type Query { people( - where: _ @whereConditions( + where: _ + @whereConditions( columns: ["age", "type", "hair_color", "height"] - relations: ["friends", "friends.friends"]) + relations: ["friends", "friends.friends"] + ) ): [Person!]! @all } @@ -155,8 +158,8 @@ input QueryPeopleWhereWhereConditionsRelation { "Allowed relation names for Query.people.where." enum QueryPeopleWhereRelation { - FRIENDS @enum(value: "friends") - FRIENDS__FRIENDS @enum(value: "friends.friends") + FRIENDS @enum(value: "friends") + FRIENDS__FRIENDS @enum(value: "friends.friends") } ``` @@ -263,13 +266,7 @@ This query retrieves all persons that have at least 1 role. ```graphql { - people(where: { - HAS: { - relation: ROLE, - amount: 1, - operator: GTE - } - }) { + people(where: { HAS: { relation: ROLE, amount: 1, operator: GTE } }) { name } } @@ -286,19 +283,21 @@ you can also omit them: } ``` -You can also add additional +You can also add additional This filters people who have an access level of at least 5 through one of their roles: ```graphql { - people(where: { - HAS: { - relation: ROLE, - amount: 1, - operator: GTE, - condition: { column: ACCESS_LEVEL, operator: GTE, value: 5 } + people( + where: { + HAS: { + relation: ROLE + amount: 1 + operator: GTE + condition: { column: ACCESS_LEVEL, operator: GTE, value: 5 } + } } - }) { + ) { name } } From f953b5ca58db29f629df354b224e7e66498b741c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 26 Jan 2022 10:25:26 +0100 Subject: [PATCH 08/10] sync docs --- docs/5/eloquent/complex-where-conditions.md | 198 ++++++++------------ 1 file changed, 76 insertions(+), 122 deletions(-) diff --git a/docs/5/eloquent/complex-where-conditions.md b/docs/5/eloquent/complex-where-conditions.md index 756d41075a..c179a83cae 100644 --- a/docs/5/eloquent/complex-where-conditions.md +++ b/docs/5/eloquent/complex-where-conditions.md @@ -5,7 +5,7 @@ Adding query conditions ad-hoc can be cumbersome and limiting when you require manifold ways to filter query results. Lighthouse's `WhereConditions` extension can give advanced query capabilities to clients -and allow them to apply complex, dynamic WHERE conditions to queries. +and allow them to apply complex and dynamic filters. ## Setup @@ -19,14 +19,9 @@ Add the service provider to your `config/app.php`: Install the dependency [mll-lab/graphql-php-scalars](https://github.com/mll-lab/graphql-php-scalars): - composer require mll-lab/graphql-php-scalars:^4 + composer require mll-lab/graphql-php-scalars -## Usage - -You can use this feature through a set of schema directives that enhance fields -with advanced filter capabilities. - -### @whereConditions +## @whereConditions ```graphql """ @@ -47,6 +42,20 @@ directive @whereConditions( """ columnsEnum: String + """ + Restrict the allowed relation names to a well-defined enum. + This improves introspection capabilities and security. + Mutually exclusive with the `relationsEnum` argument. + """ + relations: [String!] + + """ + Use an existing enum type to restrict the allowed relations to a well-defined enum. + This allows you to re-use the same enum for multiple fields. + Mutually exclusive with the `relations` argument. + """ + relationsEnum: String + """ Reference a method that applies the client given conditions to the query builder. @@ -62,12 +71,26 @@ directive @whereConditions( ) on ARGUMENT_DEFINITION ``` -You can apply this directive on any field that performs an Eloquent query: +### Basic usage + +You can apply this directive on any field that performs an Eloquent query. + +It is recommended to use both: + +- the `columns` or the `columnsEnum` argument +- the `relations` or the `relationsEnum` argument. + +When you don't define allowed values, clients can specify arbitrary `String` values, +which poses a risk to performance, security and type safety. ```graphql type Query { people( - where: _ @whereConditions(columns: ["age", "type", "hair_color", "height"]) + where: _ + @whereConditions( + columns: ["age", "type", "hair_color", "height"] + relations: ["friends", "friends.friends"] + ) ): [Person!]! @all } @@ -77,16 +100,19 @@ type Person { height: Int! type: String! hair_color: String! + friends: [Person!]! @hasMany } ``` +### Generated code + Lighthouse automatically generates definitions for an `Enum` type and an `Input` type that are restricted to the defined columns, so you do not have to specify them by hand. The blank type named `_` will be changed to the actual type. Here are the types that will be included in the compiled schema: ```graphql -"Dynamic WHERE conditions for the `where` argument on the query `people`." +"Dynamic WHERE conditions for Query.people.where." input QueryPeopleWhereWhereConditions { "The column that is used for the condition." column: QueryPeopleWhereColumn @@ -107,18 +133,18 @@ input QueryPeopleWhereWhereConditions { HAS: QueryPeopleWhereWhereConditionsRelation } -"Allowed column names for the `where` argument on the query `people`." +"Allowed column names for Query.people.where." enum QueryPeopleWhereColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - hair_color @enum(value: "hair_color") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } -"Dynamic HAS conditions for WHERE condition queries." +"Dynamic HAS conditions for Query.people.where." input QueryPeopleWhereWhereConditionsRelation { "The relation that is checked." - relation: String! + relation: QueryPeopleWhereRelation! "The comparison operator to test against the amount." operator: SQLOperator = GTE @@ -129,10 +155,18 @@ input QueryPeopleWhereWhereConditionsRelation { "Additional condition logic." condition: QueryPeopleWhereWhereConditions } + +"Allowed relation names for Query.people.where." +enum QueryPeopleWhereRelation { + FRIENDS @enum(value: "friends") + FRIENDS__FRIENDS @enum(value: "friends.friends") +} ``` +### Reuse existing enum + Alternatively to the `columns` argument, you can also use `columnsEnum` in case you -want to re-use a list of allowed columns. Here's how your schema could look like: +want to re-use an enum of allowed columns. Here's how your schema could look like: ```graphql type Query { @@ -144,11 +178,11 @@ type Query { ): [Person!]! @paginated } -"A custom description for this custom enum." +"Filterable columns of Person." enum PersonColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - hair_color @enum(value: "hair_color") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -156,10 +190,9 @@ enum PersonColumn { Lighthouse will still automatically generate the necessary input types. Instead of creating enums for the allowed columns, it will simply use the existing `PersonColumn` enum. -It is recommended to either use the `columns` or the `columnsEnum` argument. -When you don't define any allowed columns, clients can specify arbitrary column names as a `String`. -This approach should by taken with care, as it carries -potential performance and security risks and offers little type safety. +The same works for `relationColumns`. + +### Example queries A simple query for a person who is exactly 42 years old would look like this: @@ -185,7 +218,7 @@ The following query gets actors over age 37 who either have red hair or are at l { column: TYPE, value: "Actor" } { OR: [ - { column: hair_color, value: "red" } + { column: HAIR_COLOR, value: "red" } { column: HEIGHT, operator: GTE, value: 150 } ] } @@ -205,7 +238,7 @@ query gets people that have no hair and blue-ish eyes: people( where: { AND: [ - { column: hair_color, operator: IS_NULL } + { column: HAIR_COLOR, operator: IS_NULL } { column: EYES, operator: IN, value: ["blue", "aqua", "turquoise"] } ] } @@ -226,124 +259,45 @@ This query would retrieve all persons without any condition: } ``` -### @whereHasConditions +### Has Relations -```graphql -""" -Allows clients to filter a query based on the existence of a related model, using -a dynamically controlled `WHERE` condition that applies to the relationship. -""" -directive @whereHasConditions( - """ - The Eloquent relationship that the conditions will be applied to. - - This argument can be omitted if the argument name follows the naming - convention `has{$RELATION}`. For example, if the Eloquent relationship - is named `posts`, the argument name must be `hasPosts`. - """ - relation: String - - """ - Restrict the allowed column names to a well-defined enum. - This improves introspection capabilities and security. - Mutually exclusive with the `columnsEnum` argument. - """ - columns: [String!] - - """ - Use an existing enum type to restrict the allowed columns to a well-defined enum. - This allows you to re-use the same enum for multiple fields. - Mutually exclusive with the `columns` argument. - """ - columnsEnum: String - - """ - Reference a method that applies the client given conditions to the query builder. - - Expected signature: `( - \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder, - array $whereConditions - ): void` - - Consists of two parts: a class name and a method name, separated by an `@` symbol. - If you pass only a class name, the method name defaults to `__invoke`. - """ - handler: String = "\\Nuwave\\Lighthouse\\WhereConditions\\WhereConditionsHandler" -) on ARGUMENT_DEFINITION -``` - -This directive works very similar to [@whereConditions](#whereconditions), except that -the conditions are applied to a relation sub query: - -```graphql -type Query { - people( - hasRole: _ @whereHasConditions(columns: ["name", "access_level"]) - ): [Person!]! @all -} - -type Role { - name: String! - access_level: Int -} -``` - -Again, Lighthouse will auto-generate an `input` and `enum` definition for your query: - -```graphql -"Dynamic WHERE conditions for the `hasRole` argument on the query `people`." -input QueryPeopleHasRoleWhereConditions { - "The column that is used for the condition." - column: QueryPeopleHasRoleColumn - - "The operator that is used for the condition." - operator: SQLOperator = EQ - - "The value that is used for the condition." - value: Mixed - - "A set of conditions that requires all conditions to match." - AND: [QueryPeopleHasRoleWhereConditions!] - - "A set of conditions that requires at least one condition to match." - OR: [QueryPeopleHasRoleWhereConditions!] -} - -"Allowed column names for the `hasRole` argument on the query `people`." -enum QueryPeopleHasRoleColumn { - NAME @enum(value: "name") - ACCESS_LEVEL @enum(value: "access_level") -} -``` - -A simple query for a person who has an access level of at least 5, through one of -their roles, looks like this: +Use the `HAS` clause to filter by relation existence. +This query retrieves all persons that have at least 1 role. ```graphql { - people(hasRole: { column: ACCESS_LEVEL, operator: GTE, value: 5 }) { + people(where: { HAS: { relation: ROLE, amount: 1, operator: GTE } }) { name } } ``` -You can also query for relationship existence without any condition; simply use an empty object as argument value. -This query would retrieve all persons that have a role: +The default values for `amount` and `operator` are included in the previous example, +you can also omit them: ```graphql { - people(hasRole: {}) { + people(where: { HAS: { relation: ROLE } }) { name } } ``` -Just like with the [@whereCondition](../api-reference/directives.md#whereconditions) directive, using `null` as argument value does not have any effect on the query. -This query would retrieve all persons, no matter if they have a role or not: +You can also add additional +This filters people who have an access level of at least 5 through one of their roles: ```graphql { - people(hasRole: null) { + people( + where: { + HAS: { + relation: ROLE + amount: 1 + operator: GTE + condition: { column: ACCESS_LEVEL, operator: GTE, value: 5 } + } + } + ) { name } } From 72d1f01f5f6d07fd5ada8cb54397b5d57f7739f2 Mon Sep 17 00:00:00 2001 From: spawnia Date: Wed, 26 Jan 2022 09:26:29 +0000 Subject: [PATCH 09/10] Normalize composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index f756063d8c..b434c6047d 100644 --- a/composer.json +++ b/composer.json @@ -79,8 +79,8 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/", - "Benchmarks\\": "benchmarks" + "Benchmarks\\": "benchmarks", + "Tests\\": "tests/" } }, "config": { From 01bd4254de4b18638ba403a4d3faa6941d4eb661 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 26 Jan 2022 10:27:49 +0100 Subject: [PATCH 10/10] fix v4 --- docs/4/eloquent/complex-where-conditions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/4/eloquent/complex-where-conditions.md b/docs/4/eloquent/complex-where-conditions.md index 688a07230d..7f89f74060 100644 --- a/docs/4/eloquent/complex-where-conditions.md +++ b/docs/4/eloquent/complex-where-conditions.md @@ -95,7 +95,7 @@ input PeopleWhereWhereConditions { enum PeopleWhereColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - hair_color @enum(value: "hair_color") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } ``` @@ -117,7 +117,7 @@ type Query { enum PersonColumn { AGE @enum(value: "age") TYPE @enum(value: "type") - hair_color @enum(value: "hair_color") + HAIR_COLOR @enum(value: "hair_color") HEIGHT @enum(value: "height") } ```