From 6fec0049e4e74443c72e8909768da35555560302 Mon Sep 17 00:00:00 2001 From: Adrian Philipp Date: Mon, 9 Apr 2018 12:37:04 +0200 Subject: [PATCH 01/31] Dump schema: Adds option to export schema with descriptions --- Command/GraphQLDumpSchemaCommand.php | 9 +- .../Command/GraphDumpSchemaCommandTest.php | 14 + .../Command/fixtures/schema.descriptions.json | 1391 +++++++++++++++++ 3 files changed, 1413 insertions(+), 1 deletion(-) create mode 100644 Tests/Functional/Command/fixtures/schema.descriptions.json diff --git a/Command/GraphQLDumpSchemaCommand.php b/Command/GraphQLDumpSchemaCommand.php index 3372d0e9a..e84c63d33 100644 --- a/Command/GraphQLDumpSchemaCommand.php +++ b/Command/GraphQLDumpSchemaCommand.php @@ -60,6 +60,12 @@ protected function configure() InputOption::VALUE_NONE, 'Enabled classic json format: { "__schema": {...} }.' ) + ->addOption( + 'with-descriptions', + null, + InputOption::VALUE_NONE, + 'Dump schema including descriptions.' + ) ; } @@ -74,13 +80,14 @@ private function createFile(InputInterface $input) { $format = strtolower($input->getOption('format')); $schemaName = $input->getOption('schema'); + $includeDescription = $input->getOption('with-descriptions'); $file = $input->getOption('file') ?: $this->baseExportPath.sprintf('/../var/schema%s.%s', $schemaName ? '.'.$schemaName : '', $format); switch ($format) { case 'json': $request = [ - 'query' => Introspection::getIntrospectionQuery(false), + 'query' => Introspection::getIntrospectionQuery($includeDescription), 'variables' => [], 'operationName' => null, ]; diff --git a/Tests/Functional/Command/GraphDumpSchemaCommandTest.php b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php index 0d597b09c..1bb3933b7 100644 --- a/Tests/Functional/Command/GraphDumpSchemaCommandTest.php +++ b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php @@ -51,6 +51,20 @@ public function testDump($format, $withFormatOption = true) ); } + public function testDumpWithDescriptions() + { + $file = $this->cacheDir.'/schema.json'; + $this->assertCommandExecution( + [ + '--file' => $file, + '--with-descriptions' => true, + ], + __DIR__.'/fixtures/schema.descriptions.json', + $file, + 'json' + ); + } + public function testClassicJsonFormat() { $file = $this->cacheDir.'/schema.json'; diff --git a/Tests/Functional/Command/fixtures/schema.descriptions.json b/Tests/Functional/Command/fixtures/schema.descriptions.json new file mode 100644 index 000000000..63d265259 --- /dev/null +++ b/Tests/Functional/Command/fixtures/schema.descriptions.json @@ -0,0 +1,1391 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "user", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": null, + "fields": [ + { + "name": "name", + "description": "the user name", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friends", + "description": null, + "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "friendConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friendsForward", + "description": null, + "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "userConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friendsBackward", + "description": null, + "args": [ + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "userConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8\ncharacter sequences. The String type is most often used by GraphQL to\nrepresent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric\nvalues. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "friendConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "totalCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "friendEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "Information about pagination in a connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "friendEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "friendshipTime", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "userConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "userEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "userEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to\nrefetch an object or as key for a cache. The ID type appears in a JSON\nresponse as a String; however, it is not intended to be human-readable.\nWhen expected as an input type, any string (such as `\"4\"`) or integer\n(such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional\nvalues as specified by\n[IEEE 754](http:\/\/en.wikipedia.org\/wiki\/IEEE_floating_point). ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https:\/\/daringfireball.net\/projects\/markdown\/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ] + } + ] + } +} From 50f6b06833af4aa298a595160da3cf1e339edd0d Mon Sep 17 00:00:00 2001 From: Maxime COLIN Date: Tue, 10 Apr 2018 15:08:09 +0200 Subject: [PATCH 02/31] Update custom exception mapping doc --- Resources/doc/error-handling/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/error-handling/index.md b/Resources/doc/error-handling/index.md index a6159fbba..67ef8023e 100644 --- a/Resources/doc/error-handling/index.md +++ b/Resources/doc/error-handling/index.md @@ -95,7 +95,7 @@ define a custom exception mapping: ```yaml overblog_graphql: #... - definitions: + errors_handler: #... # change to true to try to map an exception to a parent exception if the exact exception is not in # the mapping From b18e0b75156bbc7de2e120e452b0f0fef89d1182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Tainon?= Date: Sun, 15 Apr 2018 15:01:59 +0200 Subject: [PATCH 03/31] Fix minor typo in documentation --- Resources/doc/definitions/resolver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index d926ebde4..271337c66 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -135,7 +135,7 @@ Resolvers can be define 2 different ways **Note:** * When using service id as FQCN in yaml definition, backslashes must be correctly escaped, - here an example `'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args['name']])'`. + here an example `'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args["name"]])'`. * You can also see the more straight forward way using [resolver map](resolver-map.md) 2. **The service way** From 60eec0cb9a103fd4561a0c75e58c369175e4e955 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 17 Apr 2018 20:05:19 +0200 Subject: [PATCH 04/31] Enum type --- Resources/doc/definitions/type-system/enum.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Resources/doc/definitions/type-system/enum.md b/Resources/doc/definitions/type-system/enum.md index fc089a688..2225c889d 100644 --- a/Resources/doc/definitions/type-system/enum.md +++ b/Resources/doc/definitions/type-system/enum.md @@ -20,7 +20,6 @@ Episode: value: 5 description: "Released in 1980." JEDI: 6 # using the short syntax (JEDI value equal to 6) -# in this case FORCEAWAKENS value = FORCEAWAKENS -# FORCEAWAKENS: -# description: "Released in 2015." + FORCEAWAKENS: # in this case FORCEAWAKENS value = FORCEAWAKENS + description: "Released in 2015." ``` From 9e8fdb9af681431cd3518df7d63a884d6837f4f4 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 17 Apr 2018 20:05:33 +0200 Subject: [PATCH 05/31] Update enum.md --- Resources/doc/definitions/type-system/enum.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/definitions/type-system/enum.md b/Resources/doc/definitions/type-system/enum.md index 2225c889d..ae8f1986d 100644 --- a/Resources/doc/definitions/type-system/enum.md +++ b/Resources/doc/definitions/type-system/enum.md @@ -5,7 +5,7 @@ Enum # MyBundle/Resources/config/graphql/Episode.types.yml # The original trilogy consists of three movies. # This implements the following type system shorthand: -# enum Episode { NEWHOPE, EMPIRE, JEDI } +# enum Episode { NEWHOPE, EMPIRE, JEDI, FORCEAWAKENS } Episode: type: enum config: From 2d8393a4aabfaed4cefc7d8fc9f9b4ccfc40b549 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 19 Apr 2018 19:06:17 +0200 Subject: [PATCH 06/31] resolver.md --- Resources/doc/definitions/resolver.md | 380 ++++++++++++++------------ 1 file changed, 204 insertions(+), 176 deletions(-) diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index 271337c66..2818d1a2c 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -1,3 +1,5 @@ + + # Resolver To ease development we named 2 types of resolver: @@ -7,181 +9,207 @@ To ease development we named 2 types of resolver: This is just a recommendation. -Resolvers can be define 2 different ways - -1. **The PHP way** - - You can declare a resolver (any class that implements `Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface` - or `Overblog\GraphQLBundle\Definition\Resolver\MutationInterface`) - in `src/*Bundle/GraphQL` or `app/GraphQL` and they will be auto discovered. - Auto map classes method are accessible by: - * double-colon (::) to separate service id (class name) and the method names - (example: `AppBunble\GraphQL\CustomResolver::myMethod`) - * for callable classes you can use the service id (example: `AppBunble\GraphQL\InvokeResolver` for a resolver implementing the `__invoke` method) - you can also alias a type by implementing `Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface` - which returns a map of method/alias. The service created will autowire the `__construct` - and `Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer` methods. - - Here an example: - ```php - 'say_hello']; - } - } - ``` - - `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or - `say_hello` alias. - - we can also use class invoker: - ```php - value += $number; - } - - /** - * {@inheritdoc} - */ - public static function getAliases() - { - return ['addition' => 'add']; - } - } - ``` - `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or - `add` alias. - - You can also define custom dirs using the config (Symfony <3.3): - ```yaml - overblog_graphql: - definitions: - auto_mapping: - directories: - - "%kernel.root_dir%/src/*Bundle/CustomDir" - - "%kernel.root_dir%/src/AppBundle/{foo,bar}" - ``` - - If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native - DI `autoconfigure`: - - ```yaml - overblog_graphql: - definitions: - auto_mapping: false - ``` - - Here an example of how this can be done with DI `autoconfigure`: - - ```yaml - services: - App\Mutation\: - resource: '../src/Mutation' - tags: ['overblog_graphql.mutation'] - - App\Resolver\: - resource: '../src/Resolver' - tags: ['overblog_graphql.resolver'] - ``` - - **Note:** - * When using service id as FQCN in yaml definition, backslashes must be correctly escaped, - here an example `'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args["name"]])'`. - * You can also see the more straight forward way using [resolver map](resolver-map.md) - -2. **The service way** - - Creating a service tagged `overblog_graphql.resolver` for resolvers - or `overblog_graphql.mutation` for mutations. - - Using the php way examples: - - ```yaml - services: - App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello - - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" - ``` - - `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or - `say_hello` alias. - - for invokable classes no need to use `alias` and `method` attributes: - - ```yaml - services: - App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver } - ``` - - This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. - - for mutation: - - ```yaml - services: - App\GraphQL\Mutation\CalcMutation: - # only for sf < 3.3 - #class: App\GraphQL\Mutation\CalcMutation - tags: - - { name: overblog_graphql.mutation, method: addition, alias: add } - ``` - `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or - `add` alias. +Resolvers can be define 2 different ways: + +## The PHP way + + +You can declare a resolver (any class that implements `Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface` or `Overblog\GraphQLBundle\Definition\Resolver\MutationInterface`) in `src/*Bundle/GraphQL` or `app/GraphQL` and they will be auto discovered. +Auto map classes method are accessible by: +* double-colon (::) to separate service id (class name) and the method names +(example: `AppBunble\GraphQL\CustomResolver::myMethod`) +* for callable classes you can use the service id (example: `AppBunble\GraphQL\InvokeResolver` for a resolver implementing the `__invoke` method) you can also alias a type by implementing `Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface` which returns a map of method/alias. The service created will autowire the `__construct` and `Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer` methods. + +**Note:** +* When using service id as FQCN in yaml definition, backslashes must be correctly escaped, here an example: +`'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args["name"]])'`. +* You can also see the more straight forward way using [resolver map](resolver-map.md). + +### Resolver + +Example using an alias: +````yaml +resolve: '@=resolver("say_hello", [args["name"]])' +```` + +```php + 'say_hello']; + } +} +```` + +Example using a fully qualified method name: +````yaml +resolve: '@=resolver("App\\GraphQL\\Resolver\\Greetings::sayHello", [args["name"]])' +```` + +Note: backslashes must be correctly escaped and respect the use of single and double quotes. + +```php +value += $number; + } + + /** + * {@inheritdoc} + */ + public static function getAliases() + { + return ['addition' => 'add']; + } +} +``` +`addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or +`add` alias. + +You can also define custom dirs using the config (Symfony <3.3): +```yaml +overblog_graphql: +definitions: + auto_mapping: + directories: + - "%kernel.root_dir%/src/*Bundle/CustomDir" + - "%kernel.root_dir%/src/AppBundle/{foo,bar}" +``` + +If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native +DI `autoconfigure`: + +```yaml +overblog_graphql: +definitions: + auto_mapping: false +``` + +Here an example of how this can be done with DI `autoconfigure`: + +```yaml +services: +App\Mutation\: + resource: '../src/Mutation' + tags: ['overblog_graphql.mutation'] + +App\Resolver\: + resource: '../src/Resolver' + tags: ['overblog_graphql.resolver'] +``` + +## The service way + +Creating a service tagged `overblog_graphql.resolver` for resolvers +or `overblog_graphql.mutation` for mutations. + +Using the php way examples: + +```yaml +services: +App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello + - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" +``` + +`SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or +`say_hello` alias. + +for invokable classes no need to use `alias` and `method` attributes: + +```yaml +services: +App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver } +``` + +This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. + +for mutation: + +```yaml +services: +App\GraphQL\Mutation\CalcMutation: + # only for sf < 3.3 + #class: App\GraphQL\Mutation\CalcMutation + tags: + - { name: overblog_graphql.mutation, method: addition, alias: add } +``` +`addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or +`add` alias. Next step [solving N+1 problem](solving-n-plus-1-problem.md) From c977abd5115c7ad4471a2567cae4458861df139b Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 19 Apr 2018 19:06:42 +0200 Subject: [PATCH 07/31] resolver.md --- Resources/doc/definitions/resolver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index 2818d1a2c..289256955 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -44,7 +44,7 @@ class Greetings implements ResolverInterface, AliasedInterface { public function sayHello($name) { - return sprintf('hello %s!!!', $name); + return sprintf('hello %s!!!', $name); } /** From b1e3f819670a0988a671b0c93efb42f95657b43f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 19 Apr 2018 19:09:13 +0200 Subject: [PATCH 08/31] resolver.md --- Resources/doc/definitions/resolver.md | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index 289256955..95d61e4d7 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -138,10 +138,10 @@ You can also define custom dirs using the config (Symfony <3.3): ```yaml overblog_graphql: definitions: - auto_mapping: - directories: - - "%kernel.root_dir%/src/*Bundle/CustomDir" - - "%kernel.root_dir%/src/AppBundle/{foo,bar}" + auto_mapping: + directories: + - "%kernel.root_dir%/src/*Bundle/CustomDir" + - "%kernel.root_dir%/src/AppBundle/{foo,bar}" ``` If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native @@ -150,7 +150,7 @@ DI `autoconfigure`: ```yaml overblog_graphql: definitions: - auto_mapping: false + auto_mapping: false ``` Here an example of how this can be done with DI `autoconfigure`: @@ -158,12 +158,12 @@ Here an example of how this can be done with DI `autoconfigure`: ```yaml services: App\Mutation\: - resource: '../src/Mutation' - tags: ['overblog_graphql.mutation'] + resource: '../src/Mutation' + tags: ['overblog_graphql.mutation'] App\Resolver\: - resource: '../src/Resolver' - tags: ['overblog_graphql.resolver'] + resource: '../src/Resolver' + tags: ['overblog_graphql.resolver'] ``` ## The service way @@ -176,11 +176,11 @@ Using the php way examples: ```yaml services: App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello - - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello + - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" ``` `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or @@ -191,10 +191,10 @@ for invokable classes no need to use `alias` and `method` attributes: ```yaml services: App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver } + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver } ``` This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. @@ -204,10 +204,10 @@ for mutation: ```yaml services: App\GraphQL\Mutation\CalcMutation: - # only for sf < 3.3 - #class: App\GraphQL\Mutation\CalcMutation - tags: - - { name: overblog_graphql.mutation, method: addition, alias: add } + # only for sf < 3.3 + #class: App\GraphQL\Mutation\CalcMutation + tags: + - { name: overblog_graphql.mutation, method: addition, alias: add } ``` `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or `add` alias. From cf4697c38e2cf4241c95c841b13e262c909a24e8 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 19 Apr 2018 19:22:44 +0200 Subject: [PATCH 09/31] relay-paginator.md --- Resources/doc/helpers/relay-paginator.md | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Resources/doc/helpers/relay-paginator.md b/Resources/doc/helpers/relay-paginator.md index 8e606faa4..c1551ad36 100644 --- a/Resources/doc/helpers/relay-paginator.md +++ b/Resources/doc/helpers/relay-paginator.md @@ -174,6 +174,11 @@ class DataBackend { return count($array); } + + public function countAll() + { + return count($this->data); + } } $backend = new DataBackend(); @@ -195,6 +200,50 @@ $result = $paginator->backward( You should get the 4 last items of the _data set_. +#### Within a resolver + +````yaml +resolve: '@=resolver("App\\GraphQL\\Resolver\\Greetings::sayHello", [args])' +```` + +```` +sayHello(first: 1, after: "YXJyYXljb25uZWN0aW9uOjI="){ # after: base64_encode('arrayconnection:2') + edges { + cursor # YXJyYXljb25uZWN0aW9uOjM= + node # D + } + pageInfo { + hasNextPage # true + } +} +```` + +````php +getData($offset, $limit); + }); + + return $paginator->auto($args, function() use ($backend) { + return $backend->countAll(); + }); + } +} +```` + #### Promise handling Paginator also supports promises if you [use that feature](https://github.com/webonyx/graphql-php/pull/67) From 9b48a77080c0d3a4296cc8a091b1e006c29959fe Mon Sep 17 00:00:00 2001 From: Yuri Mironenko Date: Fri, 20 Apr 2018 14:01:40 +0300 Subject: [PATCH 10/31] Change graphql format --- Resources/doc/definitions/type-system/input-object.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/definitions/type-system/input-object.md b/Resources/doc/definitions/type-system/input-object.md index 10280b627..edba9d5b6 100644 --- a/Resources/doc/definitions/type-system/input-object.md +++ b/Resources/doc/definitions/type-system/input-object.md @@ -5,7 +5,7 @@ Input object # src/MyBundle/Resources/config/graphql/HumanAndDroid.types.yml # # This implements the following type system shorthand: -# type HeroInput { +# input HeroInput { # name: Episode! # } HeroInput: From 47f54b1daf55a24c61af5e2a5fee51981c1f9289 Mon Sep 17 00:00:00 2001 From: Michal Zdrojewski Date: Wed, 25 Apr 2018 14:39:01 +0100 Subject: [PATCH 11/31] Fixed typos and changed the wording slightly. --- Resources/doc/definitions/expression-language.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/doc/definitions/expression-language.md b/Resources/doc/definitions/expression-language.md index f1c918779..4e852a310 100644 --- a/Resources/doc/definitions/expression-language.md +++ b/Resources/doc/definitions/expression-language.md @@ -1,7 +1,7 @@ Expression language =================== -All definitions configs entries can use expression language but it must be explicitly triggered using "@=" like prefix. +All definition config entries can use expression language but it must be explicitly triggered using "@=" like prefix. **Functions description:** @@ -43,11 +43,11 @@ Expression | Description | Scope Custom expression function -------------------------- -Custom expression function is easy as creating a tagged service. -Adding useful expression function can help user create simple resolver without having to leave config file, -this also improve performance by removing a useless external resolver call. +Adding custom expression function is easy since all you need to do is create a tagged service. +Expression functions can help user create simple resolver without having to leave config file, +this also improves performance by removing a useless external resolver call. -Here an example to add an custom expression equivalent to php `json_decode`: +Here is an example to add a custom expression equivalent to php `json_decode`: ```php Date: Wed, 9 May 2018 13:21:23 +0200 Subject: [PATCH 12/31] User Deprecated: Referencing controllers with a single colon is deprecated since Symfony 4.1 --- Resources/config/routing/graphql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Resources/config/routing/graphql.yml b/Resources/config/routing/graphql.yml index daeda1615..a0b1f1ce1 100644 --- a/Resources/config/routing/graphql.yml +++ b/Resources/config/routing/graphql.yml @@ -1,23 +1,23 @@ overblog_graphql_endpoint: path: / defaults: - _controller: overblog_graphql.controller.graphql:endpointAction + _controller: overblog_graphql.controller.graphql::endpointAction _format: "json" overblog_graphql_batch_endpoint: path: /batch defaults: - _controller: overblog_graphql.controller.graphql:batchEndpointAction + _controller: overblog_graphql.controller.graphql::batchEndpointAction _format: "json" overblog_graphql_multiple_endpoint: path: /graphql/{schemaName} defaults: - _controller: overblog_graphql.controller.graphql:endpointAction + _controller: overblog_graphql.controller.graphql::endpointAction _format: "json" overblog_graphql_batch_multiple_endpoint: path: /graphql/{schemaName}/batch defaults: - _controller: overblog_graphql.controller.graphql:batchEndpointAction + _controller: overblog_graphql.controller.graphql::batchEndpointAction _format: "json" From daefb71e84cb9778aabe34e98b5271e8c745a7b8 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Wed, 9 May 2018 14:13:38 +0200 Subject: [PATCH 13/31] Disabled http_exception_listener Fix tests by removing `http_exception_listener` because it flatten exception to html response --- Tests/Functional/App/TestKernel.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Tests/Functional/App/TestKernel.php b/Tests/Functional/App/TestKernel.php index f5e906f62..16863d2fc 100644 --- a/Tests/Functional/App/TestKernel.php +++ b/Tests/Functional/App/TestKernel.php @@ -16,9 +16,11 @@ use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; -final class TestKernel extends Kernel +final class TestKernel extends Kernel implements CompilerPassInterface { private $testCase; @@ -71,5 +73,20 @@ public function registerContainerConfiguration(LoaderInterface $loader) } else { $loader->load(__DIR__.'/config/config.yml'); } + + $loader->load(function (ContainerBuilder $container) { + $container->addCompilerPass($this); + }); + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + // disabled http_exception_listener because it flatten exception to html response + if ($container->has('http_exception_listener')) { + $container->removeDefinition('http_exception_listener'); + } } } From 0972d9a373574527611fee990d8711be4790d3b3 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Fri, 11 May 2018 07:17:17 +0200 Subject: [PATCH 14/31] Improve documentation (#327) Object access control --- README.md | 1 + .../doc/security/fields-access-control.md | 12 ++++++- Resources/doc/security/index.md | 1 + .../doc/security/object-access-control.md | 33 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 Resources/doc/security/object-access-control.md diff --git a/README.md b/README.md index f951b0bf1..b861cf6ff 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Documentation - [Promise](Resources/doc/data-fetching/promise.md) - [Security](Resources/doc/security/index.md) - [Handle CORS](Resources/doc/security/handle-cors.md) + - [Object access control](Resources/doc/security/object-access-control.md) - [Fields access control](Resources/doc/security/fields-access-control.md) - [Fields public control](Resources/doc/security/fields-public-control.md) - [Limiting query depth](Resources/doc/security/limiting-query-depth.md) diff --git a/Resources/doc/security/fields-access-control.md b/Resources/doc/security/fields-access-control.md index 523234dc5..7949edbfb 100644 --- a/Resources/doc/security/fields-access-control.md +++ b/Resources/doc/security/fields-access-control.md @@ -1,10 +1,14 @@ Fields access Control ====================== -An access control can be add on each field using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. +An access control can be added on each field using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. If `config.fields.*.access` value is true field will be normally resolved but will be `null` otherwise. Act like access is`true` if not set. +Note: +- in query mode: execute resolver -> execute access -> manage result in function of access +- in mutation mode: execute access -> execute resolver if access result is true + In the example below the Human name is available only for authenticated users. ```yaml @@ -32,3 +36,9 @@ Human: description: "The home planet of the human, or null if unknown." interfaces: [Character] ``` + +Performance +----------- +Checking access on each field can be a performance issue and may be dealt with using: +- using a custom cache to do the check only once +- using [Object access control](object-access-control.md) diff --git a/Resources/doc/security/index.md b/Resources/doc/security/index.md index be362d504..b49af5486 100644 --- a/Resources/doc/security/index.md +++ b/Resources/doc/security/index.md @@ -2,6 +2,7 @@ Security ======== * [Handle CORS](handle-cors.md) +* [Object access control](object-access-control.md) * [Fields access control](fields-access-control.md) * [Fields public control](fields-public-control.md) * [Limiting query depth](limiting-query-depth.md) diff --git a/Resources/doc/security/object-access-control.md b/Resources/doc/security/object-access-control.md new file mode 100644 index 000000000..fc670a588 --- /dev/null +++ b/Resources/doc/security/object-access-control.md @@ -0,0 +1,33 @@ +Object access Control +====================== + +If your GraphQL schema have multiple paths to the same resolver, you may end up with duplicated access control on the different fields leading to this resolver. + +An access control can be added on the whole object using a decorator type for this protected field and make every parent extend this type. + + +An access control can be added on each field using `config.fields.*.access` or globally with `config.fieldsDefaultAccess`. +If `config.fields.*.access` value is true field will be normally resolved but will be `null` otherwise. +Act like access is`true` if not set. + +In the example below the user field protection is set by the decorator: + +```yaml +ProtectedUser: + type: object + decorator: true + config: + fields: + user: {type: User, access: '@=isAuthenticated()'} + +Foo: + type: object + inherits: [ProtectedUser] + config: + fields: + other: {type: String!} + +Bar: + type: object + inherits: [ProtectedUser] +``` From 843a9d7b108c30a803c75d20e3906035533a213d Mon Sep 17 00:00:00 2001 From: Xavier HAUSHERR Date: Mon, 14 May 2018 11:04:52 +0200 Subject: [PATCH 15/31] Update graphql.yml --- Resources/config/routing/graphql.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Resources/config/routing/graphql.yml b/Resources/config/routing/graphql.yml index a0b1f1ce1..7a88f5a5f 100644 --- a/Resources/config/routing/graphql.yml +++ b/Resources/config/routing/graphql.yml @@ -1,23 +1,23 @@ overblog_graphql_endpoint: path: / defaults: - _controller: overblog_graphql.controller.graphql::endpointAction + _controller: Overblog\GraphQLBundle\Controller\GraphController::endpointAction _format: "json" overblog_graphql_batch_endpoint: path: /batch defaults: - _controller: overblog_graphql.controller.graphql::batchEndpointAction + _controller: Overblog\GraphQLBundle\Controller\GraphController::batchEndpointAction _format: "json" overblog_graphql_multiple_endpoint: path: /graphql/{schemaName} defaults: - _controller: overblog_graphql.controller.graphql::endpointAction + _controller: Overblog\GraphQLBundle\Controller\GraphController::endpointAction _format: "json" overblog_graphql_batch_multiple_endpoint: path: /graphql/{schemaName}/batch defaults: - _controller: overblog_graphql.controller.graphql::batchEndpointAction + _controller: Overblog\GraphQLBundle\Controller\GraphController::batchEndpointAction _format: "json" From ede385dadd35f5818bc5b06171b103d9bf82ea4c Mon Sep 17 00:00:00 2001 From: Xavier HAUSHERR Date: Mon, 14 May 2018 11:27:11 +0200 Subject: [PATCH 16/31] Update services.yml --- Resources/config/services.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Resources/config/services.yml b/Resources/config/services.yml index e8e1d307f..fc921aa6d 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -119,6 +119,10 @@ services: - "@overblog_graphql.request_parser" - "%overblog_graphql.handle_cors%" - "%overblog_graphql.batching_method%" + + Overblog\GraphQLBundle\Controller\GraphController: + public: true + alias: 'overblog_graphql.controller.graphql' overblog_graphql.command.dump_schema: class: Overblog\GraphQLBundle\Command\GraphQLDumpSchemaCommand From 2a74fd6dc1e790bb92e2df45af344d8b423f76ee Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Tue, 22 May 2018 07:49:50 +0200 Subject: [PATCH 17/31] Add a slide add the slide GraphQL in Symfony *by Bernd Alter* --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b861cf6ff..dc0cf1eac 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Documentation Talks and slides to help you start ---------------------------------- +* GraphQL in Symfony *by Bernd Alter* - [Twitter](https://twitter.com/bazoo0815) + - [Talk about GraphQL and its implementation with Symfony (26.04.2017)](https://www.slideshare.net/berndalter7/graphql-in-symfony) `English` * GraphQL is right in front of us, let's to it! *by Renato Mendes Figueiredo* - [Twitter](https://twitter.com/renatomefi), [GitHub](https://github.com/renatomefi) - [Slides at http://talks.mefi.in/graphql-scotphp17](http://talks.mefi.in/graphql-scotphp17/) `English` - [Video at SymfonyCamp UA 2017](https://www.youtube.com/watch?v=jyoYlnCPNgk) `English` From ed16cb63ad4daf4c83073e29230eaa57d79df3ad Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 23 May 2018 09:44:39 +0200 Subject: [PATCH 18/31] Example with a PHP constant --- Resources/doc/definitions/type-system/enum.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Resources/doc/definitions/type-system/enum.md b/Resources/doc/definitions/type-system/enum.md index ae8f1986d..66d1d3172 100644 --- a/Resources/doc/definitions/type-system/enum.md +++ b/Resources/doc/definitions/type-system/enum.md @@ -17,7 +17,8 @@ Episode: # to deprecate a value, only set the deprecation reason #deprecationReason: "Just because" EMPIRE: - value: 5 + # We can use a PHP constant to avoid a magic number + value: '@=constant("App\\StarWars\\Movies::MOVIE_EMPIRE")' description: "Released in 1980." JEDI: 6 # using the short syntax (JEDI value equal to 6) FORCEAWAKENS: # in this case FORCEAWAKENS value = FORCEAWAKENS From 03b41ee967f738e10de9ed4481f7d7fc68fedf19 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 23 May 2018 09:44:39 +0200 Subject: [PATCH 19/31] Example with a PHP constant From 5c8623abe740cfde563ffdc5e532e90b24a960f0 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Mon, 28 May 2018 10:12:13 +0200 Subject: [PATCH 20/31] Fix tests with symfony 4.1 beta --- .travis.yml | 12 +++---- .../OverblogGraphQLTypesExtension.php | 35 +++++++++++++------ .../OverblogGraphQLTypesExtensionTest.php | 8 ++--- Tests/Functional/App/TestKernel.php | 2 -- Tests/Functional/App/config/access/config.yml | 1 - Tests/Functional/App/config/config.yml | 22 ++++++++++-- Tests/Functional/App/config/public/config.yml | 1 - .../App/config/queryComplexityEnv/config.yml | 2 +- .../App/config/queryMaxDepthEnv/config.yml | 2 +- Tests/Functional/App/config/security.yml | 19 ---------- .../Functional/Command/CompileCommandTest.php | 2 +- composer.json | 1 - phpunit.xml.dist | 6 ++-- 13 files changed, 61 insertions(+), 52 deletions(-) delete mode 100644 Tests/Functional/App/config/security.yml diff --git a/.travis.yml b/.travis.yml index 958a5c769..1e26a4c47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - php: 5.6 env: SYMFONY_VERSION=3.1.* SYMFONY_DEPRECATIONS_HELPER=disabled - php: 7.0 - env: SYMFONY_VERSION=3.2.* + env: SYMFONY_VERSION=3.2.* PHPUNIT_VERSION=^5.7.26 - php: 7.1 - php: 7.2 env: PHP_CS_FIXER=true @@ -30,12 +30,10 @@ matrix: - php: 7.2 env: SYMFONY_VERSION=4.0.* TEST_COVERAGE=true - php: 7.2 - env: SYMFONY_VERSION=4.1.* DEPENDENCIES=dev + env: SYMFONY_VERSION=4.1.* STABILITY=beta - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs allow_failures: - - php: 7.2 - env: SYMFONY_VERSION=4.1.* DEPENDENCIES=dev - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs @@ -46,15 +44,17 @@ cache: - vendor/ before_install: - - if [ ${DEPENDENCIES} = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi; + - if [ "${STABILITY}" != "" ]; then perl -pi -e 's/^}$/,"minimum-stability":"'"${STABILITY}"'"}/' composer.json; fi; + - if [ "${SYMFONY_VERSION}" != "" ]; then perl -pi -e 's#"(symfony/.*)":\s*".*"#"$1":"'"${SYMFONY_VERSION}"'"#' composer.json; fi; + - if [ "${PHPUNIT_VERSION}" != "" ]; then composer req "phpunit/phpunit:${PHPUNIT_VERSION}" --dev --no-update; fi; - if [ ${TEST_COVERAGE} != true ]; then phpenv config-rm xdebug.ini || true; fi - composer selfupdate - - if [ $SYMFONY_VERSION ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --dev --no-update; fi; - if [ $GRAPHQLPHP_VERSION ]; then composer require "webonyx/graphql-php:${GRAPHQLPHP_VERSION}" --dev --no-update; fi; install: composer update --prefer-source --no-interaction --optimize-autoloader ${COMPOSER_UPDATE_FLAGS} script: + - composer validate --no-check-lock --strict - bin/phpunit --debug $( if [ $TEST_COVERAGE = true ]; then echo "-d xdebug.max_nesting_level=1000 --coverage-clover=build/logs/clover.xml"; fi; ) - if [ ${PHP_CS_FIXER} = true ]; then composer require --dev 'friendsofphp/php-cs-fixer:^2.0' && bin/php-cs-fixer fix --diff --dry-run -v; fi; diff --git a/DependencyInjection/OverblogGraphQLTypesExtension.php b/DependencyInjection/OverblogGraphQLTypesExtension.php index 3c46a047e..350e69532 100644 --- a/DependencyInjection/OverblogGraphQLTypesExtension.php +++ b/DependencyInjection/OverblogGraphQLTypesExtension.php @@ -41,44 +41,57 @@ class OverblogGraphQLTypesExtension extends Extension public function load(array $configs, ContainerBuilder $container) { - $this->checkTypesDuplication($configs); - // flatten config is a requirement to support inheritance - $flattenConfig = [call_user_func_array('array_merge', $configs)]; - $configuration = $this->getConfiguration($flattenConfig, $container); - $config = $this->processConfiguration($configuration, $flattenConfig); + $configs = array_filter($configs); + //$configs = array_filter($configs); + if (count($configs) > 1) { + throw new \InvalidArgumentException('Configs type should never contain more than one config to deal with inheritance.'); + } + $configuration = $this->getConfiguration($configs, $container); + $config = $this->processConfiguration($configuration, $configs); $container->setParameter($this->getAlias().'.config', $config); } - public function containerPrependExtensionConfig(array $config, ContainerBuilder $container) + public function containerPrependExtensionConfig(array $configs, ContainerBuilder $container) { - $typesMappings = $this->mappingConfig($config, $container); + $typesMappings = $this->mappingConfig($configs, $container); // reset treated files $this->treatedFiles = []; $typesMappings = call_user_func_array('array_merge', $typesMappings); + $typeConfigs = []; // treats mappings foreach ($typesMappings as $params) { - $this->prependExtensionConfigFromFiles($params['type'], $params['files'], $container); + $typeConfigs = array_merge($typeConfigs, $this->parseTypeConfigFiles($params['type'], $params['files'], $container)); } + + $this->checkTypesDuplication($typeConfigs); + // flatten config is a requirement to support inheritance + $flattenTypeConfig = call_user_func_array('array_merge', $typeConfigs); + + $container->prependExtensionConfig($this->getAlias(), $flattenTypeConfig); } /** * @param $type * @param SplFileInfo[] $files * @param ContainerBuilder $container + * + * @return array */ - private function prependExtensionConfigFromFiles($type, $files, ContainerBuilder $container) + private function parseTypeConfigFiles($type, $files, ContainerBuilder $container) { + $config = []; foreach ($files as $file) { $fileRealPath = $file->getRealPath(); if (isset($this->treatedFiles[$fileRealPath])) { continue; } - $typeConfig = call_user_func(self::PARSERS[$type].'::parse', $file, $container); - $container->prependExtensionConfig($this->getAlias(), $typeConfig); + $config[] = call_user_func(self::PARSERS[$type].'::parse', $file, $container); $this->treatedFiles[$file->getRealPath()] = true; } + + return $config; } private function checkTypesDuplication(array $typeConfigs) diff --git a/Tests/DependencyInjection/OverblogGraphQLTypesExtensionTest.php b/Tests/DependencyInjection/OverblogGraphQLTypesExtensionTest.php index f1ea2583e..929cdcae2 100644 --- a/Tests/DependencyInjection/OverblogGraphQLTypesExtensionTest.php +++ b/Tests/DependencyInjection/OverblogGraphQLTypesExtensionTest.php @@ -35,12 +35,12 @@ public function tearDown() } /** - * @expectedException \Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Configs type should never contain more than one config to deal with inheritance. */ - public function testDuplicatedType() + public function testMultipleConfigNotAllowed() { - $type = ['foo' => []]; - $configs = [$type, $type]; + $configs = [['foo' => []], ['bar' => []]]; $this->extension->load($configs, $this->container); } diff --git a/Tests/Functional/App/TestKernel.php b/Tests/Functional/App/TestKernel.php index a226f0f6b..f40fc357f 100644 --- a/Tests/Functional/App/TestKernel.php +++ b/Tests/Functional/App/TestKernel.php @@ -5,7 +5,6 @@ use Overblog\GraphQLBundle\OverblogGraphQLBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -23,7 +22,6 @@ public function registerBundles() return [ new FrameworkBundle(), new SecurityBundle(), - new TwigBundle(), new OverblogGraphQLBundle(), ]; } diff --git a/Tests/Functional/App/config/access/config.yml b/Tests/Functional/App/config/access/config.yml index b9158fe75..bc6af7294 100644 --- a/Tests/Functional/App/config/access/config.yml +++ b/Tests/Functional/App/config/access/config.yml @@ -1,6 +1,5 @@ imports: - { resource: ../config.yml } - - { resource: ../security.yml } - { resource: ../connection/services.yml } - { resource: ../mutation/services.yml } diff --git a/Tests/Functional/App/config/config.yml b/Tests/Functional/App/config/config.yml index f2e9aaa7c..fca8eeecf 100644 --- a/Tests/Functional/App/config/config.yml +++ b/Tests/Functional/App/config/config.yml @@ -3,11 +3,29 @@ framework: secret: test router: resource: "%kernel.root_dir%/config/routing.yml" - templating: - engines: ['twig'] profiler: enabled: false +security: + providers: + in_memory: + memory: + users: + ryan: + password: 123 + roles: 'ROLE_USER' + admin: + password: 123 + roles: 'ROLE_ADMIN' + encoders: + Symfony\Component\Security\Core\User\User: plaintext + firewalls: + graph: + pattern: ^/ + http_basic: ~ + stateless: true + anonymous: true + overblog_graphql: errors_handler: debug: false diff --git a/Tests/Functional/App/config/public/config.yml b/Tests/Functional/App/config/public/config.yml index 901ce6e7d..d6147da5f 100644 --- a/Tests/Functional/App/config/public/config.yml +++ b/Tests/Functional/App/config/public/config.yml @@ -1,6 +1,5 @@ imports: - { resource: ../config.yml } - - { resource: ../security.yml } overblog_graphql: definitions: diff --git a/Tests/Functional/App/config/queryComplexityEnv/config.yml b/Tests/Functional/App/config/queryComplexityEnv/config.yml index e2eb1d756..4ffdeac06 100644 --- a/Tests/Functional/App/config/queryComplexityEnv/config.yml +++ b/Tests/Functional/App/config/queryComplexityEnv/config.yml @@ -3,7 +3,7 @@ imports: - { resource: ../connection/services.yml } parameters: - env(GRAPHQL_QUERY_MAX_COMPLEXITY): 10 + env(GRAPHQL_QUERY_MAX_COMPLEXITY): "10" overblog_graphql: security: diff --git a/Tests/Functional/App/config/queryMaxDepthEnv/config.yml b/Tests/Functional/App/config/queryMaxDepthEnv/config.yml index ae134d970..029f93458 100644 --- a/Tests/Functional/App/config/queryMaxDepthEnv/config.yml +++ b/Tests/Functional/App/config/queryMaxDepthEnv/config.yml @@ -3,7 +3,7 @@ imports: - { resource: ../connection/services.yml } parameters: - env(GRAPHQL_QUERY_MAX_DEPTH): 3 + env(GRAPHQL_QUERY_MAX_DEPTH): "3" overblog_graphql: security: diff --git a/Tests/Functional/App/config/security.yml b/Tests/Functional/App/config/security.yml deleted file mode 100644 index 37325245c..000000000 --- a/Tests/Functional/App/config/security.yml +++ /dev/null @@ -1,19 +0,0 @@ -security: - providers: - in_memory: - memory: - users: - ryan: - password: 123 - roles: 'ROLE_USER' - admin: - password: 123 - roles: 'ROLE_ADMIN' - encoders: - Symfony\Component\Security\Core\User\User: plaintext - firewalls: - graph: - pattern: ^/ - http_basic: ~ - stateless: true - anonymous: true diff --git a/Tests/Functional/Command/CompileCommandTest.php b/Tests/Functional/Command/CompileCommandTest.php index 05ed59570..b94b3863d 100644 --- a/Tests/Functional/Command/CompileCommandTest.php +++ b/Tests/Functional/Command/CompileCommandTest.php @@ -78,11 +78,11 @@ private function displayExpected($isVerbose = false) \-[\-]+\s+\-[\-]+\s class\s+path\s* \-[\-]+\s+\-[\-]+\s - Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\PageInfoType {{PATH}}/PageInfoType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\QueryType {{PATH}}/QueryType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\UserType {{PATH}}/UserType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\friendConnectionType {{PATH}}/friendConnectionType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\userConnectionType {{PATH}}/userConnectionType.php + Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\PageInfoType {{PATH}}/PageInfoType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\friendEdgeType {{PATH}}/friendEdgeType.php Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\userEdgeType {{PATH}}/userEdgeType.php \-[\-]+\s+\-[\-]+\s diff --git a/composer.json b/composer.json index ec8825850..08dbea9a4 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,6 @@ "symfony/process": "^3.1 || ^4.0", "symfony/security-bundle": "^3.1 || ^4.0", "symfony/templating": "^3.1 || ^4.0", - "symfony/twig-bundle": "^3.1 || ^4.0", "symfony/web-profiler-bundle": "^3.1 || ^4.0", "symfony/yaml": "^3.1 || ^4.0" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 464c72439..c16e2199f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,8 +29,10 @@ - + - + + + From 9f15ec7387c463b4bbbaef8c702b542d660c83c0 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Mon, 28 May 2018 13:09:57 +0200 Subject: [PATCH 21/31] Fix travis build --- .travis.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88b4102cb..d02172fe0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,15 +14,16 @@ branches: matrix: fast_finish: true include: - - php: 5.6 - env: COMPOSER_UPDATE_FLAGS=--prefer-lowest SYMFONY_DEPRECATIONS_HELPER=disabled - - php: 5.6 - env: SYMFONY_VERSION=3.1.* SYMFONY_DEPRECATIONS_HELPER=disabled - - php: 7.0 - env: SYMFONY_VERSION=3.2.* PHPUNIT_VERSION=^5.7.26 - - php: 7.1 - php: 7.2 - env: SYMFONY_VERSION=4.1.* STABILITY=beta + env: PHP_CS_FIXER=true + - php: 7.2 + env: SYMFONY_VERSION=3.4.* + - php: 7.2 + env: SYMFONY_VERSION=4.0.* TEST_COVERAGE=true + - php: 7.2 + env: SYMFONY_VERSION=4.1.* DEPENDENCIES=beta + - php: nightly + env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs allow_failures: - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs From 1ed56daa7b5aefc916d29d5bfbf43b0606907997 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Mon, 28 May 2018 13:13:55 +0200 Subject: [PATCH 22/31] Fix composer minimum stability --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d02172fe0..b322bd236 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ matrix: - php: 7.2 env: SYMFONY_VERSION=4.0.* TEST_COVERAGE=true - php: 7.2 - env: SYMFONY_VERSION=4.1.* DEPENDENCIES=beta + env: SYMFONY_VERSION=4.1.* STABILITY=beta - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs allow_failures: From 3ce5878402257adb7e22cc057cd827935aac8f51 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Tue, 29 May 2018 07:49:51 +0200 Subject: [PATCH 23/31] Fix examples indentation --- Resources/doc/definitions/resolver.md | 130 +++++++++++++------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index 95d61e4d7..98b2e4680 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -1,5 +1,3 @@ - - # Resolver To ease development we named 2 types of resolver: @@ -42,18 +40,18 @@ use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; class Greetings implements ResolverInterface, AliasedInterface { - public function sayHello($name) - { - return sprintf('hello %s!!!', $name); - } - - /** - * {@inheritdoc} - */ - public static function getAliases() - { - return ['sayHello' => 'say_hello']; - } + public function sayHello($name) + { + return sprintf('hello %s!!!', $name); + } + + /** + * {@inheritdoc} + */ + public static function getAliases() + { + return ['sayHello' => 'say_hello']; + } } ```` @@ -73,10 +71,10 @@ use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; class Greetings implements ResolverInterface { - public function sayHello($name) - { - return sprintf('hello %s!!!', $name); - } + public function sayHello($name) + { + return sprintf('hello %s!!!', $name); + } } ``` @@ -94,10 +92,10 @@ use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; class Greetings implements ResolverInterface { - public function __invoke($name) - { - return sprintf('hello %s!!!', $name); - } + public function __invoke($name) + { + return sprintf('hello %s!!!', $name); + } } ``` @@ -115,20 +113,20 @@ use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface; class CalcMutation implements MutationInterface, AliasedInterface { - private $value; - - public function addition($number) - { - $this->value += $number; - } - - /** - * {@inheritdoc} - */ - public static function getAliases() - { - return ['addition' => 'add']; - } + private $value; + + public function addition($number) + { + $this->value += $number; + } + + /** + * {@inheritdoc} + */ + public static function getAliases() + { + return ['addition' => 'add']; + } } ``` `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or @@ -137,11 +135,11 @@ class CalcMutation implements MutationInterface, AliasedInterface You can also define custom dirs using the config (Symfony <3.3): ```yaml overblog_graphql: -definitions: - auto_mapping: - directories: - - "%kernel.root_dir%/src/*Bundle/CustomDir" - - "%kernel.root_dir%/src/AppBundle/{foo,bar}" + definitions: + auto_mapping: + directories: + - "%kernel.root_dir%/src/*Bundle/CustomDir" + - "%kernel.root_dir%/src/AppBundle/{foo,bar}" ``` If using Symfony 3.3+ disabling auto mapping can be a solution to leave place to native @@ -149,21 +147,21 @@ DI `autoconfigure`: ```yaml overblog_graphql: -definitions: - auto_mapping: false + definitions: + auto_mapping: false ``` Here an example of how this can be done with DI `autoconfigure`: ```yaml services: -App\Mutation\: - resource: '../src/Mutation' - tags: ['overblog_graphql.mutation'] - -App\Resolver\: - resource: '../src/Resolver' - tags: ['overblog_graphql.resolver'] + App\Mutation\: + resource: '../src/Mutation' + tags: ['overblog_graphql.mutation'] + + App\Resolver\: + resource: '../src/Resolver' + tags: ['overblog_graphql.resolver'] ``` ## The service way @@ -175,12 +173,12 @@ Using the php way examples: ```yaml services: -App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello - - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" + App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver, method: sayHello, alias: say_hello } # add alias say_hello + - { name: overblog_graphql.resolver, method: sayHello } # add service id "App\GraphQL\Resolver\Greetings" ``` `SayHello` resolver can be access by using `App\GraphQL\Resolver\Greetings::sayHello` or @@ -190,11 +188,11 @@ for invokable classes no need to use `alias` and `method` attributes: ```yaml services: -App\GraphQL\Resolver\Greetings: - # only for sf < 3.3 - #class: App\GraphQL\Resolver\Greetings - tags: - - { name: overblog_graphql.resolver } + App\GraphQL\Resolver\Greetings: + # only for sf < 3.3 + #class: App\GraphQL\Resolver\Greetings + tags: + - { name: overblog_graphql.resolver } ``` This way resolver can be accessed with service id `App\GraphQL\Resolver\Greetings`. @@ -203,11 +201,11 @@ for mutation: ```yaml services: -App\GraphQL\Mutation\CalcMutation: - # only for sf < 3.3 - #class: App\GraphQL\Mutation\CalcMutation - tags: - - { name: overblog_graphql.mutation, method: addition, alias: add } + App\GraphQL\Mutation\CalcMutation: + # only for sf < 3.3 + #class: App\GraphQL\Mutation\CalcMutation + tags: + - { name: overblog_graphql.mutation, method: addition, alias: add } ``` `addition` mutation can be access by using `App\GraphQL\Mutation\CalcMutation::addition` or `add` alias. From 3c336ca3a2f6aa2702c32510ebbfdd5dc66f208a Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Wed, 30 May 2018 16:54:20 +0200 Subject: [PATCH 24/31] Allow webonyx/graphql-php 0.12 --- .travis.yml | 4 +- Command/GraphQLDumpSchemaCommand.php | 3 +- Config/Parser/GraphQLParser.php | 3 ++ Definition/Type/CustomScalarType.php | 2 +- DependencyInjection/Configuration.php | 4 +- .../OverblogGraphQLExtension.php | 11 ---- Resources/config/services.yml | 2 +- Tests/Config/Parser/GraphQLParserTest.php | 20 ++++++- .../fixtures/graphql/schema-0.11.graphql | 54 +++++++++++++++++++ .../Parser/fixtures/graphql/schema.graphql | 6 +-- Tests/Functional/App/Type/YearScalarType.php | 7 +-- .../Command/GraphDumpSchemaCommandTest.php | 2 + .../Command/fixtures/schema.graphql | 36 ++++++------- Tests/Functional/Upload/UploadTest.php | 3 +- Upload/Type/GraphQLUploadType.php | 2 +- composer.json | 2 +- 16 files changed, 109 insertions(+), 52 deletions(-) create mode 100644 Tests/Config/Parser/fixtures/graphql/schema-0.11.graphql diff --git a/.travis.yml b/.travis.yml index 1e26a4c47..8768ebebb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ matrix: fast_finish: true include: - php: 5.6 - env: COMPOSER_UPDATE_FLAGS=--prefer-lowest SYMFONY_DEPRECATIONS_HELPER=disabled + env: COMPOSER_UPDATE_FLAGS=--prefer-lowest SYMFONY_DEPRECATIONS_HELPER=disabled GRAPHQLPHP_VERSION=^0.11.2 - php: 5.6 - env: SYMFONY_VERSION=3.1.* SYMFONY_DEPRECATIONS_HELPER=disabled + env: SYMFONY_VERSION=3.1.* SYMFONY_DEPRECATIONS_HELPER=disabled GRAPHQLPHP_VERSION=0.12 - php: 7.0 env: SYMFONY_VERSION=3.2.* PHPUNIT_VERSION=^5.7.26 - php: 7.1 diff --git a/Command/GraphQLDumpSchemaCommand.php b/Command/GraphQLDumpSchemaCommand.php index 3372d0e9a..549d51a1f 100644 --- a/Command/GraphQLDumpSchemaCommand.php +++ b/Command/GraphQLDumpSchemaCommand.php @@ -80,7 +80,8 @@ private function createFile(InputInterface $input) switch ($format) { case 'json': $request = [ - 'query' => Introspection::getIntrospectionQuery(false), + // TODO(mcg-web): remove silence deprecation notices after removing webonyx/graphql-php <= 0.11 + 'query' => @Introspection::getIntrospectionQuery(false), 'variables' => [], 'operationName' => null, ]; diff --git a/Config/Parser/GraphQLParser.php b/Config/Parser/GraphQLParser.php index b9f6d2dd9..a77fb57d7 100644 --- a/Config/Parser/GraphQLParser.php +++ b/Config/Parser/GraphQLParser.php @@ -294,6 +294,9 @@ private function astValueNodeToConfig(ValueNode $valueNode) private function cleanAstDescription($description) { + if (property_exists($description, 'value')) { + $description = $description->value; + } $description = trim($description); return empty($description) ? null : $description; diff --git a/Definition/Type/CustomScalarType.php b/Definition/Type/CustomScalarType.php index 5a04fa3f1..6223ba19d 100644 --- a/Definition/Type/CustomScalarType.php +++ b/Definition/Type/CustomScalarType.php @@ -35,7 +35,7 @@ public function parseValue($value) /** * {@inheritdoc} */ - public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode) + public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode, array $variables = null) { return $this->call('parseLiteral', $valueNode); } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d0f25b632..88316163a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -149,9 +149,7 @@ private function servicesSection() ->scalarNode('expression_language') ->defaultValue(self::NAME.'.expression_language.default') ->end() - ->scalarNode('cache_expression_language_parser') - ->defaultValue(self::NAME.'.cache_expression_language_parser.default') - ->end() + ->scalarNode('cache_expression_language_parser')->end() ->end() ->end(); diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php index f1423ce44..aa8c9ff93 100644 --- a/DependencyInjection/OverblogGraphQLExtension.php +++ b/DependencyInjection/OverblogGraphQLExtension.php @@ -13,7 +13,6 @@ use Overblog\GraphQLBundle\EventListener\DebugListener; use Overblog\GraphQLBundle\EventListener\ErrorHandlerListener; use Overblog\GraphQLBundle\EventListener\ErrorLoggerListener; -use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -21,7 +20,6 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\ExpressionLanguage\ParserCache\ArrayParserCache; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Kernel; @@ -33,7 +31,6 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->treatConfigs($configs, $container); $this->setBatchingMethod($config, $container); - $this->setExpressionLanguageDefaultParser($container); $this->setServicesAliases($config, $container); $this->setSchemaBuilderArguments($config, $container); $this->setSchemaArguments($config, $container); @@ -124,14 +121,6 @@ private function setBatchingMethod(array $config, ContainerBuilder $container) $container->setParameter($this->getAlias().'.batching_method', $config['batching_method']); } - private function setExpressionLanguageDefaultParser(ContainerBuilder $container) - { - $class = version_compare(Kernel::VERSION, '3.2.0', '>=') ? ArrayAdapter::class : ArrayParserCache::class; - $definition = new Definition($class); - $definition->setPublic(false); - $container->setDefinition($this->getAlias().'.cache_expression_language_parser.default', $definition); - } - private function setDebugListener(array $config, ContainerBuilder $container) { if ($config['definitions']['show_debug_info']) { diff --git a/Resources/config/services.yml b/Resources/config/services.yml index e8e1d307f..1027a5f9c 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -75,7 +75,7 @@ services: class: Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage public: false arguments: - - '@overblog_graphql.cache_expression_language_parser' + - '@?overblog_graphql.cache_expression_language_parser' overblog_graphql.cache_compiler: class: Overblog\GraphQLBundle\Generator\TypeGenerator diff --git a/Tests/Config/Parser/GraphQLParserTest.php b/Tests/Config/Parser/GraphQLParserTest.php index ab7ee7454..4b97a388e 100644 --- a/Tests/Config/Parser/GraphQLParserTest.php +++ b/Tests/Config/Parser/GraphQLParserTest.php @@ -19,12 +19,15 @@ public function setUp() public function testParse() { - $fileName = __DIR__.'/fixtures/graphql/schema.graphql'; + $fileName = sprintf( + __DIR__.'/fixtures/graphql/schema%s.graphql', + isset($_SERVER['GRAPHQLPHP_VERSION']) && '^0.11.2' === $_SERVER['GRAPHQLPHP_VERSION'] ? '-0.11' : '' + ); $expected = include __DIR__.'/fixtures/graphql/schema.php'; $this->assertContainerAddFileToResources($fileName); $config = GraphQLParser::parse(new \SplFileInfo($fileName), $this->containerBuilder); - $this->assertEquals($expected, $config); + $this->assertEquals($expected, self::cleanConfig($config)); } public function testParseEmptyFile() @@ -65,4 +68,17 @@ private function assertContainerAddFileToResources($fileName) ->method('addResource') ->with($fileName); } + + private static function cleanConfig($config) + { + foreach ($config as $key => &$value) { + if (is_array($value)) { + $value = self::cleanConfig($value); + } + } + + return array_filter($config, function ($item) { + return !is_array($item) || !empty($item); + }); + } } diff --git a/Tests/Config/Parser/fixtures/graphql/schema-0.11.graphql b/Tests/Config/Parser/fixtures/graphql/schema-0.11.graphql new file mode 100644 index 000000000..3fd725f2d --- /dev/null +++ b/Tests/Config/Parser/fixtures/graphql/schema-0.11.graphql @@ -0,0 +1,54 @@ +# Root Query +type Query { + hero( + # Episode list to use to filter + episodes: [Episode!]! = [NEWHOPE, EMPIRE] + ): Character + # search for a droid + droid(id: ID!): Droid +} + +type Starship { + id: ID! + name: String! + length(unit: LengthUnit = METER): Float +} + +enum Episode { + NEWHOPE + EMPIRE + JEDI +} + +interface Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! +} + +type Human implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + starships: [Starship] + totalCredits: Int +} + +type Droid implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + primaryFunction: String +} + +union SearchResult = Human | Droid | Starship + +input ReviewInput { + stars: Int! = 5 + commentary: String = null +} + +scalar Year diff --git a/Tests/Config/Parser/fixtures/graphql/schema.graphql b/Tests/Config/Parser/fixtures/graphql/schema.graphql index 3fd725f2d..bb9b1732d 100644 --- a/Tests/Config/Parser/fixtures/graphql/schema.graphql +++ b/Tests/Config/Parser/fixtures/graphql/schema.graphql @@ -1,10 +1,10 @@ -# Root Query +"""Root Query""" type Query { hero( - # Episode list to use to filter + """Episode list to use to filter""" episodes: [Episode!]! = [NEWHOPE, EMPIRE] ): Character - # search for a droid + """search for a droid""" droid(id: ID!): Droid } diff --git a/Tests/Functional/App/Type/YearScalarType.php b/Tests/Functional/App/Type/YearScalarType.php index e80a6a3f5..6944f258c 100644 --- a/Tests/Functional/App/Type/YearScalarType.php +++ b/Tests/Functional/App/Type/YearScalarType.php @@ -5,7 +5,6 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\ScalarType; -use GraphQL\Utils; class YearScalarType extends ScalarType { @@ -22,17 +21,13 @@ public function serialize($value) */ public function parseValue($value) { - if (!is_string($value)) { - throw new Error(sprintf('Cannot represent following value as a valid year: %s.', Utils::printSafeJson($value))); - } - return (int) str_replace(' AC', '', $value); } /** * {@inheritdoc} */ - public function parseLiteral($valueNode) + public function parseLiteral($valueNode, array $variables = null) { if (!$valueNode instanceof StringValueNode) { throw new Error('Query error: Can only parse strings got: '.$valueNode->kind, [$valueNode]); diff --git a/Tests/Functional/Command/GraphDumpSchemaCommandTest.php b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php index 0d597b09c..99e01366d 100644 --- a/Tests/Functional/Command/GraphDumpSchemaCommandTest.php +++ b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php @@ -126,6 +126,8 @@ private function assertCommandExecution(array $input, $expectedFile, $actualFile $actual = json_decode($actual, true); $this->sortSchemaEntry($expected, 'types', 'name'); $this->sortSchemaEntry($actual, 'types', 'name'); + } elseif ('graphql' === $format && isset($_SERVER['GRAPHQLPHP_VERSION']) && '^0.11.2' === $_SERVER['GRAPHQLPHP_VERSION']) { + $expected = preg_replace('@"""(.*)"""@', '# $1', $expected); } $this->assertEquals($expected, $actual); diff --git a/Tests/Functional/Command/fixtures/schema.graphql b/Tests/Functional/Command/fixtures/schema.graphql index 97dd9e6db..6ebdece93 100644 --- a/Tests/Functional/Command/fixtures/schema.graphql +++ b/Tests/Functional/Command/fixtures/schema.graphql @@ -1,15 +1,15 @@ -# Information about pagination in a connection. +"""Information about pagination in a connection.""" type PageInfo { - # When paginating forwards, are there more items? + """When paginating forwards, are there more items?""" hasNextPage: Boolean! - # When paginating backwards, are there more items? + """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! - # When paginating backwards, the cursor to continue. + """When paginating backwards, the cursor to continue.""" startCursor: String - # When paginating forwards, the cursor to continue. + """When paginating forwards, the cursor to continue.""" endCursor: String } @@ -18,49 +18,49 @@ type Query { } type User { - # the user name + """the user name""" name: String friends(after: String, first: Int, before: String, last: Int): friendConnection friendsForward(after: String, first: Int): userConnection friendsBackward(before: String, last: Int): userConnection } -# A connection to a list of items. +"""A connection to a list of items.""" type friendConnection { totalCount: Int - # Information to aid in pagination. + """Information to aid in pagination.""" pageInfo: PageInfo! - # Information to aid in pagination. + """Information to aid in pagination.""" edges: [friendEdge] } -# An edge in a connection. +"""An edge in a connection.""" type friendEdge { friendshipTime: String - # The item at the end of the edge. + """The item at the end of the edge.""" node: User - # A cursor for use in pagination. + """A cursor for use in pagination.""" cursor: String! } -# A connection to a list of items. +"""A connection to a list of items.""" type userConnection { - # Information to aid in pagination. + """Information to aid in pagination.""" pageInfo: PageInfo! - # Information to aid in pagination. + """Information to aid in pagination.""" edges: [userEdge] } -# An edge in a connection. +"""An edge in a connection.""" type userEdge { - # The item at the end of the edge. + """The item at the end of the edge.""" node: User - # A cursor for use in pagination. + """A cursor for use in pagination.""" cursor: String! } diff --git a/Tests/Functional/Upload/UploadTest.php b/Tests/Functional/Upload/UploadTest.php index ab5e2cebb..35810927c 100644 --- a/Tests/Functional/Upload/UploadTest.php +++ b/Tests/Functional/Upload/UploadTest.php @@ -1,6 +1,6 @@ expectException(InvariantViolation::class); - $this->expectExceptionMessage('Upload scalar serialization unsupported.'); $this->uploadRequest( [ 'operations' => [ diff --git a/Upload/Type/GraphQLUploadType.php b/Upload/Type/GraphQLUploadType.php index 17fdd6df2..da6265496 100644 --- a/Upload/Type/GraphQLUploadType.php +++ b/Upload/Type/GraphQLUploadType.php @@ -49,7 +49,7 @@ public function serialize($value) /** * {@inheritdoc} */ - public function parseLiteral($valueNode) + public function parseLiteral($valueNode, array $variables = null) { throw new InvariantViolation(sprintf('%s scalar literal unsupported.', $this->name)); } diff --git a/composer.json b/composer.json index 08dbea9a4..c6f098b53 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "symfony/framework-bundle": "^3.1 || ^4.0", "symfony/options-resolver": "^3.1 || ^4.0", "symfony/property-access": "^3.1 || ^4.0", - "webonyx/graphql-php": "^0.11.2" + "webonyx/graphql-php": "^0.11.2 || ^0.12.0" }, "suggest": { "nelmio/cors-bundle": "For more flexibility when using CORS prefight", From 27298264285f28f3d6a1ffe9f8698ea08dc3c298 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 31 May 2018 08:51:27 +0200 Subject: [PATCH 25/31] Update InheritanceProcessor.php --- Config/Processor/InheritanceProcessor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Config/Processor/InheritanceProcessor.php b/Config/Processor/InheritanceProcessor.php index dc548f57c..008fbe9d7 100644 --- a/Config/Processor/InheritanceProcessor.php +++ b/Config/Processor/InheritanceProcessor.php @@ -121,7 +121,7 @@ private static function checkTypeExists($name, array $configs, $child) { if (!isset($configs[$name])) { throw new \InvalidArgumentException(sprintf( - 'Type %s inherits by %s not found.', + 'Type %s inherited by %s not found.', json_encode($name), json_encode($child) )); @@ -142,9 +142,9 @@ private static function checkAllowedInheritsTypes($name, array $config, array $a { if (empty($config['decorator']) && isset($config['type']) && !in_array($config['type'], $allowedTypes)) { throw new \InvalidArgumentException(sprintf( - 'Type %s can\'t inherits %s because %s is not allowed type (%s).', - json_encode($name), + 'Type %s can\'t inherit %s because its type (%s) is not allowed type (%s).', json_encode($child), + json_encode($name), json_encode($config['type']), json_encode($allowedTypes) )); From 315137e8ff08a1d84f20d79c89dbd22830160b61 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 31 May 2018 08:54:01 +0200 Subject: [PATCH 26/31] Update InheritanceProcessorTest.php --- Tests/Config/Processor/InheritanceProcessorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Config/Processor/InheritanceProcessorTest.php b/Tests/Config/Processor/InheritanceProcessorTest.php index 3854dcaa6..c4f6b291e 100644 --- a/Tests/Config/Processor/InheritanceProcessorTest.php +++ b/Tests/Config/Processor/InheritanceProcessorTest.php @@ -17,7 +17,7 @@ class InheritanceProcessorTest extends TestCase /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Type "toto" inherits by "bar" not found. + * @expectedExceptionMessage Type "toto" inherited by "bar" not found. */ public function testExtendsUnknownType() { @@ -53,7 +53,7 @@ public function testCircularExtendsType() /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Type "toto" can't inherits "bar" because "enum" is not allowed type (["object","interface"]). + * @expectedExceptionMessage Type "bar" can't inherit "toto" because its type ("enum") is not allowed type (["object","interface"]). */ public function testNotAllowedType() { From 5ca4636c0ae80c0ddbcf97760963dc03b8e8cb0a Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Thu, 31 May 2018 08:24:40 +0200 Subject: [PATCH 27/31] Fix compile cache warmup on init cache --- .travis.yml | 2 -- CacheWarmer/CompileCacheWarmer.php | 6 ++++ DependencyInjection/Configuration.php | 2 +- Generator/TypeGenerator.php | 35 ++++++++++++++++--- Resources/config/services.yml | 1 + Tests/Functional/App/config/access/config.yml | 1 - Tests/Functional/App/config/config.yml | 21 +++++++++++ Tests/Functional/App/config/public/config.yml | 1 - .../App/config/queryComplexityEnv/config.yml | 2 +- .../App/config/queryMaxDepthEnv/config.yml | 2 +- Tests/Functional/App/config/security.yml | 19 ---------- 11 files changed, 61 insertions(+), 31 deletions(-) delete mode 100644 Tests/Functional/App/config/security.yml diff --git a/.travis.yml b/.travis.yml index 0de2a4d6a..acc61dd2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,8 +34,6 @@ matrix: - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs allow_failures: - - php: 7.2 - env: SYMFONY_VERSION=4.1.* DEPENDENCIES=dev - php: nightly env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs diff --git a/CacheWarmer/CompileCacheWarmer.php b/CacheWarmer/CompileCacheWarmer.php index 87935a530..bf0cd9e12 100644 --- a/CacheWarmer/CompileCacheWarmer.php +++ b/CacheWarmer/CompileCacheWarmer.php @@ -28,6 +28,12 @@ public function isOptional() */ public function warmUp($cacheDir) { + // use warm up cache dir if type generator cache dir not already explicitly declare + $baseCacheDir = $this->typeGenerator->getBaseCacheDir(); + if (null === $this->typeGenerator->getCacheDir(false)) { + $this->typeGenerator->setBaseCacheDir($cacheDir); + } $this->typeGenerator->compile(TypeGenerator::MODE_WRITE | TypeGenerator::MODE_OVERRIDE); + $this->typeGenerator->setBaseCacheDir($baseCacheDir); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 6c7856425..1dd8c3515 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -46,7 +46,7 @@ public function getConfigTreeBuilder() ->scalarNode('internal_error_message')->defaultNull()->end() ->variableNode('default_resolver')->defaultValue([Resolver::class, 'defaultResolveFn'])->end() ->scalarNode('class_namespace')->defaultValue('Overblog\\GraphQLBundle\\__DEFINITIONS__')->end() - ->scalarNode('cache_dir')->defaultValue($this->cacheDir.'/overblog/graphql-bundle/__definitions__')->end() + ->scalarNode('cache_dir')->defaultNull()->end() ->booleanNode('use_classloader_listener')->defaultTrue()->end() ->booleanNode('auto_compile')->defaultTrue()->end() ->booleanNode('show_debug_info')->defaultFalse()->end() diff --git a/Generator/TypeGenerator.php b/Generator/TypeGenerator.php index 00686d974..1f53ce2a0 100644 --- a/Generator/TypeGenerator.php +++ b/Generator/TypeGenerator.php @@ -22,27 +22,52 @@ class TypeGenerator extends BaseTypeGenerator private $useClassMap = true; + private $baseCacheDir; + private static $classMapLoaded = false; - public function __construct($classNamespace, array $skeletonDirs, $cacheDir, callable $defaultResolver, array $configs, $useClassMap = true) + public function __construct($classNamespace, array $skeletonDirs, $cacheDir, callable $defaultResolver, array $configs, $useClassMap = true, $baseCacheDir = null) { $this->setCacheDir($cacheDir); $this->defaultResolver = $defaultResolver; $this->configs = $this->processConfigs($configs); $this->useClassMap = $useClassMap; + $this->baseCacheDir = $baseCacheDir; + parent::__construct($classNamespace, $skeletonDirs); } /** - * @return string + * @return string|null + */ + public function getBaseCacheDir() + { + return $this->baseCacheDir; + } + + /** + * @param string|null $baseCacheDir */ - public function getCacheDir() + public function setBaseCacheDir($baseCacheDir) { - return $this->cacheDir; + $this->baseCacheDir = $baseCacheDir; + } + + /** + * @return string|null + */ + public function getCacheDir(/*bool $useDefault = true*/) + { + $useDefault = func_num_args() > 0 ? func_get_arg(0) : true; + if ($useDefault) { + return $this->cacheDir ?: $this->baseCacheDir.'/overblog/graphql-bundle/__definitions__'; + } else { + return $this->cacheDir; + } } /** - * @param string $cacheDir + * @param string|null $cacheDir * * @return $this */ diff --git a/Resources/config/services.yml b/Resources/config/services.yml index ae70602a8..17ad9ec0c 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -98,6 +98,7 @@ services: - "%overblog_graphql.default_resolver%" - "%overblog_graphql_types.config%" - "%overblog_graphql.use_classloader_listener%" + - "%kernel.cache_dir%" calls: - ["addUseStatement", ["Symfony\\Component\\DependencyInjection\\ContainerInterface"]] - ["addUseStatement", ["Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface"]] diff --git a/Tests/Functional/App/config/access/config.yml b/Tests/Functional/App/config/access/config.yml index b9158fe75..bc6af7294 100644 --- a/Tests/Functional/App/config/access/config.yml +++ b/Tests/Functional/App/config/access/config.yml @@ -1,6 +1,5 @@ imports: - { resource: ../config.yml } - - { resource: ../security.yml } - { resource: ../connection/services.yml } - { resource: ../mutation/services.yml } diff --git a/Tests/Functional/App/config/config.yml b/Tests/Functional/App/config/config.yml index d3d1ad89a..9dcbe22bf 100644 --- a/Tests/Functional/App/config/config.yml +++ b/Tests/Functional/App/config/config.yml @@ -8,6 +8,27 @@ framework: profiler: enabled: false +security: + providers: + in_memory: + memory: + users: + ryan: + password: 123 + roles: 'ROLE_USER' + admin: + password: 123 + roles: 'ROLE_ADMIN' + encoders: + Symfony\Component\Security\Core\User\User: plaintext + firewalls: + graph: + pattern: ^/ + http_basic: ~ + stateless: true + anonymous: true + + overblog_graphql: definitions: config_validation: true diff --git a/Tests/Functional/App/config/public/config.yml b/Tests/Functional/App/config/public/config.yml index 901ce6e7d..d6147da5f 100644 --- a/Tests/Functional/App/config/public/config.yml +++ b/Tests/Functional/App/config/public/config.yml @@ -1,6 +1,5 @@ imports: - { resource: ../config.yml } - - { resource: ../security.yml } overblog_graphql: definitions: diff --git a/Tests/Functional/App/config/queryComplexityEnv/config.yml b/Tests/Functional/App/config/queryComplexityEnv/config.yml index e2eb1d756..4ffdeac06 100644 --- a/Tests/Functional/App/config/queryComplexityEnv/config.yml +++ b/Tests/Functional/App/config/queryComplexityEnv/config.yml @@ -3,7 +3,7 @@ imports: - { resource: ../connection/services.yml } parameters: - env(GRAPHQL_QUERY_MAX_COMPLEXITY): 10 + env(GRAPHQL_QUERY_MAX_COMPLEXITY): "10" overblog_graphql: security: diff --git a/Tests/Functional/App/config/queryMaxDepthEnv/config.yml b/Tests/Functional/App/config/queryMaxDepthEnv/config.yml index ae134d970..029f93458 100644 --- a/Tests/Functional/App/config/queryMaxDepthEnv/config.yml +++ b/Tests/Functional/App/config/queryMaxDepthEnv/config.yml @@ -3,7 +3,7 @@ imports: - { resource: ../connection/services.yml } parameters: - env(GRAPHQL_QUERY_MAX_DEPTH): 3 + env(GRAPHQL_QUERY_MAX_DEPTH): "3" overblog_graphql: security: diff --git a/Tests/Functional/App/config/security.yml b/Tests/Functional/App/config/security.yml deleted file mode 100644 index 37325245c..000000000 --- a/Tests/Functional/App/config/security.yml +++ /dev/null @@ -1,19 +0,0 @@ -security: - providers: - in_memory: - memory: - users: - ryan: - password: 123 - roles: 'ROLE_USER' - admin: - password: 123 - roles: 'ROLE_ADMIN' - encoders: - Symfony\Component\Security\Core\User\User: plaintext - firewalls: - graph: - pattern: ^/ - http_basic: ~ - stateless: true - anonymous: true From db25cfff3ae47182f7da53b1d35680a0eedd628c Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Thu, 31 May 2018 11:11:13 +0200 Subject: [PATCH 28/31] Give some to PHP CS --- Generator/TypeGenerator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Generator/TypeGenerator.php b/Generator/TypeGenerator.php index a74760c91..7ef810c49 100644 --- a/Generator/TypeGenerator.php +++ b/Generator/TypeGenerator.php @@ -35,8 +35,7 @@ public function __construct( $useClassMap = true, callable $configProcessor = null, $baseCacheDir = null - ) - { + ) { $this->setCacheDir($cacheDir); $this->configProcessor = null === $configProcessor ? static::DEFAULT_CONFIG_PROCESSOR : $configProcessor; $this->configs = $configs; From 3102ea7a010274d2f48313d49e26de1709db45cb Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Sun, 3 Jun 2018 13:44:02 +0200 Subject: [PATCH 29/31] Remove useless import --- DependencyInjection/OverblogGraphQLExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php index aa8c9ff93..f185e2078 100644 --- a/DependencyInjection/OverblogGraphQLExtension.php +++ b/DependencyInjection/OverblogGraphQLExtension.php @@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\HttpKernel\Kernel; class OverblogGraphQLExtension extends Extension implements PrependExtensionInterface { From c011b3fdadf821247f7e0dfa9e069167e9330a19 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sun, 3 Jun 2018 13:08:24 +0200 Subject: [PATCH 30/31] doc(graphql-schema-language): rewrite and add section for mutations --- .../definitions/graphql-schema-language.md | 113 +++++++++++++++--- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/Resources/doc/definitions/graphql-schema-language.md b/Resources/doc/definitions/graphql-schema-language.md index 6d36c48ca..9a342f553 100644 --- a/Resources/doc/definitions/graphql-schema-language.md +++ b/Resources/doc/definitions/graphql-schema-language.md @@ -8,32 +8,117 @@ or this [cheat sheet](https://github.com/sogko/graphql-shorthand-notation-cheat- #### Usage +##### Define Types + +> [GraphQL documentation about types and fields](https://graphql.github.io/learn/schema/#object-types-and-fields). + + ```graphql # config/graphql/schema.types.graphql -type Query { - bar: Foo! - baz(id: ID!): Baz +type Character { + # Name of the character + name: String! + # This character appears in those episodes + appearsIn: [Episode]! } +``` + +##### Define Enumeration types -scalar Baz +> [GraphQL documentation about Enumerations types](https://graphql.github.io/learn/schema/#enumeration-types). -interface Foo { - # Description of my is_foo field - is_foo: Boolean +```graphql +# Enumeration of episodes +enum Episode { + NEWHOPE + EMPIRE + JEDI } -type Bar implements Foo { - is_foo: Boolean - user: User! +``` + +##### Define Interfaces + +> [GraphQL documentation about Interfaces](https://graphql.github.io/learn/schema/#interfaces). + +```graphql +interface Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! +} +``` + +```graphql +type Human implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + starships: [Starship] + totalCredits: Int } -enum User { - TATA - TITI - TOTO +type Droid implements Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + primaryFunction: String } ``` +##### Define queries + +> [GraphQL documentation about Query type](https://graphql.github.io/learn/schema/#the-query-and-mutation-types). + +```graphql +type RootQuery { + # Access all characters + characters: [Character]! +} +``` + +Do not forget to configure your schema **query** type, as described in the [schema documentation](https://github.com/overblog/GraphQLBundle/blob/master/Resources/doc/definitions/schema.md). + +```yml +overblog_graphql: + definitions: + schema: + query: RootQuery +``` + +##### Define mutations + +> [GraphQL documentation about Mutation type](https://graphql.github.io/learn/schema/#the-query-and-mutation-types). + +```graphql +input CreateCharacter { + name: String! +} + +input UpdateCharacter { + name: String! +} + +type RootMutation { + createCharacter(character: CreateCharacter!): Character! + updateCharacter(characterId: ID!, character: UpdateCharacter!): Character! +} +``` + +Do not forget to configure your schema **mutation** type, as described in the [schema documentation](https://github.com/overblog/GraphQLBundle/blob/master/Resources/doc/definitions/schema.md). + +```yml +overblog_graphql: + definitions: + schema: + mutation: RootMutation +``` + +--- + When using this shorthand syntax, you define your field resolvers (and some more configuration) separately from the schema. Since the schema already describes all of the fields, arguments, and result types, the only thing left is a collection of callable that are called to actually execute these fields. From 9aae7d1b340282d5430205d08a2bfae4f0bff3c1 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Sun, 3 Jun 2018 13:44:02 +0200 Subject: [PATCH 31/31] Remove useless import --- DependencyInjection/OverblogGraphQLExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php index aa8c9ff93..f185e2078 100644 --- a/DependencyInjection/OverblogGraphQLExtension.php +++ b/DependencyInjection/OverblogGraphQLExtension.php @@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\HttpKernel\Kernel; class OverblogGraphQLExtension extends Extension implements PrependExtensionInterface {