├── .gitignore ├── Command └── GraphQLConfigureCommand.php ├── Config └── Rule │ └── TypeValidationRule.php ├── Controller ├── GraphQLController.php └── GraphQLExplorerController.php ├── DependencyInjection ├── Compiler │ └── GraphQlCompilerPass.php ├── Configuration.php └── GraphQLExtension.php ├── Event └── ResolveEvent.php ├── Exception └── UnableToInitializeSchemaServiceException.php ├── Execution ├── Container │ └── SymfonyContainer.php ├── Context │ └── ExecutionContext.php └── Processor.php ├── Field └── AbstractContainerAwareField.php ├── GraphQLBundle.php ├── LICENCE ├── README.md ├── Resources ├── config │ ├── route.xml │ └── services.xml ├── public │ ├── graphiql.css │ ├── graphiql.min.js │ └── react │ │ ├── fetch.min.js │ │ ├── react-15.0.1.min.js │ │ └── react-dom-15.0.1.min.js └── views │ └── Feature │ └── explorer.html.twig ├── Security ├── Manager │ ├── DefaultSecurityManager.php │ └── SecurityManagerInterface.php └── Voter │ ├── AbstractListVoter.php │ ├── BlacklistVoter.php │ └── WhitelistVoter.php ├── Tests └── DependencyInjection │ ├── Fixtures │ └── config │ │ └── yml │ │ ├── empty.yml │ │ └── full.yml │ └── GraphQLExtensionTest.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | composer.lock -------------------------------------------------------------------------------- /Command/GraphQLConfigureCommand.php: -------------------------------------------------------------------------------- 1 | setName('graphql:configure') 23 | ->setDescription('Generates GraphQL Schema class') 24 | ->addOption('composer'); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $isComposerCall = $input->getOption('composer'); 33 | 34 | $container = $this->getContainer(); 35 | $rootDir = $container->getParameter('kernel.root_dir'); 36 | $configFile = $rootDir . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config/packages/graphql.yml'; 37 | 38 | $className = 'Schema'; 39 | $schemaNamespace = self::PROJECT_NAMESPACE . '\\GraphQL'; 40 | $graphqlPath = rtrim($rootDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'GraphQL'; 41 | $classPath = $graphqlPath . DIRECTORY_SEPARATOR . $className . '.php'; 42 | 43 | $inputHelper = $this->getHelper('question'); 44 | if (file_exists($classPath)) { 45 | if (!$isComposerCall) { 46 | $output->writeln(sprintf('Schema class %s was found.', $schemaNamespace . '\\' . $className)); 47 | } 48 | } else { 49 | $question = new ConfirmationQuestion(sprintf('Confirm creating class at %s ? [Y/n]', $schemaNamespace . '\\' . $className), true); 50 | if (!$inputHelper->ask($input, $output, $question)) { 51 | return; 52 | } 53 | 54 | if (!is_dir($graphqlPath)) { 55 | mkdir($graphqlPath, 0777, true); 56 | } 57 | file_put_contents($classPath, $this->getSchemaClassTemplate($schemaNamespace, $className)); 58 | 59 | $output->writeln('Schema file has been created at'); 60 | $output->writeln($classPath . "\n"); 61 | 62 | if (!file_exists($configFile)) { 63 | $question = new ConfirmationQuestion(sprintf('Config file not found (look at %s). Create it? [Y/n]', $configFile), true); 64 | if (!$inputHelper->ask($input, $output, $question)) { 65 | return; 66 | } 67 | 68 | touch($configFile); 69 | } 70 | 71 | $originalConfigData = file_get_contents($configFile); 72 | if (strpos($originalConfigData, 'graphql') === false) { 73 | $projectNameSpace = self::PROJECT_NAMESPACE; 74 | $configData = <<graphQLRouteExists()) { 83 | $question = new ConfirmationQuestion('Confirm adding GraphQL route? [Y/n]', true); 84 | $resource = $this->getMainRouteConfig(); 85 | if ($resource && $inputHelper->ask($input, $output, $question)) { 86 | $routeConfigData = <<writeln('Config was added to ' . $resource); 93 | } 94 | } else { 95 | if (!$isComposerCall) { 96 | $output->writeln('GraphQL default route was found.'); 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * @return null|string 103 | * 104 | * @throws \Exception 105 | */ 106 | protected function getMainRouteConfig() 107 | { 108 | $routerResources = $this->getContainer()->get('router')->getRouteCollection()->getResources(); 109 | foreach ($routerResources as $resource) { 110 | /** @var FileResource|DirectoryResource $resource */ 111 | if (method_exists($resource, 'getResource') && substr($resource->getResource(), -11) == 'routes.yaml') { 112 | return $resource->getResource(); 113 | } 114 | } 115 | 116 | return null; 117 | } 118 | 119 | /** 120 | * @return bool 121 | * @throws \Exception 122 | */ 123 | protected function graphQLRouteExists() 124 | { 125 | $routerResources = $this->getContainer()->get('router')->getRouteCollection()->getResources(); 126 | foreach ($routerResources as $resource) { 127 | /** @var FileResource|DirectoryResource $resource */ 128 | if (method_exists($resource, 'getResource') && strpos($resource->getResource(), 'GraphQLController.php') !== false) { 129 | return true; 130 | } 131 | } 132 | 133 | return false; 134 | } 135 | 136 | protected function generateRoutes() 137 | { 138 | 139 | } 140 | 141 | protected function getSchemaClassTemplate($nameSpace, $className = 'Schema') 142 | { 143 | $tpl = <<getQuery()->addFields([ 160 | 'hello' => [ 161 | 'type' => new StringType(), 162 | 'args' => [ 163 | 'name' => [ 164 | 'type' => new StringType(), 165 | 'defaultValue' => 'Stranger' 166 | ] 167 | ], 168 | 'resolve' => function (\$context, \$args) { 169 | return 'Hello ' . \$args['name']; 170 | } 171 | ] 172 | ]); 173 | } 174 | 175 | } 176 | 177 | 178 | TEXT; 179 | 180 | return $tpl; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Config/Rule/TypeValidationRule.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Youshido\GraphQLBundle\Config\Rule; 10 | 11 | use Youshido\GraphQL\Type\TypeService; 12 | use Youshido\GraphQL\Validator\ConfigValidator\Rules\TypeValidationRule as BaseTypeValidationRule; 13 | 14 | class TypeValidationRule extends BaseTypeValidationRule 15 | { 16 | 17 | public function validate($data, $ruleInfo) 18 | { 19 | if (!is_string($ruleInfo)) { 20 | return false; 21 | } 22 | 23 | if (($ruleInfo == TypeService::TYPE_CALLABLE) && ( 24 | is_callable($data) || 25 | (is_array($data) && count($data) == 2 && substr($data[0], 0, 1) == '@')) 26 | ) { 27 | return true; 28 | } 29 | return parent::validate($data, $ruleInfo); 30 | } 31 | 32 | 33 | } -------------------------------------------------------------------------------- /Controller/GraphQLController.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Controller; 9 | 10 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 11 | use Symfony\Component\DependencyInjection\ContainerAwareInterface; 12 | use Symfony\Component\HttpFoundation\JsonResponse; 13 | use Symfony\Component\Routing\Annotation\Route; 14 | use Youshido\GraphQL\Exception\ConfigurationException; 15 | use Youshido\GraphQLBundle\Exception\UnableToInitializeSchemaServiceException; 16 | use Youshido\GraphQLBundle\Execution\Processor; 17 | 18 | class GraphQLController extends Controller 19 | { 20 | /** 21 | * @Route("/graphql") 22 | * 23 | * @throws \Exception 24 | * 25 | * @return JsonResponse 26 | */ 27 | public function defaultAction() 28 | { 29 | try { 30 | $this->initializeSchemaService(); 31 | } catch (UnableToInitializeSchemaServiceException $e) { 32 | return new JsonResponse( 33 | [['message' => 'Schema class ' . $this->getSchemaClass() . ' does not exist']], 34 | 200, 35 | $this->getResponseHeaders() 36 | ); 37 | } 38 | 39 | if ($this->get('request_stack')->getCurrentRequest()->getMethod() == 'OPTIONS') { 40 | return $this->createEmptyResponse(); 41 | } 42 | 43 | list($queries, $isMultiQueryRequest) = $this->getPayload(); 44 | 45 | $queryResponses = array_map(function($queryData) { 46 | return $this->executeQuery($queryData['query'], $queryData['variables']); 47 | }, $queries); 48 | 49 | $response = new JsonResponse($isMultiQueryRequest ? $queryResponses : $queryResponses[0], 200, $this->getParameter('graphql.response.headers')); 50 | 51 | if ($this->getParameter('graphql.response.json_pretty')) { 52 | $response->setEncodingOptions($response->getEncodingOptions() | JSON_PRETTY_PRINT); 53 | } 54 | 55 | return $response; 56 | } 57 | 58 | private function createEmptyResponse() 59 | { 60 | return new JsonResponse([], 200, $this->getResponseHeaders()); 61 | } 62 | 63 | private function executeQuery($query, $variables) 64 | { 65 | /** @var Processor $processor */ 66 | $processor = $this->get('graphql.processor'); 67 | $processor->processPayload($query, $variables); 68 | 69 | return $processor->getResponseData(); 70 | } 71 | 72 | /** 73 | * @return array 74 | * 75 | * @throws \Exception 76 | */ 77 | private function getPayload() 78 | { 79 | $request = $this->get('request_stack')->getCurrentRequest(); 80 | $query = $request->get('query', null); 81 | $variables = $request->get('variables', []); 82 | $isMultiQueryRequest = false; 83 | $queries = []; 84 | 85 | $variables = is_string($variables) ? json_decode($variables, true) ?: [] : []; 86 | 87 | $content = $request->getContent(); 88 | if (!empty($content)) { 89 | if ($request->headers->has('Content-Type') && 'application/graphql' == $request->headers->get('Content-Type')) { 90 | $queries[] = [ 91 | 'query' => $content, 92 | 'variables' => [], 93 | ]; 94 | } else { 95 | $params = json_decode($content, true); 96 | 97 | if ($params) { 98 | // check for a list of queries 99 | if (isset($params[0]) === true) { 100 | $isMultiQueryRequest = true; 101 | } else { 102 | $params = [$params]; 103 | } 104 | 105 | foreach ($params as $queryParams) { 106 | $query = isset($queryParams['query']) ? $queryParams['query'] : $query; 107 | 108 | if (isset($queryParams['variables'])) { 109 | if (is_string($queryParams['variables'])) { 110 | $variables = json_decode($queryParams['variables'], true) ?: $variables; 111 | } else { 112 | $variables = $queryParams['variables']; 113 | } 114 | 115 | $variables = is_array($variables) ? $variables : []; 116 | } 117 | 118 | $queries[] = [ 119 | 'query' => $query, 120 | 'variables' => $variables, 121 | ]; 122 | } 123 | } 124 | } 125 | } else { 126 | $queries[] = [ 127 | 'query' => $query, 128 | 'variables' => $variables, 129 | ]; 130 | } 131 | 132 | return [$queries, $isMultiQueryRequest]; 133 | } 134 | 135 | /** 136 | * @throws \Exception 137 | */ 138 | private function initializeSchemaService() 139 | { 140 | if ($this->container->initialized('graphql.schema')) { 141 | return; 142 | } 143 | 144 | $this->container->set('graphql.schema', $this->makeSchemaService()); 145 | } 146 | 147 | /** 148 | * @return object 149 | * 150 | * @throws \Exception 151 | */ 152 | private function makeSchemaService() 153 | { 154 | if ($this->container->has($this->getSchemaService())) { 155 | return $this->container->get($this->getSchemaService()); 156 | } 157 | 158 | $schemaClass = $this->getSchemaClass(); 159 | if (!$schemaClass || !class_exists($schemaClass)) { 160 | throw new UnableToInitializeSchemaServiceException(); 161 | } 162 | 163 | if ($this->container->has($schemaClass)) { 164 | return $this->container->get($schemaClass); 165 | } 166 | 167 | $schema = new $schemaClass(); 168 | if ($schema instanceof ContainerAwareInterface) { 169 | $schema->setContainer($this->container); 170 | } 171 | 172 | return $schema; 173 | } 174 | 175 | /** 176 | * @return string 177 | */ 178 | private function getSchemaClass() 179 | { 180 | return $this->getParameter('graphql.schema_class'); 181 | } 182 | 183 | /** 184 | * @return string 185 | */ 186 | private function getSchemaService() 187 | { 188 | $serviceName = $this->getParameter('graphql.schema_service'); 189 | 190 | if (substr($serviceName, 0, 1) === '@') { 191 | return substr($serviceName, 1, strlen($serviceName) - 1); 192 | } 193 | 194 | return $serviceName; 195 | } 196 | 197 | private function getResponseHeaders() 198 | { 199 | return $this->getParameter('graphql.response.headers'); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Controller/GraphQLExplorerController.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Controller; 9 | 10 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 11 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 12 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 13 | 14 | class GraphQLExplorerController extends Controller 15 | { 16 | /** 17 | * @Route("/graphql/explorer") 18 | * 19 | * @return \Symfony\Component\HttpFoundation\Response 20 | */ 21 | public function explorerAction() 22 | { 23 | $response = $this->render('@GraphQLBundle/Feature/explorer.html.twig', [ 24 | 'graphQLUrl' => $this->generateUrl('youshido_graphql_graphql_default'), 25 | 'tokenHeader' => 'access-token' 26 | ]); 27 | 28 | $date = \DateTime::createFromFormat('U', strtotime('tomorrow'), new \DateTimeZone('UTC')); 29 | $response->setExpires($date); 30 | $response->setPublic(); 31 | 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/GraphQlCompilerPass.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class GraphQlCompilerPass implements CompilerPassInterface 17 | { 18 | /** 19 | * You can modify the container here before it is dumped to PHP code. 20 | * 21 | * @param ContainerBuilder $container 22 | * 23 | * @throws \Exception 24 | */ 25 | public function process(ContainerBuilder $container) 26 | { 27 | if ($loggerAlias = $container->getParameter('graphql.logger')) { 28 | if (strpos($loggerAlias, '@') === 0) { 29 | $loggerAlias = substr($loggerAlias, 1); 30 | } 31 | 32 | if (!$container->has($loggerAlias)) { 33 | throw new \RuntimeException(sprintf('Logger "%s" not found', $loggerAlias)); 34 | } 35 | 36 | $container->getDefinition('graphql.processor')->addMethodCall('setLogger', [new Reference($loggerAlias)]); 37 | } 38 | 39 | if ($maxComplexity = $container->getParameter('graphql.max_complexity')) { 40 | $container->getDefinition('graphql.processor')->addMethodCall('setMaxComplexity', [$maxComplexity]); 41 | } 42 | 43 | $this->processSecurityGuard($container); 44 | } 45 | 46 | /** 47 | * @param ContainerBuilder $container 48 | * 49 | * @throws \Exception 50 | */ 51 | private function processSecurityGuard(ContainerBuilder $container) 52 | { 53 | $guardConfig = $container->getParameter('graphql.security.guard_config'); 54 | $whiteList = $container->getParameter('graphql.security.white_list'); 55 | $blackList = $container->getParameter('graphql.security.black_list'); 56 | 57 | if ((!$guardConfig['field'] && !$guardConfig['operation']) && ($whiteList || $blackList)) { 58 | if ($whiteList && $blackList) { 59 | throw new \RuntimeException('Configuration error: Only one white or black list allowed'); 60 | } 61 | 62 | $this->addListVoter($container, BlacklistVoter::class, $blackList); 63 | $this->addListVoter($container, WhitelistVoter::class, $whiteList); 64 | } 65 | } 66 | 67 | /** 68 | * @param ContainerBuilder $container 69 | * @param $voterClass 70 | * @param array $list 71 | * 72 | * @throws \Exception 73 | */ 74 | private function addListVoter(ContainerBuilder $container, $voterClass, array $list) 75 | { 76 | if ($list) { 77 | $container 78 | ->getDefinition('graphql.security.voter') 79 | ->setClass($voterClass) 80 | ->addMethodCall('setEnabled', [true]) 81 | ->addMethodCall('setList', [$list]); 82 | 83 | $container->setParameter('graphql.security.guard_config', [ 84 | 'operation' => true, 85 | 'field' => false, 86 | ]); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('graphql'); 23 | 24 | $rootNode 25 | ->children() 26 | ->scalarNode('schema_class')->defaultValue(null)->end() 27 | ->scalarNode('schema_service')->defaultValue(null)->end() 28 | ->integerNode('max_complexity')->defaultValue(null)->end() 29 | ->scalarNode('logger')->defaultValue(null)->end() 30 | ->arrayNode('security') 31 | ->addDefaultsIfNotSet() 32 | ->canBeUnset() 33 | ->children() 34 | ->arrayNode('guard') 35 | ->addDefaultsIfNotSet() 36 | ->canBeUnset() 37 | ->children() 38 | ->booleanNode('operation')->defaultFalse()->end() 39 | ->booleanNode('field')->defaultFalse()->end() 40 | ->end() 41 | ->end() 42 | ->arrayNode('white_list') 43 | ->canBeUnset() 44 | ->defaultValue([]) 45 | ->prototype('scalar')->end() 46 | ->end() 47 | ->arrayNode('black_list') 48 | ->canBeUnset() 49 | ->defaultValue([]) 50 | ->prototype('scalar')->end() 51 | ->end() 52 | ->end() 53 | ->end() 54 | ->arrayNode('response') 55 | ->addDefaultsIfNotSet() 56 | ->canBeUnset() 57 | ->children() 58 | ->booleanNode('json_pretty')->defaultTrue()->end() 59 | ->arrayNode('headers') 60 | ->prototype('array') 61 | ->children() 62 | ->scalarNode('name')->end() 63 | ->scalarNode('value')->defaultValue(null)->end() 64 | ->end() 65 | ->end() 66 | ->end() 67 | ->end() 68 | ->end() 69 | ->end(); 70 | 71 | return $treeBuilder; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /DependencyInjection/GraphQLExtension.php: -------------------------------------------------------------------------------- 1 | config = $this->processConfiguration($configuration, $configs); 26 | 27 | $preparedHeaders = []; 28 | $headers = $this->config['response']['headers'] ? $this->config['response']['headers'] : $this->getDefaultHeaders(); 29 | foreach ($headers as $header) { 30 | $preparedHeaders[$header['name']] = $header['value']; 31 | } 32 | 33 | $container->setParameter('graphql.response.headers', $preparedHeaders); 34 | $container->setParameter('graphql.schema_class', $this->config['schema_class']); 35 | $container->setParameter('graphql.schema_service', $this->config['schema_service']); 36 | $container->setParameter('graphql.logger', $this->config['logger']); 37 | $container->setParameter('graphql.max_complexity', $this->config['max_complexity']); 38 | $container->setParameter('graphql.response.json_pretty', $this->config['response']['json_pretty']); 39 | 40 | $container->setParameter('graphql.security.guard_config', [ 41 | 'field' => $this->config['security']['guard']['field'], 42 | 'operation' => $this->config['security']['guard']['operation'] 43 | ]); 44 | 45 | $container->setParameter('graphql.security.black_list', $this->config['security']['black_list']); 46 | $container->setParameter('graphql.security.white_list', $this->config['security']['white_list']); 47 | 48 | 49 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 50 | $loader->load('services.xml'); 51 | } 52 | 53 | private function getDefaultHeaders() 54 | { 55 | return [ 56 | ['name' => 'Access-Control-Allow-Origin', 'value' => '*'], 57 | ['name' => 'Access-Control-Allow-Headers', 'value' => 'Content-Type'], 58 | ]; 59 | } 60 | 61 | public function getAlias() 62 | { 63 | return "graphql"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Event/ResolveEvent.php: -------------------------------------------------------------------------------- 1 | field = $field; 31 | $this->astFields = $astFields; 32 | $this->resolvedValue = $resolvedValue; 33 | parent::__construct('ResolveEvent', [$field, $astFields, $resolvedValue]); 34 | } 35 | 36 | /** 37 | * Returns the field. 38 | * 39 | * @return FieldInterface 40 | */ 41 | public function getField() 42 | { 43 | return $this->field; 44 | } 45 | 46 | /** 47 | * Returns the AST fields. 48 | * 49 | * @return array 50 | */ 51 | public function getAstFields() 52 | { 53 | return $this->astFields; 54 | } 55 | 56 | /** 57 | * Returns the resolved value. 58 | * 59 | * @return mixed|null 60 | */ 61 | public function getResolvedValue() 62 | { 63 | return $this->resolvedValue; 64 | } 65 | 66 | /** 67 | * Allows the event listener to manipulate the resolved value. 68 | * 69 | * @param $resolvedValue 70 | */ 71 | public function setResolvedValue($resolvedValue) 72 | { 73 | $this->resolvedValue = $resolvedValue; 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Exception/UnableToInitializeSchemaServiceException.php: -------------------------------------------------------------------------------- 1 | 6 | * created: 9/23/16 10:08 PM 7 | */ 8 | 9 | namespace Youshido\GraphQLBundle\Execution\Container; 10 | 11 | 12 | use Symfony\Component\DependencyInjection\ContainerAwareInterface; 13 | use Symfony\Component\DependencyInjection\ContainerAwareTrait; 14 | use Youshido\GraphQL\Execution\Container\ContainerInterface; 15 | 16 | class SymfonyContainer implements ContainerInterface, ContainerAwareInterface 17 | { 18 | use ContainerAwareTrait; 19 | 20 | public function get($id) 21 | { 22 | return $this->container->get($id); 23 | } 24 | 25 | public function set($id, $value) 26 | { 27 | $this->container->set($id, $value); 28 | return $this; 29 | } 30 | 31 | public function remove($id) 32 | { 33 | throw new \RuntimeException('Remove method is not available for Symfony container'); 34 | } 35 | 36 | public function has($id) 37 | { 38 | return $this->container->has($id); 39 | } 40 | 41 | public function initialized($id) 42 | { 43 | return $this->container->initialized($id); 44 | } 45 | 46 | public function setParameter($name, $value) 47 | { 48 | $this->container->setParameter($name, $value); 49 | return $this; 50 | } 51 | 52 | public function getParameter($name) 53 | { 54 | return $this->container->getParameter($name); 55 | } 56 | 57 | public function hasParameter($name) 58 | { 59 | return $this->container->hasParameter($name); 60 | } 61 | 62 | /** 63 | * Exists temporarily for ContainerAwareField that is to be removed in 1.5 64 | * @return mixed 65 | */ 66 | public function getSymfonyContainer() 67 | { 68 | return $this->container; 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /Execution/Context/ExecutionContext.php: -------------------------------------------------------------------------------- 1 | 6 | * created: 9/23/16 11:20 PM 7 | */ 8 | 9 | namespace Youshido\GraphQLBundle\Execution\Context; 10 | 11 | use \Youshido\GraphQL\Execution\Context\ExecutionContext as BaseExecutionContext; 12 | use Youshido\GraphQL\Schema\AbstractSchema; 13 | use Youshido\GraphQL\Validator\ConfigValidator\ConfigValidator; 14 | use Youshido\GraphQLBundle\Config\Rule\TypeValidationRule; 15 | 16 | class ExecutionContext extends BaseExecutionContext 17 | { 18 | public function __construct(AbstractSchema $schema) { 19 | $validator = ConfigValidator::getInstance(); 20 | $validator->addRule('type', new TypeValidationRule($validator)); 21 | 22 | parent::__construct($schema); 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /Execution/Processor.php: -------------------------------------------------------------------------------- 1 | executionContext = $executionContext; 45 | $this->eventDispatcher = $eventDispatcher; 46 | 47 | parent::__construct($executionContext->getSchema()); 48 | } 49 | 50 | /** 51 | * @param SecurityManagerInterface $securityManger 52 | * 53 | * @return Processor 54 | */ 55 | public function setSecurityManager(SecurityManagerInterface $securityManger) 56 | { 57 | $this->securityManager = $securityManger; 58 | 59 | return $this; 60 | } 61 | 62 | public function processPayload($payload, $variables = [], $reducers = []) 63 | { 64 | if ($this->logger) { 65 | $this->logger->debug(sprintf('GraphQL query: %s', $payload), (array)$variables); 66 | } 67 | 68 | parent::processPayload($payload, $variables); 69 | } 70 | 71 | protected function resolveQuery(Query $query) 72 | { 73 | $this->assertClientHasOperationAccess($query); 74 | 75 | return parent::resolveQuery($query); 76 | } 77 | 78 | private function dispatchResolveEvent(ResolveEvent $event, $name){ 79 | $major = Kernel::MAJOR_VERSION; 80 | $minor = Kernel::MINOR_VERSION; 81 | 82 | if($major > 4 || ($major === 4 && $minor >= 3)){ 83 | $this->eventDispatcher->dispatch($event, $name); 84 | }else{ 85 | $this->eventDispatcher->dispatch($name, $event); 86 | } 87 | } 88 | 89 | protected function doResolve(FieldInterface $field, AstFieldInterface $ast, $parentValue = null) 90 | { 91 | /** @var AstQuery|AstField $ast */ 92 | $arguments = $this->parseArgumentsValues($field, $ast); 93 | $astFields = $ast instanceof AstQuery ? $ast->getFields() : []; 94 | 95 | $event = new ResolveEvent($field, $astFields); 96 | $this->dispatchResolveEvent($event, 'graphql.pre_resolve'); 97 | 98 | $resolveInfo = $this->createResolveInfo($field, $astFields); 99 | $this->assertClientHasFieldAccess($resolveInfo); 100 | 101 | if (in_array('Symfony\Component\DependencyInjection\ContainerAwareInterface', class_implements($field))) { 102 | /** @var $field ContainerAwareInterface */ 103 | $field->setContainer($this->executionContext->getContainer()->getSymfonyContainer()); 104 | } 105 | 106 | if (($field instanceof AbstractField) && ($resolveFunc = $field->getConfig()->getResolveFunction())) { 107 | if ($this->isServiceReference($resolveFunc)) { 108 | $service = substr($resolveFunc[0], 1); 109 | $method = $resolveFunc[1]; 110 | if (!$this->executionContext->getContainer()->has($service)) { 111 | throw new ResolveException(sprintf('Resolve service "%s" not found for field "%s"', $service, $field->getName())); 112 | } 113 | 114 | $serviceInstance = $this->executionContext->getContainer()->get($service); 115 | 116 | if (!method_exists($serviceInstance, $method)) { 117 | throw new ResolveException(sprintf('Resolve method "%s" not found in "%s" service for field "%s"', $method, $service, $field->getName())); 118 | } 119 | 120 | $result = $serviceInstance->$method($parentValue, $arguments, $resolveInfo); 121 | } else { 122 | $result = $resolveFunc($parentValue, $arguments, $resolveInfo); 123 | } 124 | } elseif ($field instanceof Field) { 125 | $result = TypeService::getPropertyValue($parentValue, $field->getName()); 126 | } else { 127 | $result = $field->resolve($parentValue, $arguments, $resolveInfo); 128 | } 129 | 130 | $event = new ResolveEvent($field, $astFields, $result); 131 | $this->dispatchResolveEvent($event, 'graphql.post_resolve'); 132 | return $event->getResolvedValue(); 133 | } 134 | 135 | private function assertClientHasOperationAccess(Query $query) 136 | { 137 | if ($this->securityManager->isSecurityEnabledFor(SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE) 138 | && !$this->securityManager->isGrantedToOperationResolve($query) 139 | ) { 140 | throw $this->securityManager->createNewOperationAccessDeniedException($query); 141 | } 142 | } 143 | 144 | private function assertClientHasFieldAccess(ResolveInfo $resolveInfo) 145 | { 146 | if ($this->securityManager->isSecurityEnabledFor(SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE) 147 | && !$this->securityManager->isGrantedToFieldResolve($resolveInfo) 148 | ) { 149 | throw $this->securityManager->createNewFieldAccessDeniedException($resolveInfo); 150 | } 151 | } 152 | 153 | 154 | private function isServiceReference($resolveFunc) 155 | { 156 | return is_array($resolveFunc) && count($resolveFunc) == 2 && strpos($resolveFunc[0], '@') === 0; 157 | } 158 | 159 | public function setLogger($logger = null) 160 | { 161 | $this->logger = $logger; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Field/AbstractContainerAwareField.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Youshido\GraphQLBundle\Field; 10 | 11 | use Symfony\Component\DependencyInjection\ContainerAwareInterface; 12 | use Symfony\Component\DependencyInjection\ContainerAwareTrait; 13 | use Youshido\GraphQL\Field\AbstractField as BaseAbstractField; 14 | 15 | 16 | abstract class AbstractContainerAwareField extends BaseAbstractField implements ContainerAwareInterface 17 | { 18 | 19 | use ContainerAwareTrait; 20 | 21 | } -------------------------------------------------------------------------------- /GraphQLBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new GraphQlCompilerPass()); 19 | $container->addCompilerPass( 20 | new RegisterListenersPass( 21 | 'graphql.event_dispatcher', 22 | 'graphql.event_listener', 23 | 'graphql.event_subscriber' 24 | ), 25 | PassConfig::TYPE_BEFORE_REMOVING 26 | ); 27 | } 28 | 29 | 30 | public function getContainerExtension() 31 | { 32 | if (null === $this->extension) { 33 | $this->extension = new GraphQLExtension(); 34 | } 35 | 36 | return $this->extension; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Paweł Dziok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symfony GraphQl Bundle 2 | 3 | ### This is a bundle based on the pure [PHP GraphQL Server](http://github.com/youshido/graphql/) implementation 4 | 5 | This bundle provides you with: 6 | 7 | * Full compatibility with the [RFC Specification for GraphQL](https://facebook.github.io/graphql/) 8 | * Agile object oriented structure to architect your GraphQL Schema 9 | * Intuitive Type system that allows you to build your project much faster and stay consistent 10 | * Built-in validation for the GraphQL Schema you develop 11 | * Well documented classes with a lot of examples 12 | * Automatically created endpoint /graphql to handle requests 13 | 14 | **There are simple demo application to demonstrate how we build our API, see [GraphQLDemoApp](https://github.com/Youshido/GraphQLDemoApp).** 15 | 16 | ## Table of Contents 17 | 18 | * [Installation](#installation) 19 | * [Symfony features included](#symfony-features-included) 20 | * [AbstractContainerAwareField class](#class-abstractcontainerawarefield) 21 | * [Service method as callable](#service-method-as-callable) 22 | * [Security](#security) 23 | * [Documentation](#documentation) 24 | 25 | 26 | ## Installation 27 | 28 | We assume you have `composer`, if you're not – install it from the [official website](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx). 29 | If you need any help installing Symfony framework – here's the link [http://symfony.com/doc/current/book/installation.html](http://symfony.com/doc/current/book/installation.html). 30 | > Shortcut to install Symfony: `composer create-project symfony/framework-standard-edition my_project_name` 31 | 32 | Once you have your composer up and running – you're ready to install the GraphQL Bundle. 33 | Go to your project folder and run: 34 | ```sh 35 | composer require youshido/graphql-bundle 36 | ``` 37 | 38 | Then enable bundle in your `app/AppKernel.php` 39 | ```php 40 | new Youshido\GraphQLBundle\GraphQLBundle(), 41 | ``` 42 | 43 | Add the routing reference to the `app/config/routing.yml`: 44 | ```yaml 45 | graphql: 46 | resource: "@GraphQLBundle/Controller/" 47 | ``` 48 | or 49 | ```yaml 50 | graphql: 51 | resource: "@GraphQLBundle/Resources/config/route.xml" 52 | ``` 53 | If you don't have a web server configured you can use a bundled version, simply run `php bin/console server:run`. 54 | 55 | Let's check if you've done everything right so far – try to access url `localhost:8000/graphql`. 56 | You should get a JSON response with the following error: 57 | ```js 58 | {"errors":[{"message":"Schema class does not exist"}]} 59 | ``` 60 | 61 | That's because there was no GraphQL Schema specified for the processor yet. You need to create a GraphQL Schema class and set it inside your `app/config/config.yml` file. 62 | 63 | > There is a way where you can use inline approach and do not create a Schema class, in order to do that you have to define your own GraphQL controller and use a `->setSchema` method of the processor to set the Schema. 64 | 65 | The fastest way to create a Schema class is to use a generator shipped with this bundle: 66 | ```sh 67 | php bin/console graphql:configure AppBundle 68 | ``` 69 | Here *AppBundle* is a name of the bundle where the class will be generated in. 70 | You will be requested for a confirmation to create a class. 71 | 72 | After you've added parameters to the config file, try to access the following link in the browser – `http://localhost:8000/graphql?query={hello(name:World)}` 73 | 74 | > Alternatively, you can execute the same request using CURL client in your console 75 | > `curl http://localhost:8000/graphql --data "query={ hello(name: \"World\") }"` 76 | 77 | Successful response from a test Schema will be displayed: 78 | ```js 79 | {"data":{"hello":"world!"}} 80 | ``` 81 | 82 | That means you have GraphQL Bundle for the Symfony Framework configured and now can architect your GraphQL Schema: 83 | 84 | Next step would be to link assets for GraphiQL Explorer by executing: 85 | ```sh 86 | php bin/console assets:install --symlink 87 | ``` 88 | Now you can access it at `http://localhost:8000/graphql/explorer` 89 | 90 | ## Symfony features 91 | ### Class AbstractContainerAwareField: 92 | AbstractContainerAwareField class used for auto passing container to field, add ability to use container in resolve function: 93 | ```php 94 | class RootDirField extends AbstractContainerAwareField 95 | { 96 | 97 | /** 98 | * @inheritdoc 99 | */ 100 | public function getType() 101 | { 102 | return new StringType(); 103 | } 104 | 105 | /** 106 | * @inheritdoc 107 | */ 108 | public function resolve($value, array $args, ResolveInfo $info) 109 | { 110 | return $this->container->getParameter('kernel.root_dir'); 111 | } 112 | 113 | /** 114 | * @inheritdoc 115 | */ 116 | public function getName() 117 | { 118 | return 'rootDir'; 119 | } 120 | ``` 121 | 122 | ### Service method as callable: 123 | Ability to pass service method as resolve callable: 124 | ```php 125 | $config->addField(new Field([ 126 | 'name' => 'cacheDir', 127 | 'type' => new StringType(), 128 | 'resolve' => ['@resolve_service', 'getCacheDir'] 129 | ])) 130 | ``` 131 | ### Events: 132 | You can use the Symfony Event Dispatcher to get control over specific events which happen when resolving graphql queries. 133 | 134 | ```php 135 | namespace ...\...\..; 136 | 137 | use Youshido\GraphQL\Event\ResolveEvent; 138 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 139 | 140 | class MyGraphQLResolveEventSubscriber implements EventSubscriberInterface 141 | { 142 | public static function getSubscribedEvents() 143 | { 144 | return [ 145 | 'graphql.pre_resolve' => 'onPreResolve', 146 | 'graphql.post_resolve' => 'onPostResolve' 147 | ]; 148 | } 149 | 150 | public function onPreResolve(ResolveEvent $event) 151 | { 152 | //$event->getFields / $event->getAstFields().. 153 | } 154 | 155 | public function onPostResolve(ResolveEvent $event) 156 | { 157 | //$event->getFields / $event->getAstFields().. 158 | } 159 | } 160 | ``` 161 | #### Configuration 162 | 163 | Now configure you subscriber so events will be caught. This can be done in Symfony by either XML, Yaml or PHP. 164 | 165 | ```xml 166 | 167 | 168 | 169 | ``` 170 | 171 | ### Security: 172 | Bundle provides two ways to guard your application: using black/white operation list or using security voter. 173 | 174 | #### Black/white list 175 | Used to guard some root operations. To enable it you need to write following in your config.yml file: 176 | ```yaml 177 | graphql: 178 | 179 | #... 180 | 181 | security: 182 | black_list: ['hello'] # or white_list: ['hello'] 183 | 184 | ``` 185 | #### Using security voter: 186 | Used to guard any field resolve and support two types of guards: root operation and any other field resolving (including internal fields, scalar type fields, root operations). To guard root operation with your specified logic you need to enable it in configuration and use `SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE` attribute. The same things need to do to enable field guard, but in this case use `SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE` attribute. 187 | [Official documentation](http://symfony.com/doc/current/security/voters.html) about voters. 188 | 189 | > Note: Enabling field security lead to a significant reduction in performance 190 | 191 | Config example: 192 | ```yaml 193 | graphql: 194 | security: 195 | guard: 196 | field: true # for any field security 197 | operation: true # for root level security 198 | ``` 199 | 200 | Voter example (add in to your `services.yml` file with tag `security.voter`): 201 | ```php 202 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 203 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 204 | use Youshido\GraphQL\Execution\ResolveInfo; 205 | use Youshido\GraphQLBundle\Security\Manager\SecurityManagerInterface; 206 | 207 | class GraphQLVoter extends Voter 208 | { 209 | 210 | /** 211 | * @inheritdoc 212 | */ 213 | protected function supports($attribute, $subject) 214 | { 215 | return in_array($attribute, [SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE, SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE]); 216 | } 217 | 218 | /** 219 | * @inheritdoc 220 | */ 221 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 222 | { 223 | // your own validation logic here 224 | 225 | if (SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE == $attribute) { 226 | /** @var $subject ResolveInfo */ 227 | if ($subject->getField()->getName() == 'hello') { 228 | return false; 229 | } 230 | 231 | return true; 232 | } elseif (SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE == $attribute) { 233 | /** @var $subject Query */ 234 | if ($subject->getName() == '__schema') { 235 | return true; 236 | } 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | 243 | ## GraphiQL extension: 244 | To run [graphiql extension](https://github.com/graphql/graphiql) just try to access to `http://your_domain/graphql/explorer` 245 | 246 | ## Documentation 247 | All detailed documentation is available on the main GraphQL repository – http://github.com/youshido/graphql/. 248 | -------------------------------------------------------------------------------- /Resources/config/route.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | GraphQLBundle:GraphQL:default 9 | 10 | 11 | 12 | GraphQLBundle:GraphQLExplorer:explorer 13 | 14 | 15 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Youshido\GraphQLBundle\Execution\Processor 9 | Youshido\GraphQLBundle\Execution\Context\ExecutionContext 10 | Youshido\GraphQLBundle\Security\Manager\DefaultSecurityManager 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | %graphql.security.guard_config% 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Resources/public/graphiql.css: -------------------------------------------------------------------------------- 1 | .graphiql-container, 2 | .graphiql-container button, 3 | .graphiql-container input { 4 | color: #141823; 5 | font-family: 6 | system, 7 | -apple-system, 8 | 'San Francisco', 9 | '.SFNSDisplay-Regular', 10 | 'Segoe UI', 11 | Segoe, 12 | 'Segoe WP', 13 | 'Helvetica Neue', 14 | helvetica, 15 | 'Lucida Grande', 16 | arial, 17 | sans-serif; 18 | font-size: 14px; 19 | } 20 | 21 | .graphiql-container { 22 | display: -webkit-box; 23 | display: -ms-flexbox; 24 | display: flex; 25 | -webkit-box-orient: horizontal; 26 | -webkit-box-direction: normal; 27 | -ms-flex-direction: row; 28 | flex-direction: row; 29 | height: 100%; 30 | margin: 0; 31 | overflow: hidden; 32 | width: 100%; 33 | } 34 | 35 | .graphiql-container .editorWrap { 36 | display: -webkit-box; 37 | display: -ms-flexbox; 38 | display: flex; 39 | -webkit-box-orient: vertical; 40 | -webkit-box-direction: normal; 41 | -ms-flex-direction: column; 42 | flex-direction: column; 43 | -webkit-box-flex: 1; 44 | -ms-flex: 1; 45 | flex: 1; 46 | } 47 | 48 | .graphiql-container .title { 49 | font-size: 18px; 50 | } 51 | 52 | .graphiql-container .title em { 53 | font-family: georgia; 54 | font-size: 19px; 55 | } 56 | 57 | .graphiql-container .topBarWrap { 58 | display: -webkit-box; 59 | display: -ms-flexbox; 60 | display: flex; 61 | -webkit-box-orient: horizontal; 62 | -webkit-box-direction: normal; 63 | -ms-flex-direction: row; 64 | flex-direction: row; 65 | } 66 | 67 | .graphiql-container .topBar { 68 | -webkit-box-align: center; 69 | -ms-flex-align: center; 70 | align-items: center; 71 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 72 | background: linear-gradient(#f7f7f7, #e2e2e2); 73 | border-bottom: 1px solid #d0d0d0; 74 | cursor: default; 75 | display: -webkit-box; 76 | display: -ms-flexbox; 77 | display: flex; 78 | -webkit-box-orient: horizontal; 79 | -webkit-box-direction: normal; 80 | -ms-flex-direction: row; 81 | flex-direction: row; 82 | -webkit-box-flex: 1; 83 | -ms-flex: 1; 84 | flex: 1; 85 | height: 34px; 86 | padding: 7px 14px 6px; 87 | -webkit-user-select: none; 88 | -moz-user-select: none; 89 | -ms-user-select: none; 90 | user-select: none; 91 | } 92 | 93 | .graphiql-container .toolbar { 94 | overflow-x: visible; 95 | } 96 | 97 | .graphiql-container .docExplorerShow { 98 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 99 | background: linear-gradient(#f7f7f7, #e2e2e2); 100 | border-bottom: 1px solid #d0d0d0; 101 | border-left: 1px solid rgba(0, 0, 0, 0.2); 102 | border-right: none; 103 | border-top: none; 104 | color: #3B5998; 105 | cursor: pointer; 106 | font-size: 14px; 107 | margin: 0; 108 | outline: 0; 109 | padding: 2px 20px 0 18px; 110 | } 111 | 112 | .graphiql-container .docExplorerShow:before { 113 | border-left: 2px solid #3B5998; 114 | border-top: 2px solid #3B5998; 115 | content: ''; 116 | display: inline-block; 117 | height: 9px; 118 | margin: 0 3px -1px 0; 119 | position: relative; 120 | -webkit-transform: rotate(-45deg); 121 | transform: rotate(-45deg); 122 | width: 9px; 123 | } 124 | 125 | .graphiql-container .editorBar { 126 | display: -webkit-box; 127 | display: -ms-flexbox; 128 | display: flex; 129 | -webkit-box-orient: horizontal; 130 | -webkit-box-direction: normal; 131 | -ms-flex-direction: row; 132 | flex-direction: row; 133 | -webkit-box-flex: 1; 134 | -ms-flex: 1; 135 | flex: 1; 136 | } 137 | 138 | .graphiql-container .queryWrap { 139 | display: -webkit-box; 140 | display: -ms-flexbox; 141 | display: flex; 142 | -webkit-box-orient: vertical; 143 | -webkit-box-direction: normal; 144 | -ms-flex-direction: column; 145 | flex-direction: column; 146 | -webkit-box-flex: 1; 147 | -ms-flex: 1; 148 | flex: 1; 149 | } 150 | 151 | .graphiql-container .resultWrap { 152 | border-left: solid 1px #e0e0e0; 153 | display: -webkit-box; 154 | display: -ms-flexbox; 155 | display: flex; 156 | -webkit-box-orient: vertical; 157 | -webkit-box-direction: normal; 158 | -ms-flex-direction: column; 159 | flex-direction: column; 160 | -webkit-box-flex: 1; 161 | -ms-flex: 1; 162 | flex: 1; 163 | position: relative; 164 | } 165 | 166 | .graphiql-container .docExplorerWrap { 167 | background: white; 168 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 169 | position: relative; 170 | z-index: 3; 171 | } 172 | 173 | .graphiql-container .docExplorerResizer { 174 | cursor: col-resize; 175 | height: 100%; 176 | left: -5px; 177 | position: absolute; 178 | top: 0; 179 | width: 10px; 180 | z-index: 10; 181 | } 182 | 183 | .graphiql-container .docExplorerHide { 184 | cursor: pointer; 185 | font-size: 18px; 186 | margin: -7px -8px -6px 0; 187 | padding: 18px 16px 15px 12px; 188 | } 189 | 190 | .graphiql-container .query-editor { 191 | -webkit-box-flex: 1; 192 | -ms-flex: 1; 193 | flex: 1; 194 | position: relative; 195 | } 196 | 197 | .graphiql-container .variable-editor { 198 | display: -webkit-box; 199 | display: -ms-flexbox; 200 | display: flex; 201 | -webkit-box-orient: vertical; 202 | -webkit-box-direction: normal; 203 | -ms-flex-direction: column; 204 | flex-direction: column; 205 | height: 29px; 206 | position: relative; 207 | } 208 | 209 | .graphiql-container .variable-editor-title { 210 | background: #eeeeee; 211 | border-bottom: 1px solid #d6d6d6; 212 | border-top: 1px solid #e0e0e0; 213 | color: #777; 214 | font-variant: small-caps; 215 | font-weight: bold; 216 | letter-spacing: 1px; 217 | line-height: 14px; 218 | padding: 6px 0 8px 43px; 219 | text-transform: lowercase; 220 | -webkit-user-select: none; 221 | -moz-user-select: none; 222 | -ms-user-select: none; 223 | user-select: none; 224 | } 225 | 226 | .graphiql-container .codemirrorWrap { 227 | -webkit-box-flex: 1; 228 | -ms-flex: 1; 229 | flex: 1; 230 | height: 100%; 231 | position: relative; 232 | } 233 | 234 | .graphiql-container .result-window { 235 | -webkit-box-flex: 1; 236 | -ms-flex: 1; 237 | flex: 1; 238 | height: 100%; 239 | position: relative; 240 | } 241 | 242 | .graphiql-container .footer { 243 | background: #f6f7f8; 244 | border-left: 1px solid #e0e0e0; 245 | border-top: 1px solid #e0e0e0; 246 | margin-left: 12px; 247 | position: relative; 248 | } 249 | 250 | .graphiql-container .footer:before { 251 | background: #eeeeee; 252 | bottom: 0; 253 | content: " "; 254 | left: -13px; 255 | position: absolute; 256 | top: -1px; 257 | width: 12px; 258 | } 259 | 260 | .graphiql-container .result-window .CodeMirror { 261 | background: #f6f7f8; 262 | } 263 | 264 | .graphiql-container .result-window .CodeMirror-gutters { 265 | background-color: #eeeeee; 266 | border-color: #e0e0e0; 267 | cursor: col-resize; 268 | } 269 | 270 | .graphiql-container .result-window .CodeMirror-foldgutter, 271 | .graphiql-container .result-window .CodeMirror-foldgutter-open:after, 272 | .graphiql-container .result-window .CodeMirror-foldgutter-folded:after { 273 | padding-left: 3px; 274 | } 275 | 276 | .graphiql-container .toolbar-button { 277 | background: #fdfdfd; 278 | background: -webkit-linear-gradient(#f9f9f9, #ececec); 279 | background: linear-gradient(#f9f9f9, #ececec); 280 | border-radius: 3px; 281 | box-shadow: 282 | inset 0 0 0 1px rgba(0,0,0,0.20), 283 | 0 1px 0 rgba(255,255,255, 0.7), 284 | inset 0 1px #fff; 285 | color: #555; 286 | cursor: pointer; 287 | display: inline-block; 288 | margin: 0 5px; 289 | padding: 3px 11px 5px; 290 | text-decoration: none; 291 | text-overflow: ellipsis; 292 | white-space: nowrap; 293 | max-width: 150px; 294 | } 295 | 296 | .graphiql-container .toolbar-button:active { 297 | background: -webkit-linear-gradient(#ececec, #d5d5d5); 298 | background: linear-gradient(#ececec, #d5d5d5); 299 | box-shadow: 300 | 0 1px 0 rgba(255, 255, 255, 0.7), 301 | inset 0 0 0 1px rgba(0,0,0,0.10), 302 | inset 0 1px 1px 1px rgba(0, 0, 0, 0.12), 303 | inset 0 0 5px rgba(0, 0, 0, 0.1); 304 | } 305 | 306 | .graphiql-container .toolbar-button.error { 307 | background: -webkit-linear-gradient(#fdf3f3, #e6d6d7); 308 | background: linear-gradient(#fdf3f3, #e6d6d7); 309 | color: #b00; 310 | } 311 | 312 | .graphiql-container .toolbar-button-group { 313 | margin: 0 5px; 314 | white-space: nowrap; 315 | } 316 | 317 | .graphiql-container .toolbar-button-group > * { 318 | margin: 0; 319 | } 320 | 321 | .graphiql-container .toolbar-button-group > *:not(:last-child) { 322 | border-top-right-radius: 0; 323 | border-bottom-right-radius: 0; 324 | } 325 | 326 | .graphiql-container .toolbar-button-group > *:not(:first-child) { 327 | border-top-left-radius: 0; 328 | border-bottom-left-radius: 0; 329 | margin-left: -1px; 330 | } 331 | 332 | .graphiql-container .execute-button-wrap { 333 | height: 34px; 334 | margin: 0 14px 0 28px; 335 | position: relative; 336 | } 337 | 338 | .graphiql-container .execute-button { 339 | background: -webkit-linear-gradient(#fdfdfd, #d2d3d6); 340 | background: linear-gradient(#fdfdfd, #d2d3d6); 341 | border-radius: 17px; 342 | border: 1px solid rgba(0,0,0,0.25); 343 | box-shadow: 0 1px 0 #fff; 344 | cursor: pointer; 345 | fill: #444; 346 | height: 34px; 347 | margin: 0; 348 | padding: 0; 349 | width: 34px; 350 | } 351 | 352 | .graphiql-container .execute-button svg { 353 | pointer-events: none; 354 | } 355 | 356 | .graphiql-container .execute-button:active { 357 | background: -webkit-linear-gradient(#e6e6e6, #c3c3c3); 358 | background: linear-gradient(#e6e6e6, #c3c3c3); 359 | box-shadow: 360 | 0 1px 0 #fff, 361 | inset 0 0 2px rgba(0, 0, 0, 0.2), 362 | inset 0 0 6px rgba(0, 0, 0, 0.1); 363 | } 364 | 365 | .graphiql-container .execute-button:focus { 366 | outline: 0; 367 | } 368 | 369 | .graphiql-container .toolbar-menu, 370 | .graphiql-container .toolbar-select { 371 | position: relative; 372 | } 373 | 374 | .graphiql-container .execute-options, 375 | .graphiql-container .toolbar-menu-items, 376 | .graphiql-container .toolbar-select-options { 377 | background: #fff; 378 | box-shadow: 379 | 0 0 0 1px rgba(0,0,0,0.1), 380 | 0 2px 4px rgba(0,0,0,0.25); 381 | margin: 0; 382 | padding: 6px 0; 383 | position: absolute; 384 | z-index: 100; 385 | } 386 | 387 | .graphiql-container .execute-options { 388 | min-width: 100px; 389 | top: 37px; 390 | left: -1px; 391 | } 392 | 393 | .graphiql-container .toolbar-menu-items { 394 | left: 1px; 395 | margin-top: -1px; 396 | min-width: 110%; 397 | top: 100%; 398 | visibility: hidden; 399 | } 400 | 401 | .graphiql-container .toolbar-select-options { 402 | left: 0; 403 | min-width: 100%; 404 | top: -5px; 405 | visibility: hidden; 406 | } 407 | 408 | .graphiql-container .toolbar-menu:active .toolbar-menu-items, 409 | .graphiql-container .toolbar-select:active .toolbar-select-options { 410 | visibility: visible; 411 | } 412 | 413 | .graphiql-container .execute-options > li, 414 | .graphiql-container .toolbar-menu-items > li, 415 | .graphiql-container .toolbar-select-options > li { 416 | cursor: pointer; 417 | display: block; 418 | margin: none; 419 | max-width: 300px; 420 | overflow: hidden; 421 | padding: 2px 20px 4px 11px; 422 | text-overflow: ellipsis; 423 | white-space: nowrap; 424 | } 425 | 426 | .graphiql-container .execute-options > li.selected, 427 | .graphiql-container .toolbar-menu-items > li.hover, 428 | .graphiql-container .toolbar-menu-items > li:active, 429 | .graphiql-container .toolbar-menu-items > li:hover, 430 | .graphiql-container .toolbar-select-options > li.hover, 431 | .graphiql-container .toolbar-select-options > li:active, 432 | .graphiql-container .toolbar-select-options > li:hover { 433 | background: #e10098; 434 | color: #fff; 435 | } 436 | 437 | .graphiql-container .toolbar-select-options > li > svg { 438 | display: inline; 439 | fill: #666; 440 | margin: 0 -6px 0 6px; 441 | pointer-events: none; 442 | vertical-align: middle; 443 | } 444 | 445 | .graphiql-container .toolbar-select-options > li.hover > svg, 446 | .graphiql-container .toolbar-select-options > li:active > svg, 447 | .graphiql-container .toolbar-select-options > li:hover > svg { 448 | fill: #fff; 449 | } 450 | 451 | .graphiql-container .CodeMirror-scroll { 452 | overflow-scrolling: touch; 453 | } 454 | 455 | .graphiql-container .CodeMirror { 456 | color: #141823; 457 | font-family: 458 | 'Consolas', 459 | 'Inconsolata', 460 | 'Droid Sans Mono', 461 | 'Monaco', 462 | monospace; 463 | font-size: 13px; 464 | height: 100%; 465 | left: 0; 466 | position: absolute; 467 | top: 0; 468 | width: 100%; 469 | } 470 | 471 | .graphiql-container .CodeMirror-lines { 472 | padding: 20px 0; 473 | } 474 | 475 | .CodeMirror-hint-information .content { 476 | box-orient: vertical; 477 | color: #141823; 478 | display: -webkit-box; 479 | display: -ms-flexbox; 480 | display: flex; 481 | font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; 482 | font-size: 13px; 483 | line-clamp: 3; 484 | line-height: 16px; 485 | max-height: 48px; 486 | overflow: hidden; 487 | text-overflow: -o-ellipsis-lastline; 488 | } 489 | 490 | .CodeMirror-hint-information .content p:first-child { 491 | margin-top: 0; 492 | } 493 | 494 | .CodeMirror-hint-information .content p:last-child { 495 | margin-bottom: 0; 496 | } 497 | 498 | .CodeMirror-hint-information .infoType { 499 | color: #CA9800; 500 | cursor: pointer; 501 | display: inline; 502 | margin-right: 0.5em; 503 | } 504 | 505 | .autoInsertedLeaf.cm-property { 506 | -webkit-animation-duration: 6s; 507 | animation-duration: 6s; 508 | -webkit-animation-name: insertionFade; 509 | animation-name: insertionFade; 510 | border-bottom: 2px solid rgba(255, 255, 255, 0); 511 | border-radius: 2px; 512 | margin: -2px -4px -1px; 513 | padding: 2px 4px 1px; 514 | } 515 | 516 | @-webkit-keyframes insertionFade { 517 | from, to { 518 | background: rgba(255, 255, 255, 0); 519 | border-color: rgba(255, 255, 255, 0); 520 | } 521 | 522 | 15%, 85% { 523 | background: #fbffc9; 524 | border-color: #f0f3c0; 525 | } 526 | } 527 | 528 | @keyframes insertionFade { 529 | from, to { 530 | background: rgba(255, 255, 255, 0); 531 | border-color: rgba(255, 255, 255, 0); 532 | } 533 | 534 | 15%, 85% { 535 | background: #fbffc9; 536 | border-color: #f0f3c0; 537 | } 538 | } 539 | 540 | div.CodeMirror-lint-tooltip { 541 | background-color: white; 542 | border-radius: 2px; 543 | border: 0; 544 | color: #141823; 545 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 546 | font-family: 547 | system, 548 | -apple-system, 549 | 'San Francisco', 550 | '.SFNSDisplay-Regular', 551 | 'Segoe UI', 552 | Segoe, 553 | 'Segoe WP', 554 | 'Helvetica Neue', 555 | helvetica, 556 | 'Lucida Grande', 557 | arial, 558 | sans-serif; 559 | font-size: 13px; 560 | line-height: 16px; 561 | max-width: 430px; 562 | opacity: 0; 563 | padding: 8px 10px; 564 | -webkit-transition: opacity 0.15s; 565 | transition: opacity 0.15s; 566 | white-space: pre-wrap; 567 | } 568 | 569 | div.CodeMirror-lint-tooltip > * { 570 | padding-left: 23px; 571 | } 572 | 573 | div.CodeMirror-lint-tooltip > * + * { 574 | margin-top: 12px; 575 | } 576 | 577 | /* COLORS */ 578 | 579 | .graphiql-container .CodeMirror-foldmarker { 580 | border-radius: 4px; 581 | background: #08f; 582 | background: -webkit-linear-gradient(#43A8FF, #0F83E8); 583 | background: linear-gradient(#43A8FF, #0F83E8); 584 | box-shadow: 585 | 0 1px 1px rgba(0, 0, 0, 0.2), 586 | inset 0 0 0 1px rgba(0, 0, 0, 0.1); 587 | color: white; 588 | font-family: arial; 589 | font-size: 12px; 590 | line-height: 0; 591 | margin: 0 3px; 592 | padding: 0px 4px 1px; 593 | text-shadow: 0 -1px rgba(0, 0, 0, 0.1); 594 | } 595 | 596 | .graphiql-container div.CodeMirror span.CodeMirror-matchingbracket { 597 | color: #555; 598 | text-decoration: underline; 599 | } 600 | 601 | .graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { 602 | color: #f00; 603 | } 604 | 605 | /* Comment */ 606 | .cm-comment { 607 | color: #999; 608 | } 609 | 610 | /* Punctuation */ 611 | .cm-punctuation { 612 | color: #555; 613 | } 614 | 615 | /* Keyword */ 616 | .cm-keyword { 617 | color: #B11A04; 618 | } 619 | 620 | /* OperationName, FragmentName */ 621 | .cm-def { 622 | color: #D2054E; 623 | } 624 | 625 | /* FieldName */ 626 | .cm-property { 627 | color: #1F61A0; 628 | } 629 | 630 | /* FieldAlias */ 631 | .cm-qualifier { 632 | color: #1C92A9; 633 | } 634 | 635 | /* ArgumentName and ObjectFieldName */ 636 | .cm-attribute { 637 | color: #8B2BB9; 638 | } 639 | 640 | /* Number */ 641 | .cm-number { 642 | color: #2882F9; 643 | } 644 | 645 | /* String */ 646 | .cm-string { 647 | color: #D64292; 648 | } 649 | 650 | /* Boolean */ 651 | .cm-builtin { 652 | color: #D47509; 653 | } 654 | 655 | /* EnumValue */ 656 | .cm-string-2 { 657 | color: #0B7FC7; 658 | } 659 | 660 | /* Variable */ 661 | .cm-variable { 662 | color: #397D13; 663 | } 664 | 665 | /* Directive */ 666 | .cm-meta { 667 | color: #B33086; 668 | } 669 | 670 | /* Type */ 671 | .cm-atom { 672 | color: #CA9800; 673 | } 674 | /* BASICS */ 675 | 676 | .CodeMirror { 677 | /* Set height, width, borders, and global font properties here */ 678 | color: black; 679 | font-family: monospace; 680 | height: 300px; 681 | } 682 | 683 | /* PADDING */ 684 | 685 | .CodeMirror-lines { 686 | padding: 4px 0; /* Vertical padding around content */ 687 | } 688 | .CodeMirror pre { 689 | padding: 0 4px; /* Horizontal padding of content */ 690 | } 691 | 692 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 693 | background-color: white; /* The little square between H and V scrollbars */ 694 | } 695 | 696 | /* GUTTER */ 697 | 698 | .CodeMirror-gutters { 699 | border-right: 1px solid #ddd; 700 | background-color: #f7f7f7; 701 | white-space: nowrap; 702 | } 703 | .CodeMirror-linenumbers {} 704 | .CodeMirror-linenumber { 705 | color: #999; 706 | min-width: 20px; 707 | padding: 0 3px 0 5px; 708 | text-align: right; 709 | white-space: nowrap; 710 | } 711 | 712 | .CodeMirror-guttermarker { color: black; } 713 | .CodeMirror-guttermarker-subtle { color: #999; } 714 | 715 | /* CURSOR */ 716 | 717 | .CodeMirror div.CodeMirror-cursor { 718 | border-left: 1px solid black; 719 | } 720 | /* Shown when moving in bi-directional text */ 721 | .CodeMirror div.CodeMirror-secondarycursor { 722 | border-left: 1px solid silver; 723 | } 724 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursor { 725 | background: #7e7; 726 | border: 0; 727 | width: auto; 728 | } 729 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursors { 730 | z-index: 1; 731 | } 732 | 733 | .cm-animate-fat-cursor { 734 | -webkit-animation: blink 1.06s steps(1) infinite; 735 | animation: blink 1.06s steps(1) infinite; 736 | border: 0; 737 | width: auto; 738 | } 739 | @-webkit-keyframes blink { 740 | 0% { background: #7e7; } 741 | 50% { background: none; } 742 | 100% { background: #7e7; } 743 | } 744 | @keyframes blink { 745 | 0% { background: #7e7; } 746 | 50% { background: none; } 747 | 100% { background: #7e7; } 748 | } 749 | 750 | /* Can style cursor different in overwrite (non-insert) mode */ 751 | div.CodeMirror-overwrite div.CodeMirror-cursor {} 752 | 753 | .cm-tab { display: inline-block; text-decoration: inherit; } 754 | 755 | .CodeMirror-ruler { 756 | border-left: 1px solid #ccc; 757 | position: absolute; 758 | } 759 | 760 | /* DEFAULT THEME */ 761 | 762 | .cm-s-default .cm-keyword {color: #708;} 763 | .cm-s-default .cm-atom {color: #219;} 764 | .cm-s-default .cm-number {color: #164;} 765 | .cm-s-default .cm-def {color: #00f;} 766 | .cm-s-default .cm-variable, 767 | .cm-s-default .cm-punctuation, 768 | .cm-s-default .cm-property, 769 | .cm-s-default .cm-operator {} 770 | .cm-s-default .cm-variable-2 {color: #05a;} 771 | .cm-s-default .cm-variable-3 {color: #085;} 772 | .cm-s-default .cm-comment {color: #a50;} 773 | .cm-s-default .cm-string {color: #a11;} 774 | .cm-s-default .cm-string-2 {color: #f50;} 775 | .cm-s-default .cm-meta {color: #555;} 776 | .cm-s-default .cm-qualifier {color: #555;} 777 | .cm-s-default .cm-builtin {color: #30a;} 778 | .cm-s-default .cm-bracket {color: #997;} 779 | .cm-s-default .cm-tag {color: #170;} 780 | .cm-s-default .cm-attribute {color: #00c;} 781 | .cm-s-default .cm-header {color: blue;} 782 | .cm-s-default .cm-quote {color: #090;} 783 | .cm-s-default .cm-hr {color: #999;} 784 | .cm-s-default .cm-link {color: #00c;} 785 | 786 | .cm-negative {color: #d44;} 787 | .cm-positive {color: #292;} 788 | .cm-header, .cm-strong {font-weight: bold;} 789 | .cm-em {font-style: italic;} 790 | .cm-link {text-decoration: underline;} 791 | .cm-strikethrough {text-decoration: line-through;} 792 | 793 | .cm-s-default .cm-error {color: #f00;} 794 | .cm-invalidchar {color: #f00;} 795 | 796 | .CodeMirror-composing { border-bottom: 2px solid; } 797 | 798 | /* Default styles for common addons */ 799 | 800 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 801 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 802 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 803 | .CodeMirror-activeline-background {background: #e8f2ff;} 804 | 805 | /* STOP */ 806 | 807 | /* The rest of this file contains styles related to the mechanics of 808 | the editor. You probably shouldn't touch them. */ 809 | 810 | .CodeMirror { 811 | background: white; 812 | overflow: hidden; 813 | position: relative; 814 | } 815 | 816 | .CodeMirror-scroll { 817 | height: 100%; 818 | /* 30px is the magic margin used to hide the element's real scrollbars */ 819 | /* See overflow: hidden in .CodeMirror */ 820 | margin-bottom: -30px; margin-right: -30px; 821 | outline: none; /* Prevent dragging from highlighting the element */ 822 | overflow: scroll !important; /* Things will break if this is overridden */ 823 | padding-bottom: 30px; 824 | position: relative; 825 | } 826 | .CodeMirror-sizer { 827 | border-right: 30px solid transparent; 828 | position: relative; 829 | } 830 | 831 | /* The fake, visible scrollbars. Used to force redraw during scrolling 832 | before actual scrolling happens, thus preventing shaking and 833 | flickering artifacts. */ 834 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 835 | display: none; 836 | position: absolute; 837 | z-index: 6; 838 | } 839 | .CodeMirror-vscrollbar { 840 | overflow-x: hidden; 841 | overflow-y: scroll; 842 | right: 0; top: 0; 843 | } 844 | .CodeMirror-hscrollbar { 845 | bottom: 0; left: 0; 846 | overflow-x: scroll; 847 | overflow-y: hidden; 848 | } 849 | .CodeMirror-scrollbar-filler { 850 | right: 0; bottom: 0; 851 | } 852 | .CodeMirror-gutter-filler { 853 | left: 0; bottom: 0; 854 | } 855 | 856 | .CodeMirror-gutters { 857 | min-height: 100%; 858 | position: absolute; left: 0; top: 0; 859 | z-index: 3; 860 | } 861 | .CodeMirror-gutter { 862 | display: inline-block; 863 | height: 100%; 864 | margin-bottom: -30px; 865 | vertical-align: top; 866 | white-space: normal; 867 | /* Hack to make IE7 behave */ 868 | *zoom:1; 869 | *display:inline; 870 | } 871 | .CodeMirror-gutter-wrapper { 872 | background: none !important; 873 | border: none !important; 874 | position: absolute; 875 | z-index: 4; 876 | } 877 | .CodeMirror-gutter-background { 878 | position: absolute; 879 | top: 0; bottom: 0; 880 | z-index: 4; 881 | } 882 | .CodeMirror-gutter-elt { 883 | cursor: default; 884 | position: absolute; 885 | z-index: 4; 886 | } 887 | .CodeMirror-gutter-wrapper { 888 | -webkit-user-select: none; 889 | -moz-user-select: none; 890 | -ms-user-select: none; 891 | user-select: none; 892 | } 893 | 894 | .CodeMirror-lines { 895 | cursor: text; 896 | min-height: 1px; /* prevents collapsing before first draw */ 897 | } 898 | .CodeMirror pre { 899 | -webkit-tap-highlight-color: transparent; 900 | /* Reset some styles that the rest of the page might have set */ 901 | background: transparent; 902 | border-radius: 0; 903 | border-width: 0; 904 | color: inherit; 905 | font-family: inherit; 906 | font-size: inherit; 907 | -webkit-font-variant-ligatures: none; 908 | font-variant-ligatures: none; 909 | line-height: inherit; 910 | margin: 0; 911 | overflow: visible; 912 | position: relative; 913 | white-space: pre; 914 | word-wrap: normal; 915 | z-index: 2; 916 | } 917 | .CodeMirror-wrap pre { 918 | word-wrap: break-word; 919 | white-space: pre-wrap; 920 | word-break: normal; 921 | } 922 | 923 | .CodeMirror-linebackground { 924 | position: absolute; 925 | left: 0; right: 0; top: 0; bottom: 0; 926 | z-index: 0; 927 | } 928 | 929 | .CodeMirror-linewidget { 930 | overflow: auto; 931 | position: relative; 932 | z-index: 2; 933 | } 934 | 935 | .CodeMirror-widget {} 936 | 937 | .CodeMirror-code { 938 | outline: none; 939 | } 940 | 941 | /* Force content-box sizing for the elements where we expect it */ 942 | .CodeMirror-scroll, 943 | .CodeMirror-sizer, 944 | .CodeMirror-gutter, 945 | .CodeMirror-gutters, 946 | .CodeMirror-linenumber { 947 | box-sizing: content-box; 948 | } 949 | 950 | .CodeMirror-measure { 951 | height: 0; 952 | overflow: hidden; 953 | position: absolute; 954 | visibility: hidden; 955 | width: 100%; 956 | } 957 | 958 | .CodeMirror-cursor { position: absolute; } 959 | .CodeMirror-measure pre { position: static; } 960 | 961 | div.CodeMirror-cursors { 962 | position: relative; 963 | visibility: hidden; 964 | z-index: 3; 965 | } 966 | div.CodeMirror-dragcursors { 967 | visibility: visible; 968 | } 969 | 970 | .CodeMirror-focused div.CodeMirror-cursors { 971 | visibility: visible; 972 | } 973 | 974 | .CodeMirror-selected { background: #d9d9d9; } 975 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 976 | .CodeMirror-crosshair { cursor: crosshair; } 977 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 978 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 979 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 980 | 981 | .cm-searching { 982 | background: #ffa; 983 | background: rgba(255, 255, 0, .4); 984 | } 985 | 986 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 987 | .CodeMirror span { *vertical-align: text-bottom; } 988 | 989 | /* Used to force a border model for a node */ 990 | .cm-force-border { padding-right: .1px; } 991 | 992 | @media print { 993 | /* Hide the cursor when printing */ 994 | .CodeMirror div.CodeMirror-cursors { 995 | visibility: hidden; 996 | } 997 | } 998 | 999 | /* See issue #2901 */ 1000 | .cm-tab-wrap-hack:after { content: ''; } 1001 | 1002 | /* Help users use markselection to safely style text background */ 1003 | span.CodeMirror-selectedtext { background: none; } 1004 | 1005 | .CodeMirror-dialog { 1006 | background: inherit; 1007 | color: inherit; 1008 | left: 0; right: 0; 1009 | overflow: hidden; 1010 | padding: .1em .8em; 1011 | position: absolute; 1012 | z-index: 15; 1013 | } 1014 | 1015 | .CodeMirror-dialog-top { 1016 | border-bottom: 1px solid #eee; 1017 | top: 0; 1018 | } 1019 | 1020 | .CodeMirror-dialog-bottom { 1021 | border-top: 1px solid #eee; 1022 | bottom: 0; 1023 | } 1024 | 1025 | .CodeMirror-dialog input { 1026 | background: transparent; 1027 | border: 1px solid #d3d6db; 1028 | color: inherit; 1029 | font-family: monospace; 1030 | outline: none; 1031 | width: 20em; 1032 | } 1033 | 1034 | .CodeMirror-dialog button { 1035 | font-size: 70%; 1036 | } 1037 | .graphiql-container .doc-explorer { 1038 | background: white; 1039 | } 1040 | 1041 | .graphiql-container .doc-explorer-title-bar { 1042 | cursor: default; 1043 | display: -webkit-box; 1044 | display: -ms-flexbox; 1045 | display: flex; 1046 | height: 34px; 1047 | line-height: 14px; 1048 | padding: 8px 8px 5px; 1049 | position: relative; 1050 | -webkit-user-select: none; 1051 | -moz-user-select: none; 1052 | -ms-user-select: none; 1053 | user-select: none; 1054 | } 1055 | 1056 | .graphiql-container .doc-explorer-title { 1057 | -webkit-box-flex: 1; 1058 | -ms-flex: 1; 1059 | flex: 1; 1060 | font-weight: bold; 1061 | overflow-x: hidden; 1062 | padding: 10px 0 10px 10px; 1063 | text-align: center; 1064 | text-overflow: ellipsis; 1065 | white-space: nowrap; 1066 | } 1067 | 1068 | .graphiql-container .doc-explorer-back { 1069 | color: #3B5998; 1070 | cursor: pointer; 1071 | margin: -7px 0 -6px -8px; 1072 | overflow-x: hidden; 1073 | padding: 17px 12px 16px 16px; 1074 | text-overflow: ellipsis; 1075 | white-space: nowrap; 1076 | } 1077 | 1078 | .doc-explorer-narrow .doc-explorer-back { 1079 | width: 0; 1080 | } 1081 | 1082 | .graphiql-container .doc-explorer-back:before { 1083 | border-left: 2px solid #3B5998; 1084 | border-top: 2px solid #3B5998; 1085 | content: ''; 1086 | display: inline-block; 1087 | height: 9px; 1088 | margin: 0 3px -1px 0; 1089 | position: relative; 1090 | -webkit-transform: rotate(-45deg); 1091 | transform: rotate(-45deg); 1092 | width: 9px; 1093 | } 1094 | 1095 | .graphiql-container .doc-explorer-rhs { 1096 | position: relative; 1097 | } 1098 | 1099 | .graphiql-container .doc-explorer-contents { 1100 | background-color: #ffffff; 1101 | border-top: 1px solid #d6d6d6; 1102 | bottom: 0; 1103 | left: 0; 1104 | min-width: 300px; 1105 | overflow-y: auto; 1106 | padding: 20px 15px; 1107 | position: absolute; 1108 | right: 0; 1109 | top: 47px; 1110 | } 1111 | 1112 | .graphiql-container .doc-type-description p:first-child , 1113 | .graphiql-container .doc-type-description blockquote:first-child { 1114 | margin-top: 0; 1115 | } 1116 | 1117 | .graphiql-container .doc-explorer-contents a { 1118 | cursor: pointer; 1119 | text-decoration: none; 1120 | } 1121 | 1122 | .graphiql-container .doc-explorer-contents a:hover { 1123 | text-decoration: underline; 1124 | } 1125 | 1126 | .graphiql-container .doc-value-description > :first-child { 1127 | margin-top: 4px; 1128 | } 1129 | 1130 | .graphiql-container .doc-value-description > :last-child { 1131 | margin-bottom: 4px; 1132 | } 1133 | 1134 | .graphiql-container .doc-category { 1135 | margin: 20px 0; 1136 | } 1137 | 1138 | .graphiql-container .doc-category-title { 1139 | border-bottom: 1px solid #e0e0e0; 1140 | color: #777; 1141 | cursor: default; 1142 | font-size: 14px; 1143 | font-variant: small-caps; 1144 | font-weight: bold; 1145 | letter-spacing: 1px; 1146 | margin: 0 -15px 10px 0; 1147 | padding: 10px 0; 1148 | -webkit-user-select: none; 1149 | -moz-user-select: none; 1150 | -ms-user-select: none; 1151 | user-select: none; 1152 | } 1153 | 1154 | .graphiql-container .doc-category-item { 1155 | margin: 12px 0; 1156 | color: #555; 1157 | } 1158 | 1159 | .graphiql-container .keyword { 1160 | color: #B11A04; 1161 | } 1162 | 1163 | .graphiql-container .type-name { 1164 | color: #CA9800; 1165 | } 1166 | 1167 | .graphiql-container .field-name { 1168 | color: #1F61A0; 1169 | } 1170 | 1171 | .graphiql-container .enum-value { 1172 | color: #0B7FC7; 1173 | } 1174 | 1175 | .graphiql-container .arg-name { 1176 | color: #8B2BB9; 1177 | } 1178 | 1179 | .graphiql-container .arg { 1180 | display: block; 1181 | margin-left: 1em; 1182 | } 1183 | 1184 | .graphiql-container .arg:first-child:last-child, 1185 | .graphiql-container .arg:first-child:nth-last-child(2), 1186 | .graphiql-container .arg:first-child:nth-last-child(2) ~ .arg { 1187 | display: inherit; 1188 | margin: inherit; 1189 | } 1190 | 1191 | .graphiql-container .arg:first-child:nth-last-child(2):after { 1192 | content: ', '; 1193 | } 1194 | 1195 | .graphiql-container .arg-default-value { 1196 | color: #0B7FC7; 1197 | } 1198 | 1199 | .graphiql-container .doc-deprecation { 1200 | background: #fffae8; 1201 | box-shadow: inset 0 0 1px #bfb063; 1202 | color: #867F70; 1203 | line-height: 16px; 1204 | margin: 8px -8px; 1205 | max-height: 80px; 1206 | overflow: hidden; 1207 | padding: 8px; 1208 | border-radius: 3px; 1209 | } 1210 | 1211 | .graphiql-container .doc-deprecation:before { 1212 | content: 'Deprecated:'; 1213 | color: #c79b2e; 1214 | cursor: default; 1215 | display: block; 1216 | font-size: 9px; 1217 | font-weight: bold; 1218 | letter-spacing: 1px; 1219 | line-height: 1; 1220 | padding-bottom: 5px; 1221 | text-transform: uppercase; 1222 | -webkit-user-select: none; 1223 | -moz-user-select: none; 1224 | -ms-user-select: none; 1225 | user-select: none; 1226 | } 1227 | 1228 | .graphiql-container .doc-deprecation > :first-child { 1229 | margin-top: 0; 1230 | } 1231 | 1232 | .graphiql-container .doc-deprecation > :last-child { 1233 | margin-bottom: 0; 1234 | } 1235 | 1236 | .graphiql-container .show-btn { 1237 | -webkit-appearance: initial; 1238 | display: block; 1239 | border-radius: 3px; 1240 | border: solid 1px #ccc; 1241 | text-align: center; 1242 | padding: 8px 12px 10px; 1243 | width: 100%; 1244 | box-sizing: border-box; 1245 | background: #fbfcfc; 1246 | color: #555; 1247 | cursor: pointer; 1248 | } 1249 | 1250 | .graphiql-container .search-box { 1251 | border-bottom: 1px solid #d3d6db; 1252 | display: block; 1253 | font-size: 14px; 1254 | margin: -15px -15px 12px 0; 1255 | position: relative; 1256 | } 1257 | 1258 | .graphiql-container .search-box:before { 1259 | content: '\26b2'; 1260 | cursor: pointer; 1261 | display: block; 1262 | font-size: 24px; 1263 | position: absolute; 1264 | top: -2px; 1265 | -webkit-transform: rotate(-45deg); 1266 | transform: rotate(-45deg); 1267 | -webkit-user-select: none; 1268 | -moz-user-select: none; 1269 | -ms-user-select: none; 1270 | user-select: none; 1271 | } 1272 | 1273 | .graphiql-container .search-box .search-box-clear { 1274 | background-color: #d0d0d0; 1275 | border-radius: 12px; 1276 | color: #fff; 1277 | cursor: pointer; 1278 | font-size: 11px; 1279 | padding: 1px 5px 2px; 1280 | position: absolute; 1281 | right: 3px; 1282 | top: 8px; 1283 | -webkit-user-select: none; 1284 | -moz-user-select: none; 1285 | -ms-user-select: none; 1286 | user-select: none; 1287 | } 1288 | 1289 | .graphiql-container .search-box .search-box-clear:hover { 1290 | background-color: #b9b9b9; 1291 | } 1292 | 1293 | .graphiql-container .search-box > input { 1294 | border: none; 1295 | box-sizing: border-box; 1296 | font-size: 14px; 1297 | outline: none; 1298 | padding: 6px 24px 8px 20px; 1299 | width: 100%; 1300 | } 1301 | 1302 | .graphiql-container .error-container { 1303 | font-weight: bold; 1304 | left: 0; 1305 | letter-spacing: 1px; 1306 | opacity: 0.5; 1307 | position: absolute; 1308 | right: 0; 1309 | text-align: center; 1310 | text-transform: uppercase; 1311 | top: 50%; 1312 | -webkit-transform: translate(0, -50%); 1313 | transform: translate(0, -50%); 1314 | } 1315 | .CodeMirror-foldmarker { 1316 | color: blue; 1317 | cursor: pointer; 1318 | font-family: arial; 1319 | line-height: .3; 1320 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 1321 | } 1322 | .CodeMirror-foldgutter { 1323 | width: .7em; 1324 | } 1325 | .CodeMirror-foldgutter-open, 1326 | .CodeMirror-foldgutter-folded { 1327 | cursor: pointer; 1328 | } 1329 | .CodeMirror-foldgutter-open:after { 1330 | content: "\25BE"; 1331 | } 1332 | .CodeMirror-foldgutter-folded:after { 1333 | content: "\25B8"; 1334 | } 1335 | .CodeMirror-info { 1336 | background: white; 1337 | border-radius: 2px; 1338 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1339 | box-sizing: border-box; 1340 | color: #555; 1341 | font-family: 1342 | system, 1343 | -apple-system, 1344 | 'San Francisco', 1345 | '.SFNSDisplay-Regular', 1346 | 'Segoe UI', 1347 | Segoe, 1348 | 'Segoe WP', 1349 | 'Helvetica Neue', 1350 | helvetica, 1351 | 'Lucida Grande', 1352 | arial, 1353 | sans-serif; 1354 | font-size: 13px; 1355 | line-height: 16px; 1356 | margin: 8px -8px; 1357 | max-width: 400px; 1358 | opacity: 0; 1359 | overflow: hidden; 1360 | padding: 8px 8px; 1361 | position: fixed; 1362 | -webkit-transition: opacity 0.15s; 1363 | transition: opacity 0.15s; 1364 | z-index: 50; 1365 | } 1366 | 1367 | .CodeMirror-info :first-child { 1368 | margin-top: 0; 1369 | } 1370 | 1371 | .CodeMirror-info :last-child { 1372 | margin-bottom: 0; 1373 | } 1374 | 1375 | .CodeMirror-info p { 1376 | margin: 1em 0; 1377 | } 1378 | 1379 | .CodeMirror-info .info-description { 1380 | color: #777; 1381 | line-height: 16px; 1382 | margin-top: 1em; 1383 | max-height: 80px; 1384 | overflow: hidden; 1385 | } 1386 | 1387 | .CodeMirror-info .info-deprecation { 1388 | background: #fffae8; 1389 | box-shadow: inset 0 1px 1px -1px #bfb063; 1390 | color: #867F70; 1391 | line-height: 16px; 1392 | margin: -8px; 1393 | margin-top: 8px; 1394 | max-height: 80px; 1395 | overflow: hidden; 1396 | padding: 8px; 1397 | } 1398 | 1399 | .CodeMirror-info .info-deprecation-label { 1400 | color: #c79b2e; 1401 | cursor: default; 1402 | display: block; 1403 | font-size: 9px; 1404 | font-weight: bold; 1405 | letter-spacing: 1px; 1406 | line-height: 1; 1407 | padding-bottom: 5px; 1408 | text-transform: uppercase; 1409 | -webkit-user-select: none; 1410 | -moz-user-select: none; 1411 | -ms-user-select: none; 1412 | user-select: none; 1413 | } 1414 | 1415 | .CodeMirror-info .info-deprecation-label + * { 1416 | margin-top: 0; 1417 | } 1418 | 1419 | .CodeMirror-info a { 1420 | text-decoration: none; 1421 | } 1422 | 1423 | .CodeMirror-info a:hover { 1424 | text-decoration: underline; 1425 | } 1426 | 1427 | .CodeMirror-info .type-name { 1428 | color: #CA9800; 1429 | } 1430 | 1431 | .CodeMirror-info .field-name { 1432 | color: #1F61A0; 1433 | } 1434 | 1435 | .CodeMirror-info .enum-value { 1436 | color: #0B7FC7; 1437 | } 1438 | 1439 | .CodeMirror-info .arg-name { 1440 | color: #8B2BB9; 1441 | } 1442 | 1443 | .CodeMirror-info .directive-name { 1444 | color: #B33086; 1445 | } 1446 | .CodeMirror-jump-token { 1447 | text-decoration: underline; 1448 | cursor: pointer; 1449 | } 1450 | /* The lint marker gutter */ 1451 | .CodeMirror-lint-markers { 1452 | width: 16px; 1453 | } 1454 | 1455 | .CodeMirror-lint-tooltip { 1456 | background-color: infobackground; 1457 | border-radius: 4px 4px 4px 4px; 1458 | border: 1px solid black; 1459 | color: infotext; 1460 | font-family: monospace; 1461 | font-size: 10pt; 1462 | max-width: 600px; 1463 | opacity: 0; 1464 | overflow: hidden; 1465 | padding: 2px 5px; 1466 | position: fixed; 1467 | -webkit-transition: opacity .4s; 1468 | transition: opacity .4s; 1469 | white-space: pre-wrap; 1470 | z-index: 100; 1471 | } 1472 | 1473 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { 1474 | background-position: left bottom; 1475 | background-repeat: repeat-x; 1476 | } 1477 | 1478 | .CodeMirror-lint-mark-error { 1479 | background-image: 1480 | url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") 1481 | ; 1482 | } 1483 | 1484 | .CodeMirror-lint-mark-warning { 1485 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 1486 | } 1487 | 1488 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { 1489 | background-position: center center; 1490 | background-repeat: no-repeat; 1491 | cursor: pointer; 1492 | display: inline-block; 1493 | height: 16px; 1494 | position: relative; 1495 | vertical-align: middle; 1496 | width: 16px; 1497 | } 1498 | 1499 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { 1500 | background-position: top left; 1501 | background-repeat: no-repeat; 1502 | padding-left: 18px; 1503 | } 1504 | 1505 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 1506 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); 1507 | } 1508 | 1509 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 1510 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); 1511 | } 1512 | 1513 | .CodeMirror-lint-marker-multiple { 1514 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); 1515 | background-position: right bottom; 1516 | background-repeat: no-repeat; 1517 | width: 100%; height: 100%; 1518 | } 1519 | .graphiql-container .spinner-container { 1520 | height: 36px; 1521 | left: 50%; 1522 | position: absolute; 1523 | top: 50%; 1524 | -webkit-transform: translate(-50%, -50%); 1525 | transform: translate(-50%, -50%); 1526 | width: 36px; 1527 | z-index: 10; 1528 | } 1529 | 1530 | .graphiql-container .spinner { 1531 | -webkit-animation: rotation .6s infinite linear; 1532 | animation: rotation .6s infinite linear; 1533 | border-bottom: 6px solid rgba(150, 150, 150, .15); 1534 | border-left: 6px solid rgba(150, 150, 150, .15); 1535 | border-radius: 100%; 1536 | border-right: 6px solid rgba(150, 150, 150, .15); 1537 | border-top: 6px solid rgba(150, 150, 150, .8); 1538 | display: inline-block; 1539 | height: 24px; 1540 | position: absolute; 1541 | vertical-align: middle; 1542 | width: 24px; 1543 | } 1544 | 1545 | @-webkit-keyframes rotation { 1546 | from { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 1547 | to { -webkit-transform: rotate(359deg); transform: rotate(359deg); } 1548 | } 1549 | 1550 | @keyframes rotation { 1551 | from { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 1552 | to { -webkit-transform: rotate(359deg); transform: rotate(359deg); } 1553 | } 1554 | .CodeMirror-hints { 1555 | background: white; 1556 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1557 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 1558 | font-size: 13px; 1559 | list-style: none; 1560 | margin-left: -6px; 1561 | margin: 0; 1562 | max-height: 14.5em; 1563 | overflow-y: auto; 1564 | overflow: hidden; 1565 | padding: 0; 1566 | position: absolute; 1567 | z-index: 10; 1568 | } 1569 | 1570 | .CodeMirror-hint { 1571 | border-top: solid 1px #f7f7f7; 1572 | color: #141823; 1573 | cursor: pointer; 1574 | margin: 0; 1575 | max-width: 300px; 1576 | overflow: hidden; 1577 | padding: 2px 6px; 1578 | white-space: pre; 1579 | } 1580 | 1581 | li.CodeMirror-hint-active { 1582 | background-color: #08f; 1583 | border-top-color: white; 1584 | color: white; 1585 | } 1586 | 1587 | .CodeMirror-hint-information { 1588 | border-top: solid 1px #c0c0c0; 1589 | max-width: 300px; 1590 | padding: 4px 6px; 1591 | position: relative; 1592 | z-index: 1; 1593 | } 1594 | 1595 | .CodeMirror-hint-information:first-child { 1596 | border-bottom: solid 1px #c0c0c0; 1597 | border-top: none; 1598 | margin-bottom: -1px; 1599 | } 1600 | 1601 | .CodeMirror-hint-deprecation { 1602 | background: #fffae8; 1603 | box-shadow: inset 0 1px 1px -1px #bfb063; 1604 | color: #867F70; 1605 | font-family: 1606 | system, 1607 | -apple-system, 1608 | 'San Francisco', 1609 | '.SFNSDisplay-Regular', 1610 | 'Segoe UI', 1611 | Segoe, 1612 | 'Segoe WP', 1613 | 'Helvetica Neue', 1614 | helvetica, 1615 | 'Lucida Grande', 1616 | arial, 1617 | sans-serif; 1618 | font-size: 13px; 1619 | line-height: 16px; 1620 | margin-top: 4px; 1621 | max-height: 80px; 1622 | overflow: hidden; 1623 | padding: 6px; 1624 | } 1625 | 1626 | .CodeMirror-hint-deprecation .deprecation-label { 1627 | color: #c79b2e; 1628 | cursor: default; 1629 | display: block; 1630 | font-size: 9px; 1631 | font-weight: bold; 1632 | letter-spacing: 1px; 1633 | line-height: 1; 1634 | padding-bottom: 5px; 1635 | text-transform: uppercase; 1636 | -webkit-user-select: none; 1637 | -moz-user-select: none; 1638 | -ms-user-select: none; 1639 | user-select: none; 1640 | } 1641 | 1642 | .CodeMirror-hint-deprecation .deprecation-label + * { 1643 | margin-top: 0; 1644 | } 1645 | 1646 | .CodeMirror-hint-deprecation :last-child { 1647 | margin-bottom: 0; 1648 | } 1649 | -------------------------------------------------------------------------------- /Resources/public/react/fetch.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t){if("string"!=typeof t&&(t=t.toString()),/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(t))throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function e(t){return"string"!=typeof t&&(t=t.toString()),t}function r(t){this.map={},t instanceof r?t.forEach(function(t,e){this.append(e,t)},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function o(t){return t.bodyUsed?Promise.reject(new TypeError("Already read")):void(t.bodyUsed=!0)}function n(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function s(t){var e=new FileReader;return e.readAsArrayBuffer(t),n(e)}function i(t){var e=new FileReader;return e.readAsText(t),n(e)}function a(){return this.bodyUsed=!1,this._initBody=function(t){if(this._bodyInit=t,"string"==typeof t)this._bodyText=t;else if(p.blob&&Blob.prototype.isPrototypeOf(t))this._bodyBlob=t;else if(p.formData&&FormData.prototype.isPrototypeOf(t))this._bodyFormData=t;else{if(t)throw new Error("unsupported BodyInit type");this._bodyText=""}},p.blob?(this.blob=function(){var t=o(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this.blob().then(s)},this.text=function(){var t=o(this);if(t)return t;if(this._bodyBlob)return i(this._bodyBlob);if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)}):this.text=function(){var t=o(this);return t?t:Promise.resolve(this._bodyText)},p.formData&&(this.formData=function(){return this.text().then(h)}),this.json=function(){return this.text().then(JSON.parse)},this}function u(t){var e=t.toUpperCase();return c.indexOf(e)>-1?e:t}function f(t,e){if(e=e||{},this.url=t,this.credentials=e.credentials||"omit",this.headers=new r(e.headers),this.method=u(e.method||"GET"),this.mode=e.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&e.body)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(e.body)}function h(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function d(t){var e=new r,o=t.getAllResponseHeaders().trim().split("\n");return o.forEach(function(t){var r=t.trim().split(":"),o=r.shift().trim(),n=r.join(":").trim();e.append(o,n)}),e}function l(t,e){e||(e={}),this._initBody(t),this.type="default",this.url=null,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.statusText=e.statusText,this.headers=e.headers instanceof r?e.headers:new r(e.headers),this.url=e.url||""}if(!self.fetch){r.prototype.append=function(r,o){r=t(r),o=e(o);var n=this.map[r];n||(n=[],this.map[r]=n),n.push(o)},r.prototype["delete"]=function(e){delete this.map[t(e)]},r.prototype.get=function(e){var r=this.map[t(e)];return r?r[0]:null},r.prototype.getAll=function(e){return this.map[t(e)]||[]},r.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},r.prototype.set=function(r,o){this.map[t(r)]=[e(o)]},r.prototype.forEach=function(t,e){Object.getOwnPropertyNames(this.map).forEach(function(r){this.map[r].forEach(function(o){t.call(e,o,r,this)},this)},this)};var p={blob:"FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in self},c=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];a.call(f.prototype),a.call(l.prototype),self.Headers=r,self.Request=f,self.Response=l,self.fetch=function(t,e){var r;return r=f.prototype.isPrototypeOf(t)&&!e?t:new f(t,e),new Promise(function(t,e){function o(){return"responseURL"in n?n.responseURL:/^X-Request-URL:/m.test(n.getAllResponseHeaders())?n.getResponseHeader("X-Request-URL"):void 0}var n=new XMLHttpRequest;n.onload=function(){var r=1223===n.status?204:n.status;if(100>r||r>599)return void e(new TypeError("Network request failed"));var s={status:r,statusText:n.statusText,headers:d(n),url:o()},i="response"in n?n.response:n.responseText;t(new l(i,s))},n.onerror=function(){e(new TypeError("Network request failed"))},n.open(r.method,r.url,!0),"include"===r.credentials&&(n.withCredentials=!0),"responseType"in n&&p.blob&&(n.responseType="blob"),r.headers.forEach(function(t,e){n.setRequestHeader(e,t)}),n.send("undefined"==typeof r._bodyInit?null:r._bodyInit)})},self.fetch.polyfill=!0}}(); -------------------------------------------------------------------------------- /Resources/public/react/react-dom-15.0.1.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v15.1.0 3 | * 4 | * Copyright 2013-present, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}); -------------------------------------------------------------------------------- /Resources/views/Feature/explorer.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
Loading...
63 | 176 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /Security/Manager/DefaultSecurityManager.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Security\Manager; 9 | 10 | 11 | use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; 12 | use Symfony\Component\Security\Core\Exception\AccessDeniedException; 13 | use Youshido\GraphQL\Execution\ResolveInfo; 14 | use Youshido\GraphQL\Parser\Ast\Query; 15 | 16 | class DefaultSecurityManager implements SecurityManagerInterface 17 | { 18 | 19 | /** @var bool */ 20 | private $fieldSecurityEnabled = false; 21 | 22 | /** @var bool */ 23 | private $rootOperationSecurityEnabled = false; 24 | 25 | /** @var AuthorizationCheckerInterface */ 26 | private $authorizationChecker; 27 | 28 | public function __construct(AuthorizationCheckerInterface $authorizationChecker, array $guardConfig = []) 29 | { 30 | $this->authorizationChecker = $authorizationChecker; 31 | $this->fieldSecurityEnabled = isset($guardConfig['field']) ? $guardConfig['field'] : false; 32 | $this->rootOperationSecurityEnabled = isset($guardConfig['operation']) ? $guardConfig['operation'] : false; 33 | } 34 | 35 | /** 36 | * @param string $attribute 37 | * 38 | * @return bool 39 | */ 40 | public function isSecurityEnabledFor($attribute) 41 | { 42 | if (SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE == $attribute) { 43 | return $this->fieldSecurityEnabled; 44 | } else if (SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE == $attribute) { 45 | return $this->rootOperationSecurityEnabled; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | /** 52 | * @param boolean $fieldSecurityEnabled 53 | */ 54 | public function setFieldSecurityEnabled($fieldSecurityEnabled) 55 | { 56 | $this->fieldSecurityEnabled = $fieldSecurityEnabled; 57 | } 58 | 59 | /** 60 | * @param boolean $rootOperationSecurityEnabled 61 | */ 62 | public function setRooOperationSecurityEnabled($rootOperationSecurityEnabled) 63 | { 64 | $this->rootOperationSecurityEnabled = $rootOperationSecurityEnabled; 65 | } 66 | 67 | /** 68 | * @param Query $query 69 | * 70 | * @return bool 71 | */ 72 | public function isGrantedToOperationResolve(Query $query) 73 | { 74 | return $this->authorizationChecker->isGranted(SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE, $query); 75 | } 76 | 77 | /** 78 | * @param ResolveInfo $resolveInfo 79 | * 80 | * @return bool 81 | */ 82 | public function isGrantedToFieldResolve(ResolveInfo $resolveInfo) 83 | { 84 | return $this->authorizationChecker->isGranted(SecurityManagerInterface::RESOLVE_FIELD_ATTRIBUTE, $resolveInfo); 85 | } 86 | 87 | /** 88 | * @param ResolveInfo $resolveInfo 89 | * 90 | * @return mixed 91 | * 92 | * @throw \Exception 93 | */ 94 | public function createNewFieldAccessDeniedException(ResolveInfo $resolveInfo) 95 | { 96 | return new AccessDeniedException(); 97 | } 98 | 99 | /** 100 | * @param Query $query 101 | * 102 | * @return mixed 103 | * 104 | * @throw \Exception 105 | */ 106 | public function createNewOperationAccessDeniedException(Query $query) 107 | { 108 | return new AccessDeniedException(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Security/Manager/SecurityManagerInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface SecurityManagerInterface 14 | { 15 | 16 | const RESOLVE_ROOT_OPERATION_ATTRIBUTE = 'RESOLVE_ROOT_OPERATION'; 17 | const RESOLVE_FIELD_ATTRIBUTE = 'RESOLVE_FIELD'; 18 | 19 | /** 20 | * @param $attribute string 21 | * 22 | * @return bool 23 | */ 24 | public function isSecurityEnabledFor($attribute); 25 | 26 | /** 27 | * @param ResolveInfo $resolveInfo 28 | * 29 | * @return bool 30 | */ 31 | public function isGrantedToFieldResolve(ResolveInfo $resolveInfo); 32 | 33 | /** 34 | * @param Query $query 35 | * 36 | * @return bool 37 | */ 38 | public function isGrantedToOperationResolve(Query $query); 39 | 40 | /** 41 | * @param ResolveInfo $resolveInfo 42 | * 43 | * @return mixed 44 | * 45 | * @throw \Exception 46 | */ 47 | public function createNewFieldAccessDeniedException(ResolveInfo $resolveInfo); 48 | 49 | /** 50 | * @param Query $query 51 | * 52 | * @return mixed 53 | * 54 | * @throw \Exception 55 | */ 56 | public function createNewOperationAccessDeniedException(Query $query); 57 | } -------------------------------------------------------------------------------- /Security/Voter/AbstractListVoter.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Security\Voter; 9 | 10 | 11 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 12 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 13 | use Youshido\GraphQLBundle\Security\Manager\SecurityManagerInterface; 14 | 15 | abstract class AbstractListVoter extends Voter 16 | { 17 | 18 | /** @var string[] */ 19 | private $list = []; 20 | 21 | /** @var bool */ 22 | private $enabled = false; 23 | 24 | protected function supports($attribute, $subject) 25 | { 26 | return $this->enabled && $attribute == SecurityManagerInterface::RESOLVE_ROOT_OPERATION_ATTRIBUTE; 27 | } 28 | 29 | protected function isLoggedInUser(TokenInterface $token) 30 | { 31 | return is_object($token->getUser()); 32 | } 33 | 34 | /** 35 | * @param array $list 36 | */ 37 | public function setList(array $list) 38 | { 39 | $this->list = $list; 40 | } 41 | 42 | /** 43 | * @return \string[] 44 | */ 45 | public function getList() 46 | { 47 | return $this->list; 48 | } 49 | 50 | protected function inList($query) 51 | { 52 | return in_array($query, $this->list); 53 | } 54 | 55 | public function setEnabled($enabled) 56 | { 57 | $this->enabled = $enabled; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Security/Voter/BlacklistVoter.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Security\Voter; 9 | 10 | 11 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 12 | use Youshido\GraphQL\Parser\Ast\Query; 13 | 14 | class BlacklistVoter extends AbstractListVoter 15 | { 16 | 17 | /** 18 | * Perform a single access check operation on a given attribute, subject and token. 19 | * 20 | * @param string $attribute 21 | * @param mixed $subject 22 | * @param TokenInterface $token 23 | * 24 | * @return bool 25 | */ 26 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 27 | { 28 | /** @var $subject Query */ 29 | return $this->isLoggedInUser($token) || !$this->inList($subject->getName()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/Voter/WhitelistVoter.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Youshido\GraphQLBundle\Security\Voter; 9 | 10 | 11 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 12 | use Youshido\GraphQL\Parser\Ast\Query; 13 | 14 | class WhitelistVoter extends AbstractListVoter 15 | { 16 | 17 | /** 18 | * Perform a single access check operation on a given attribute, subject and token. 19 | * 20 | * @param string $attribute 21 | * @param mixed $subject 22 | * @param TokenInterface $token 23 | * 24 | * @return bool 25 | */ 26 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 27 | { 28 | /** @var $subject Query */ 29 | return $this->isLoggedInUser($token) || $this->inList($subject->getName()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/Fixtures/config/yml/empty.yml: -------------------------------------------------------------------------------- 1 | graphql: ~ -------------------------------------------------------------------------------- /Tests/DependencyInjection/Fixtures/config/yml/full.yml: -------------------------------------------------------------------------------- 1 | graphql: 2 | schema_class: AppBundle\GraphQL\Schema 3 | max_complexity: 10 4 | logger: "@logger" 5 | security: 6 | guard: 7 | field: true 8 | operation: true 9 | black_list: ['hello'] 10 | white_list: ['world'] 11 | response: 12 | json_pretty: false 13 | headers: 14 | 'custom-header': 15 | name: 'X-Powered-By' 16 | value: 'GraphQL' 17 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/GraphQLExtensionTest.php: -------------------------------------------------------------------------------- 1 | loadContainerFromFile('empty', 'yml'); 19 | 20 | $this->assertNull($container->getParameter('graphql.schema_class')); 21 | $this->assertEquals(null, $container->getParameter('graphql.max_complexity')); 22 | $this->assertEquals(null, $container->getParameter('graphql.logger')); 23 | $this->assertEmpty($container->getParameter('graphql.security.white_list')); 24 | $this->assertEmpty($container->getParameter('graphql.security.black_list')); 25 | $this->assertEquals([ 26 | 'field' => false, 27 | 'operation' => false, 28 | ], 29 | $container->getParameter('graphql.security.guard_config') 30 | ); 31 | 32 | $this->assertTrue($container->getParameter('graphql.response.json_pretty')); 33 | $this->assertEquals([ 34 | 'Access-Control-Allow-Origin' => '*', 35 | 'Access-Control-Allow-Headers' => 'Content-Type', 36 | ], 37 | $container->getParameter('graphql.response.headers') 38 | ); 39 | } 40 | 41 | public function testDefaultCanBeOverridden() 42 | { 43 | $container = $this->loadContainerFromFile('full', 'yml'); 44 | $this->assertEquals('AppBundle\GraphQL\Schema', $container->getParameter('graphql.schema_class')); 45 | $this->assertEquals(10, $container->getParameter('graphql.max_complexity')); 46 | $this->assertEquals('@logger', $container->getParameter('graphql.logger')); 47 | 48 | $this->assertEquals(['hello'], $container->getParameter('graphql.security.black_list')); 49 | $this->assertEquals(['world'], $container->getParameter('graphql.security.white_list')); 50 | $this->assertEquals([ 51 | 'field' => true, 52 | 'operation' => true, 53 | ], 54 | $container->getParameter('graphql.security.guard_config') 55 | ); 56 | 57 | $this->assertFalse($container->getParameter('graphql.response.json_pretty')); 58 | $this->assertEquals([ 59 | 'X-Powered-By' => 'GraphQL', 60 | ], 61 | $container->getParameter('graphql.response.headers') 62 | ); 63 | 64 | } 65 | 66 | private function loadContainerFromFile($file, $type, array $services = array(), $skipEnvVars = false) 67 | { 68 | $container = new ContainerBuilder(); 69 | if ($skipEnvVars && !method_exists($container, 'resolveEnvPlaceholders')) { 70 | $this->markTestSkipped('Runtime environment variables has been introduced in the Dependency Injection version 3.2.'); 71 | } 72 | $container->setParameter('kernel.debug', false); 73 | $container->setParameter('kernel.cache_dir', '/tmp'); 74 | foreach ($services as $id => $service) { 75 | $container->set($id, $service); 76 | } 77 | $container->registerExtension(new GraphQLExtension()); 78 | $locator = new FileLocator(__DIR__.'/Fixtures/config/'.$type); 79 | 80 | switch ($type) { 81 | case 'xml': 82 | $loader = new XmlFileLoader($container, $locator); 83 | break; 84 | case 'yml': 85 | $loader = new YamlFileLoader($container, $locator); 86 | break; 87 | case 'php': 88 | $loader = new PhpFileLoader($container, $locator); 89 | break; 90 | default: 91 | throw new \InvalidArgumentException('Invalid file type'); 92 | } 93 | 94 | $loader->load($file.'.'.$type); 95 | $container->getCompilerPassConfig()->setOptimizationPasses(array( 96 | new ResolveDefinitionTemplatesPass(), 97 | )); 98 | $container->getCompilerPassConfig()->setRemovingPasses(array()); 99 | $container->compile(); 100 | return $container; 101 | } 102 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youshido/graphql-bundle", 3 | "description": "Symfony GraphQl Bundle", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Portey Vasil", 8 | "email": "portey@gmail.com" 9 | }, 10 | { 11 | "name": "Alexandr Viniychuk", 12 | "email": "a@viniychuk.com" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Youshido\\GraphQLBundle\\": "" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=5.6", 22 | "youshido/graphql": "~1.4" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "~4.7", 26 | "composer/composer": "~1.2", 27 | "symfony/framework-bundle": "~2.7|~3.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | . 17 | 18 | ./Resources 19 | ./Tests 20 | ./vendor 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------