├── .dockerignore ├── .gitignore ├── .style.yapf ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Readme.md ├── api.py ├── docker-compose.yml ├── factories.py ├── makefile ├── manage.py ├── models.py ├── ql.py ├── requirements.txt ├── requirements_dev.txt ├── settings.py ├── start_web.sh ├── static ├── css │ └── graphiql.min.css ├── index.html └── js │ ├── app-graphiql.js │ └── graphiql.min.js ├── tests.py └── utils.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pythoscope/ 3 | *.pyc 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | bin/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Installer logs 29 | pip-log.txt 30 | pip-delete-this-directory.txt 31 | 32 | # Unit test / coverage reports 33 | htmlcov/ 34 | cover/ 35 | .tox/ 36 | .coverage 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | 41 | # Translations 42 | *.mo 43 | 44 | # Mr Developer 45 | .mr.developer.cfg 46 | .project 47 | .pydevproject 48 | 49 | #PyCharm 50 | .idea/ 51 | 52 | # Rope 53 | .ropeproject 54 | 55 | # Django stuff: 56 | *.log 57 | *.pot 58 | 59 | # Sphinx documentation 60 | docs/_build/ -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | spaces_before_comment = 4 4 | split_before_logical_operator = true 5 | column_limit=130 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "pypy3" 4 | - "3.4" 5 | 6 | # command to install dependencies 7 | install: 8 | - make req 9 | # command to run tests 10 | script: make test 11 | 12 | services: 13 | - mongodb -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pypy:3-onbuild 2 | 3 | ADD ./ /app 4 | WORKDIR /app 5 | RUN make req 6 | ENV DEBUG 1 7 | 8 | EXPOSE 5000 9 | 10 | CMD ./start_web.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Myasoedov 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Example of small GraphQL app with Flask + Python3/PyPy3 + MongoDb 2 | 3 | [![Build Status](https://travis-ci.org/msoedov/flask-graphql-example.svg?branch=master)](https://travis-ci.org/msoedov/flask-graphql-example) 4 | [![Code Issues](https://www.quantifiedcode.com/api/v1/project/44eede68b96745bdafee5a8a208ea3c3/badge.svg)](https://www.quantifiedcode.com/app/project/44eede68b96745bdafee5a8a208ea3c3) 5 | 6 | ### Quick start with docker 7 | 8 | 9 | ```shell 10 | docker-compose build 11 | docker-compose up 12 | ``` 13 | 14 | Optionally populate database 15 | 16 | ```shell 17 | docker-compose run web pypy3 manage.py init 18 | ``` 19 | 20 | And then open http://localhost:5000/ui 21 | 22 | 23 | ![Demo screen](https://sc-cdn.scaleengine.net/i/1abd73bf614838ef8cae5a35093ca3cd1.png) 24 | 25 | 26 | ### Development workflow 27 | 28 | Create a virtual environment with Python3 or PyPy3 29 | 30 | Make sure you have running MongoDb instance either on localhost or 31 | ```shell 32 | export DB_PORT_27017_TCP_ADDR='ip address' 33 | 34 | ``` 35 | 36 | Likewise you can use containerized Mongo but you will need to setup env variables as well 37 | 38 | ```shell 39 | docker-compose build 40 | docker-compose up db 41 | ``` 42 | 43 | 44 | Then you can install deps and run the python app 45 | ``` 46 | make req 47 | python api.py 48 | ``` 49 | 50 | #### Miscellaneous 51 | 52 | Auto format your code 53 | ```shell 54 | 55 | make format 56 | ``` 57 | 58 | Nosetests with reload 59 | ```shell 60 | make watch 61 | ``` -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import status 4 | import trafaret as t 5 | from flask import redirect, request 6 | from flask.ext.api import FlaskAPI 7 | from flask_debugtoolbar import DebugToolbarExtension 8 | from flask_swagger import swagger 9 | from graphql.core.error import GraphQLError, format_error 10 | 11 | from factories import * 12 | from models import * 13 | from ql import schema 14 | from utils import * 15 | 16 | app = FlaskAPI(__name__, static_url_path='/static') 17 | app.config.from_object('settings.DevConfig') 18 | 19 | toolbar = DebugToolbarExtension(app) 20 | 21 | logger = logging.getLogger(__package__) 22 | 23 | 24 | @app.route('/ui') 25 | def ui(): 26 | return redirect('/static/index.html') 27 | 28 | 29 | @app.route('/graph-query', methods=['POST']) 30 | def query(): 31 | """ 32 | GraphQL query 33 | 34 | # query Yo { 35 | # user(email: "$email" ) { 36 | # email, 37 | # posts { 38 | # title 39 | # etags 40 | # tags 41 | # comments { 42 | # name 43 | # content 44 | # } 45 | # } 46 | # } 47 | # } 48 | 49 | """ 50 | query = request.json.get('query') 51 | variables = request.json.get('variables') # Todo: add handling variables 52 | logger.debug('Query: %s', request.json) 53 | result = schema.execute(query) 54 | result_hash = format_result(result) 55 | return result_hash 56 | 57 | 58 | @app.errorhandler(t.DataError) 59 | def handle_invalid_usage(data_error): 60 | error_details = {k: str(v) for k, v in data_error.error.items()} 61 | logger.error('Validation errors: %s', error_details) 62 | return error_details, status.HTTP_400_BAD_REQUEST 63 | 64 | 65 | @app.errorhandler(GraphQLError) 66 | def handle_invalid_graph_error(graphql_error): 67 | error_message = format_error(graphql_error) 68 | logger.error(error_message) 69 | return {'error': error_message}, status.HTTP_400_BAD_REQUEST 70 | 71 | 72 | @app.route('/health-check') 73 | @app.route('/ping') 74 | def health_check(): 75 | """ 76 | Health check 77 | """ 78 | return {'reply': 'pong'} 79 | 80 | 81 | @app.route("/spec") 82 | def spec(): 83 | swag = swagger(app) 84 | swag['info']['version'] = "1.0" 85 | swag['info']['title'] = "Demo of graphql API endpoint" 86 | return swag 87 | 88 | 89 | if __name__ == '__main__': 90 | app.debug = app.config['DEBUG'] 91 | app.run(host='0.0.0.0', port=5000) 92 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | ports: 4 | - "127.0.0.1:5000:5000" 5 | links: 6 | - db 7 | db: 8 | image: mongo:3.0.2 9 | ports: 10 | - "27017:27017" 11 | 12 | -------------------------------------------------------------------------------- /factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from faker import Factory 3 | 4 | from models import * 5 | 6 | fake = Factory.create() 7 | 8 | 9 | class UserFactory(factory.mongoengine.MongoEngineFactory): 10 | 11 | class Meta: 12 | model = User 13 | 14 | @factory.lazy_attribute 15 | def email(self): 16 | return fake.email() 17 | 18 | @factory.lazy_attribute 19 | def first_name(self): 20 | return fake.first_name() 21 | 22 | @factory.lazy_attribute 23 | def last_name(self): 24 | return fake.last_name() 25 | 26 | 27 | class CommentFactory(factory.mongoengine.MongoEngineFactory): 28 | 29 | @factory.lazy_attribute 30 | def name(self): 31 | return fake.first_name() 32 | 33 | @factory.lazy_attribute 34 | def content(self): 35 | return fake.text() 36 | 37 | class Meta: 38 | model = Comment 39 | 40 | 41 | class PostFactory(factory.mongoengine.MongoEngineFactory): 42 | 43 | class Meta: 44 | model = Post 45 | 46 | @factory.lazy_attribute 47 | def title(self): 48 | return fake.job() 49 | 50 | @factory.lazy_attribute 51 | def author(self): 52 | return fake.name() 53 | 54 | @factory.lazy_attribute 55 | def tags(self): 56 | return [fake.user_name() for _ in range(3)] 57 | 58 | @factory.lazy_attribute 59 | def comments(self): 60 | return CommentFactory.create_batch(5) 61 | 62 | @factory.lazy_attribute 63 | def content(self): 64 | return fake.text() 65 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | default: format test 2 | 3 | test: 4 | @nosetests --with-doctest --rednose --nocapture 5 | 6 | 7 | watch: 8 | @nosetests --rednose --with-watch 9 | 10 | 11 | clean: 12 | @find . -name '*.pyc' -delete 13 | @find . -name '__pycache__' -type d -exec rm -fr {} \; 14 | @rm -rf dist 15 | @rm -f .coverage 16 | @rm -rf htmlcov 17 | @rm -rf build 18 | 19 | format: 20 | @echo "Formating:" 21 | @yapf -dr ./ 22 | @yapf -ir ./ 23 | 24 | req-update: 25 | @pigar 26 | 27 | req: 28 | @pip install -r requirements.txt 29 | @pip install -r requirements_dev.txt -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask.ext.script import Manager 2 | 3 | from api import app, logger 4 | from factories import PostFactory, UserFactory 5 | from models import User 6 | 7 | manager = Manager(app) 8 | 9 | 10 | @manager.command 11 | def init(): 12 | """ 13 | Populate data 14 | """ 15 | User.objects.filter(email='idella00@hotmail.com').delete() 16 | user = UserFactory(email='idella00@hotmail.com') 17 | logger.debug('User %s', user) 18 | posts = PostFactory.create_batch(10, author=user) 19 | logger.debug('Created posts %s', posts) 20 | 21 | 22 | if __name__ == "__main__": 23 | manager.run() 24 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from flask import abort 2 | from mongoengine import * 3 | 4 | 5 | class BaseQuerySet(QuerySet): 6 | """ 7 | A base queryset with handy extras 8 | """ 9 | 10 | def get_or_404(self, *args, **kwargs): 11 | try: 12 | return self.get(*args, **kwargs) 13 | except (MultipleObjectsReturned, DoesNotExist, ValidationError): 14 | abort(404) 15 | 16 | def first_or_404(self): 17 | 18 | obj = self.first() 19 | if obj is None: 20 | abort(404) 21 | 22 | return obj 23 | 24 | 25 | class User(Document): 26 | email = StringField(required=True) 27 | first_name = StringField(max_length=50) 28 | last_name = StringField(max_length=50) 29 | password = StringField(max_length=200) 30 | 31 | meta = {'queryset_class': BaseQuerySet} 32 | 33 | 34 | class Comment(EmbeddedDocument): 35 | content = StringField() 36 | name = StringField(max_length=120) 37 | 38 | def __str__(self): 39 | return "Comment".format(self=self) 40 | 41 | 42 | class Post(Document): 43 | title = StringField(max_length=120, required=True) 44 | content = StringField(max_length=520, required=True) 45 | author = ReferenceField(User) 46 | tags = ListField(StringField(max_length=30)) 47 | comments = ListField(EmbeddedDocumentField(Comment)) 48 | meta = {'allow_inheritance': True, 'queryset_class': BaseQuerySet} 49 | 50 | def __str__(self): 51 | return "".format(self=self) 52 | -------------------------------------------------------------------------------- /ql.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import lru_cache 3 | 4 | import graphene 5 | import trafaret as t 6 | 7 | from models import Post, User, Comment 8 | 9 | 10 | logger = logging.getLogger(__package__) 11 | 12 | 13 | @lru_cache() 14 | def get_comments_by_id(post_id): 15 | return Post.objects.get(id=post_id).comments 16 | 17 | 18 | def construct(object_type, mongo_obj): 19 | field_names = [f.attname for f in object_type._meta.fields] 20 | if 'id' in field_names: 21 | field_names.append('_id') 22 | kwargs = {attr: val for attr, val in mongo_obj.to_mongo().items() 23 | if attr in field_names} 24 | if '_id' in kwargs: 25 | kwargs['id'] = kwargs.pop('_id') 26 | return object_type(**kwargs) 27 | 28 | 29 | class CommentField(graphene.ObjectType): 30 | content = graphene.String() 31 | name = graphene.String() 32 | 33 | 34 | class PostField(graphene.ObjectType): 35 | id = graphene.String() 36 | title = graphene.String() 37 | tags = graphene.List(graphene.String) 38 | etags = graphene.String() 39 | comments = graphene.List(CommentField) 40 | comments_count = graphene.Int() 41 | 42 | def resolve_etags(self, *a, **_): 43 | return "( {} )".format(self.tags) 44 | 45 | def resolve_comments(self, *a, **_): 46 | return [construct(CommentField, c) for c in get_comments_by_id(self.id)] 47 | 48 | 49 | class UserField(graphene.ObjectType): 50 | id = graphene.String() 51 | email = graphene.String() 52 | last_name = graphene.String() 53 | posts = graphene.List(PostField) 54 | 55 | @graphene.resolve_only_args 56 | def resolve_posts(self): 57 | posts = Post.objects.filter(author=self.id) 58 | return [construct(PostField, p) for p in posts] 59 | 60 | 61 | class UserMutation(graphene.Mutation): 62 | 63 | class Input(object): 64 | """Params for User class""" 65 | first_name = graphene.String() 66 | last_name = graphene.String() 67 | email = graphene.String() 68 | 69 | user = graphene.Field(UserField) 70 | 71 | @classmethod 72 | def mutate(cls, _, info, __): 73 | logger.debug("agrs %s", info) 74 | user_schema = t.Dict({ 75 | 'email': t.String(min_length=2), 76 | 'first_name': t.String(min_length=2), 77 | 'last_name': t.String(min_length=2), 78 | }) 79 | 80 | user_data = user_schema.check(info) 81 | user = User.objects.create(**user_data) 82 | user.save() 83 | return cls(user=construct(UserField, user)) 84 | 85 | 86 | class PostMutation(graphene.Mutation): 87 | 88 | class Input(object): 89 | """Params for Post class""" 90 | user_id = graphene.String() 91 | title = graphene.String() 92 | content = graphene.String() 93 | tags = graphene.List(graphene.String) 94 | 95 | post = graphene.Field(PostField) 96 | 97 | @classmethod 98 | def mutate(cls, _, info, __): 99 | logger.debug("agrs %s", info) 100 | post_schema = t.Dict({ 101 | 'title': t.String(min_length=2), 102 | 'user_id': t.String(min_length=2), 103 | 'content': t.String(min_length=2), 104 | t.Key('tags', 105 | optional=True): t.List(t.String, 106 | min_length=1), 107 | }) 108 | 109 | post_data = post_schema.check(info) 110 | user_id = post_data.pop('user_id') 111 | user = User.objects.get_or_404(id=user_id) 112 | post = Post(author=user, **post_data) 113 | post.save() 114 | return cls(post=construct(PostField, post)) 115 | 116 | 117 | class CommentMutation(graphene.Mutation): 118 | 119 | class Input(object): 120 | """Params for Comment class""" 121 | post_id = graphene.String() 122 | content = graphene.String() 123 | name = graphene.String() 124 | 125 | comment = graphene.Field(CommentField) 126 | post = graphene.Field(PostField) 127 | 128 | @classmethod 129 | def mutate(cls, _, info, __): 130 | logger.debug("agrs %s", info) 131 | comment_schema = t.Dict({ 132 | 'name': t.String(min_length=2, max_length=30), 133 | 'post_id': t.String(min_length=2, max_length=30), 134 | 'content': t.String(min_length=2), }) 135 | 136 | comment_data = comment_schema.check(info) 137 | post_id = comment_data.pop('post_id') 138 | post = Post.objects.get_or_404(id=post_id) 139 | comment = Comment(**comment_data) 140 | post.comments.append(comment) 141 | post.save() 142 | return cls(post=construct(PostField, post), comment=construct(CommentField, comment)) 143 | 144 | 145 | class UserQuery(graphene.ObjectType): 146 | user = graphene.Field(UserField, email=graphene.Argument(graphene.String)) 147 | ping = graphene.String(description='Ping someone', 148 | to=graphene.Argument(graphene.String)) 149 | 150 | def resolve_user(self, args, info): 151 | u = User.objects.get(email=args.get('email')) 152 | return construct(UserField, u) 153 | 154 | def resolve_ping(self, args, info): 155 | return 'Pinging {}'.format(args.get('to')) 156 | 157 | 158 | class UserMutationQuery(graphene.ObjectType): 159 | create_user = graphene.Field(UserMutation) 160 | create_post = graphene.Field(PostMutation) 161 | make_comment = graphene.Field(CommentMutation) 162 | 163 | schema = graphene.Schema(query=UserQuery, mutation=UserMutationQuery) 164 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements automatically generated by pigar. 2 | # https://github.com/Damnever/pigar 3 | 4 | # utils.py: 4 5 | Flask_API == 0.6.4 6 | 7 | # settings.py: 67 8 | coloredlogs == 5.0 9 | 10 | # runtime.py: 2 11 | gunicorn == 19.3.0 12 | 13 | # factories.py: 2 14 | fake_factory == 0.5.3 15 | 16 | # api.py: 6,7,8 17 | # manage.py: 1 18 | # models.py: 1 19 | # tests.py: 1,2 20 | Flask == 0.10.1 21 | 22 | # api.py: 10 23 | flask_swagger == 0.2.10 24 | 25 | # ql.py: 2 26 | graphene == 0.4.3 27 | 28 | # models.py: 2 29 | # settings.py: 2 30 | mongoengine == 0.10.0 31 | 32 | # api.py: 9 33 | Flask_DebugToolbar == 0.10.0 34 | 35 | # api.py: 3 36 | trafaret == 0.6.1 37 | 38 | # factories.py: 1 39 | factory_boy == 2.6.0 40 | 41 | # runtime.py: 3 42 | pigar == 0.6.3 43 | 44 | 45 | # api.py: 2 46 | python_status == 1.0.1 47 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | Flask-Script 2 | rednose==0.4.3 3 | gunicorn 4 | pigar 5 | isort 6 | nose 7 | Flask-Testing 8 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import logging.config as log_config 2 | import os 3 | 4 | from mongoengine import connect 5 | 6 | 7 | class DevConfig(object): 8 | DEBUG = True if os.getenv('DEBUG', default='') else False 9 | 10 | SECRET_KEY = 'sdfsdf82347$$%$%$%$&fsdfs!!ASx+__WEBB$' 11 | 12 | MONGODB_IP = os.getenv('DB_PORT_27017_TCP_ADDR', '127.0.0.1') 13 | MONGODB_SETTINGS = { 14 | 'db': 'tumblelog', 15 | 'host': MONGODB_IP, 16 | 'port': 27017 17 | } 18 | connect(**MONGODB_SETTINGS) 19 | LOGGING = { 20 | 'version': 1, 21 | 'disable_existing_loggers': True, 22 | 'formatters': { 23 | 'verbose': { 24 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 25 | }, 26 | 'simple': { 27 | 'format': '%(levelname)s %(message)s' 28 | }, 29 | }, 30 | 'filters': { 31 | 32 | }, 33 | 'handlers': { 34 | 'console': { 35 | 'level': 'DEBUG', 36 | 'class': 'logging.StreamHandler', 37 | 'formatter': 'verbose' 38 | }, 39 | 'null': { 40 | 'level': 'ERROR', 41 | 'class': 'logging.NullHandler', 42 | }, 43 | 44 | }, 45 | 'loggers': { 46 | 'flask': { 47 | 'handlers': ['console'], 48 | 'propagate': False, 49 | }, 50 | 'factory': { 51 | 'handlers': ['null'], 52 | 'level': 'ERROR', 53 | 'propagate': False, 54 | }, 55 | 56 | 'app': { 57 | 'handlers': ['console'], 58 | 'level': 'DEBUG', 59 | }, 60 | '': { 61 | 'handlers': ['null'], 62 | 'level': 'ERROR', 63 | 'propagate': False, 64 | }, 65 | } 66 | } 67 | log_config.dictConfig(LOGGING) 68 | import coloredlogs 69 | coloredlogs.install() 70 | -------------------------------------------------------------------------------- /start_web.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | exec gunicorn api:app -b 0.0.0.0:5000 -------------------------------------------------------------------------------- /static/css/graphiql.min.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | overflow: hidden; 5 | width: 100%; 6 | } 7 | 8 | #graphiql-container { 9 | color: #141823; 10 | width: 100%; 11 | display: -webkit-flex; 12 | display: flex; 13 | -webkit-flex-direction: row; 14 | flex-direction: row; 15 | height: 100%; 16 | font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; 17 | font-size: 14px; 18 | } 19 | 20 | #graphiql-container .editorWrap { 21 | display: -webkit-flex; 22 | display: flex; 23 | -webkit-flex-direction: column; 24 | flex-direction: column; 25 | -webkit-flex: 1; 26 | flex: 1; 27 | } 28 | 29 | #graphiql-container .title { 30 | font-size: 18px; 31 | } 32 | 33 | #graphiql-container .title em { 34 | font-family: georgia; 35 | font-size: 19px; 36 | } 37 | 38 | #graphiql-container .topBarWrap { 39 | display: -webkit-flex; 40 | display: flex; 41 | -webkit-flex-direction: row; 42 | flex-direction: row; 43 | } 44 | 45 | #graphiql-container .topBar { 46 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 47 | background: linear-gradient(#f7f7f7, #e2e2e2); 48 | border-bottom: solid 1px #d0d0d0; 49 | cursor: default; 50 | height: 34px; 51 | padding: 7px 14px 6px; 52 | -webkit-user-select: none; 53 | user-select: none; 54 | display: -webkit-flex; 55 | display: flex; 56 | -webkit-flex-direction: row; 57 | flex-direction: row; 58 | -webkit-flex: 1; 59 | flex: 1; 60 | -webkit-align-items: center; 61 | align-items: center; 62 | } 63 | 64 | #graphiql-container .docExplorerShow { 65 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 66 | background: linear-gradient(#f7f7f7, #e2e2e2); 67 | border: none; 68 | border-bottom: solid 1px #d0d0d0; 69 | border-left: solid 1px rgba(0, 0, 0, 0.2); 70 | color: #3B5998; 71 | cursor: pointer; 72 | font-size: 14px; 73 | outline: 0; 74 | padding: 2px 20px 0 18px; 75 | } 76 | 77 | #graphiql-container .docExplorerShow:before { 78 | border-left: 2px solid #3B5998; 79 | border-top: 2px solid #3B5998; 80 | content: ''; 81 | display: inline-block; 82 | height: 9px; 83 | margin: 0 3px -1px 0; 84 | position: relative; 85 | width: 9px; 86 | -webkit-transform: rotate(-45deg); 87 | transform: rotate(-45deg); 88 | } 89 | 90 | #graphiql-container .editorBar { 91 | display: -webkit-flex; 92 | display: flex; 93 | -webkit-flex-direction: row; 94 | flex-direction: row; 95 | -webkit-flex: 1; 96 | flex: 1; 97 | } 98 | 99 | #graphiql-container .queryWrap { 100 | display: -webkit-flex; 101 | display: flex; 102 | -webkit-flex-direction: column; 103 | flex-direction: column; 104 | -webkit-flex: 1; 105 | flex: 1; 106 | } 107 | 108 | #graphiql-container .resultWrap { 109 | display: -webkit-flex; 110 | display: flex; 111 | -webkit-flex-direction: column; 112 | flex-direction: column; 113 | -webkit-flex: 1; 114 | flex: 1; 115 | border-left: solid 1px #e0e0e0; 116 | } 117 | 118 | #graphiql-container .docExplorerWrap { 119 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 120 | z-index: 3; 121 | position: relative; 122 | background: white; 123 | } 124 | 125 | #graphiql-container .docExplorerResizer { 126 | cursor: col-resize; 127 | height: 100%; 128 | left: -5px; 129 | position: absolute; 130 | top: 0; 131 | width: 10px; 132 | z-index: 10; 133 | } 134 | 135 | #graphiql-container .docExplorerHide { 136 | cursor: pointer; 137 | font-size: 18px; 138 | margin: -7px -8px -6px 0; 139 | padding: 18px 16px 15px 12px; 140 | } 141 | 142 | #graphiql-container .query-editor { 143 | -webkit-flex: 1; 144 | flex: 1; 145 | position: relative; 146 | } 147 | 148 | #graphiql-container .variable-editor { 149 | height: 30px; 150 | display: -webkit-flex; 151 | display: flex; 152 | -webkit-flex-direction: column; 153 | flex-direction: column; 154 | position: relative; 155 | } 156 | 157 | #graphiql-container .variable-editor-title { 158 | background: #eeeeee; 159 | border-bottom: solid 1px #d6d6d6; 160 | border-top: solid 1px #e0e0e0; 161 | color: #777; 162 | font-variant: small-caps; 163 | font-weight: bold; 164 | letter-spacing: 1px; 165 | line-height: 14px; 166 | padding: 6px 0 8px 43px; 167 | text-transform: lowercase; 168 | -webkit-user-select: none; 169 | user-select: none; 170 | } 171 | 172 | #graphiql-container .codemirrorWrap { 173 | -webkit-flex: 1; 174 | flex: 1; 175 | position: relative; 176 | } 177 | 178 | #graphiql-container .result-window { 179 | -webkit-flex: 1; 180 | flex: 1; 181 | position: relative; 182 | } 183 | 184 | #graphiql-container .footer { 185 | background: #f6f7f8; 186 | border-left: solid 1px #e0e0e0; 187 | border-top: solid 1px #e0e0e0; 188 | margin-left: 12px; 189 | position: relative; 190 | } 191 | 192 | #graphiql-container .footer:before { 193 | background: #eeeeee; 194 | bottom: 0; 195 | content: " "; 196 | left: -13px; 197 | position: absolute; 198 | top: -1px; 199 | width: 12px; 200 | } 201 | 202 | #graphiql-container .result-window .CodeMirror { 203 | background: #f6f7f8; 204 | } 205 | 206 | #graphiql-container .result-window .CodeMirror-gutters { 207 | background-color: #eeeeee; 208 | border-color: #e0e0e0; 209 | cursor: col-resize; 210 | } 211 | 212 | #graphiql-container .result-window .CodeMirror-foldgutter, 213 | #graphiql-container .result-window .CodeMirror-foldgutter-open:after, 214 | #graphiql-container .result-window .CodeMirror-foldgutter-folded:after { 215 | padding-left: 3px; 216 | } 217 | 218 | #graphiql-container .execute-button { 219 | background: -webkit-linear-gradient(#fdfdfd, #d2d3d6); 220 | background: linear-gradient(#fdfdfd, #d2d3d6); 221 | border: solid 1px rgba(0,0,0,0.25); 222 | border-radius: 17px; 223 | box-shadow: 0 1px 0 #fff; 224 | cursor: pointer; 225 | fill: #444; 226 | height: 34px; 227 | margin: 0 14px 0 28px; 228 | padding: 0; 229 | width: 34px; 230 | } 231 | 232 | #graphiql-container .execute-button:active { 233 | background: -webkit-linear-gradient(#e6e6e6, #c0c0c0); 234 | background: linear-gradient(#e6e6e6, #c0c0c0); 235 | box-shadow: 236 | 0 1px 0 #fff, 237 | inset 0 0 2px rgba(0, 0, 0, 0.3), 238 | inset 0 0 6px rgba(0, 0, 0, 0.2); 239 | } 240 | 241 | #graphiql-container .execute-button:focus { 242 | outline: 0; 243 | } 244 | 245 | #graphiql-container .CodeMirror-scroll { 246 | -webkit-overflow-scrolling: touch; 247 | } 248 | 249 | #graphiql-container .CodeMirror { 250 | position: absolute; 251 | top: 0; 252 | left: 0; 253 | height: 100%; 254 | width: 100%; 255 | font-size: 13px; 256 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 257 | color: #141823; 258 | } 259 | 260 | #graphiql-container .CodeMirror-lines { 261 | padding: 20px 0; 262 | } 263 | 264 | .CodeMirror-hint-information .content { 265 | -webkit-box-orient: vertical; 266 | color: #141823; 267 | display: -webkit-box; 268 | font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; 269 | font-size: 13px; 270 | -webkit-line-clamp: 3; 271 | line-height: 16px; 272 | max-height: 48px; 273 | overflow: hidden; 274 | text-overflow: -o-ellipsis-lastline; 275 | } 276 | 277 | .CodeMirror-hint-information .content p:first-child { 278 | margin-top: 0; 279 | } 280 | 281 | .CodeMirror-hint-information .content p:last-child { 282 | margin-bottom: 0; 283 | } 284 | 285 | .CodeMirror-hint-information .infoType { 286 | color: #30a; 287 | margin-right: 0.5em; 288 | display: inline; 289 | cursor: pointer; 290 | } 291 | 292 | .autoInsertedLeaf.cm-property { 293 | padding: 2px 4px 1px; 294 | margin: -2px -4px -1px; 295 | border-radius: 2px; 296 | border-bottom: solid 2px rgba(255, 255, 255, 0); 297 | -webkit-animation-duration: 6s; 298 | -moz-animation-duration: 6s; 299 | animation-duration: 6s; 300 | -webkit-animation-name: insertionFade; 301 | -moz-animation-name: insertionFade; 302 | animation-name: insertionFade; 303 | } 304 | 305 | @-moz-keyframes insertionFade { 306 | from, to { 307 | background: rgba(255, 255, 255, 0); 308 | border-color: rgba(255, 255, 255, 0); 309 | } 310 | 311 | 15%, 85% { 312 | background: #fbffc9; 313 | border-color: #f0f3c0; 314 | } 315 | } 316 | 317 | @-webkit-keyframes insertionFade { 318 | from, to { 319 | background: rgba(255, 255, 255, 0); 320 | border-color: rgba(255, 255, 255, 0); 321 | } 322 | 323 | 15%, 85% { 324 | background: #fbffc9; 325 | border-color: #f0f3c0; 326 | } 327 | } 328 | 329 | @keyframes insertionFade { 330 | from, to { 331 | background: rgba(255, 255, 255, 0); 332 | border-color: rgba(255, 255, 255, 0); 333 | } 334 | 335 | 15%, 85% { 336 | background: #fbffc9; 337 | border-color: #f0f3c0; 338 | } 339 | } 340 | 341 | div.CodeMirror-lint-tooltip { 342 | background-color: white; 343 | color: #141823; 344 | border: 0; 345 | border-radius: 2px; 346 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 347 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 348 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 349 | font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; 350 | font-size: 13px; 351 | line-height: 16px; 352 | padding: 6px 10px; 353 | opacity: 0; 354 | transition: opacity 0.15s; 355 | -moz-transition: opacity 0.15s; 356 | -webkit-transition: opacity 0.15s; 357 | -o-transition: opacity 0.15s; 358 | -ms-transition: opacity 0.15s; 359 | } 360 | 361 | div.CodeMirror-lint-message-error, div.CodeMirror-lint-message-warning { 362 | padding-left: 23px; 363 | } 364 | 365 | /* COLORS */ 366 | 367 | #graphiql-container .CodeMirror-foldmarker { 368 | border-radius: 4px; 369 | background: #08f; 370 | background: -webkit-linear-gradient(#43A8FF, #0F83E8); 371 | background: linear-gradient(#43A8FF, #0F83E8); 372 | 373 | color: white; 374 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); 375 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); 376 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); 377 | font-family: arial; 378 | line-height: 0; 379 | padding: 0px 4px 1px; 380 | font-size: 12px; 381 | margin: 0 3px; 382 | text-shadow: 0 -1px rgba(0, 0, 0, 0.1); 383 | } 384 | 385 | #graphiql-container div.CodeMirror span.CodeMirror-matchingbracket { 386 | color: #555; 387 | text-decoration: underline; 388 | } 389 | 390 | #graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { 391 | color: #f00; 392 | } 393 | 394 | /* Comment */ 395 | .cm-comment { 396 | color: #999; 397 | } 398 | 399 | /* Punctuation */ 400 | .cm-punctuation { 401 | color: #555; 402 | } 403 | 404 | /* Keyword */ 405 | .cm-keyword { 406 | color: #B11A04; 407 | } 408 | 409 | /* OperationName, FragmentName */ 410 | .cm-def { 411 | color: #D2054E; 412 | } 413 | 414 | /* FieldName */ 415 | .cm-property { 416 | color: #1F61A0; 417 | } 418 | 419 | /* FieldAlias */ 420 | .cm-qualifier { 421 | color: #1C92A9; 422 | } 423 | 424 | /* ArgumentName and ObjectFieldName */ 425 | .cm-attribute { 426 | color: #8B2BB9; 427 | } 428 | 429 | /* Number */ 430 | .cm-number { 431 | color: #2882F9; 432 | } 433 | 434 | /* String */ 435 | .cm-string { 436 | color: #D64292; 437 | } 438 | 439 | /* Boolean */ 440 | .cm-builtin { 441 | color: #D47509; 442 | } 443 | 444 | /* EnumValue */ 445 | .cm-string-2 { 446 | color: #0B7FC7; 447 | } 448 | 449 | /* Variable */ 450 | .cm-variable { 451 | color: #397D13; 452 | } 453 | 454 | /* Directive */ 455 | .cm-meta { 456 | color: #B33086; 457 | } 458 | 459 | /* Type */ 460 | .cm-atom { 461 | color: #CA9800; 462 | } 463 | /* BASICS */ 464 | 465 | .CodeMirror { 466 | /* Set height, width, borders, and global font properties here */ 467 | font-family: monospace; 468 | height: 300px; 469 | color: black; 470 | } 471 | 472 | /* PADDING */ 473 | 474 | .CodeMirror-lines { 475 | padding: 4px 0; /* Vertical padding around content */ 476 | } 477 | .CodeMirror pre { 478 | padding: 0 4px; /* Horizontal padding of content */ 479 | } 480 | 481 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 482 | background-color: white; /* The little square between H and V scrollbars */ 483 | } 484 | 485 | /* GUTTER */ 486 | 487 | .CodeMirror-gutters { 488 | border-right: 1px solid #ddd; 489 | background-color: #f7f7f7; 490 | white-space: nowrap; 491 | } 492 | .CodeMirror-linenumbers {} 493 | .CodeMirror-linenumber { 494 | padding: 0 3px 0 5px; 495 | min-width: 20px; 496 | text-align: right; 497 | color: #999; 498 | white-space: nowrap; 499 | } 500 | 501 | .CodeMirror-guttermarker { color: black; } 502 | .CodeMirror-guttermarker-subtle { color: #999; } 503 | 504 | /* CURSOR */ 505 | 506 | .CodeMirror div.CodeMirror-cursor { 507 | border-left: 1px solid black; 508 | } 509 | /* Shown when moving in bi-directional text */ 510 | .CodeMirror div.CodeMirror-secondarycursor { 511 | border-left: 1px solid silver; 512 | } 513 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursor { 514 | width: auto; 515 | border: 0; 516 | background: #7e7; 517 | } 518 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursors { 519 | z-index: 1; 520 | } 521 | 522 | .cm-animate-fat-cursor { 523 | width: auto; 524 | border: 0; 525 | -webkit-animation: blink 1.06s steps(1) infinite; 526 | -moz-animation: blink 1.06s steps(1) infinite; 527 | animation: blink 1.06s steps(1) infinite; 528 | } 529 | @-moz-keyframes blink { 530 | 0% { background: #7e7; } 531 | 50% { background: none; } 532 | 100% { background: #7e7; } 533 | } 534 | @-webkit-keyframes blink { 535 | 0% { background: #7e7; } 536 | 50% { background: none; } 537 | 100% { background: #7e7; } 538 | } 539 | @keyframes blink { 540 | 0% { background: #7e7; } 541 | 50% { background: none; } 542 | 100% { background: #7e7; } 543 | } 544 | 545 | /* Can style cursor different in overwrite (non-insert) mode */ 546 | div.CodeMirror-overwrite div.CodeMirror-cursor {} 547 | 548 | .cm-tab { display: inline-block; text-decoration: inherit; } 549 | 550 | .CodeMirror-ruler { 551 | border-left: 1px solid #ccc; 552 | position: absolute; 553 | } 554 | 555 | /* DEFAULT THEME */ 556 | 557 | .cm-s-default .cm-keyword {color: #708;} 558 | .cm-s-default .cm-atom {color: #219;} 559 | .cm-s-default .cm-number {color: #164;} 560 | .cm-s-default .cm-def {color: #00f;} 561 | .cm-s-default .cm-variable, 562 | .cm-s-default .cm-punctuation, 563 | .cm-s-default .cm-property, 564 | .cm-s-default .cm-operator {} 565 | .cm-s-default .cm-variable-2 {color: #05a;} 566 | .cm-s-default .cm-variable-3 {color: #085;} 567 | .cm-s-default .cm-comment {color: #a50;} 568 | .cm-s-default .cm-string {color: #a11;} 569 | .cm-s-default .cm-string-2 {color: #f50;} 570 | .cm-s-default .cm-meta {color: #555;} 571 | .cm-s-default .cm-qualifier {color: #555;} 572 | .cm-s-default .cm-builtin {color: #30a;} 573 | .cm-s-default .cm-bracket {color: #997;} 574 | .cm-s-default .cm-tag {color: #170;} 575 | .cm-s-default .cm-attribute {color: #00c;} 576 | .cm-s-default .cm-header {color: blue;} 577 | .cm-s-default .cm-quote {color: #090;} 578 | .cm-s-default .cm-hr {color: #999;} 579 | .cm-s-default .cm-link {color: #00c;} 580 | 581 | .cm-negative {color: #d44;} 582 | .cm-positive {color: #292;} 583 | .cm-header, .cm-strong {font-weight: bold;} 584 | .cm-em {font-style: italic;} 585 | .cm-link {text-decoration: underline;} 586 | .cm-strikethrough {text-decoration: line-through;} 587 | 588 | .cm-s-default .cm-error {color: #f00;} 589 | .cm-invalidchar {color: #f00;} 590 | 591 | .CodeMirror-composing { border-bottom: 2px solid; } 592 | 593 | /* Default styles for common addons */ 594 | 595 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 596 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 597 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 598 | .CodeMirror-activeline-background {background: #e8f2ff;} 599 | 600 | /* STOP */ 601 | 602 | /* The rest of this file contains styles related to the mechanics of 603 | the editor. You probably shouldn't touch them. */ 604 | 605 | .CodeMirror { 606 | position: relative; 607 | overflow: hidden; 608 | background: white; 609 | } 610 | 611 | .CodeMirror-scroll { 612 | overflow: scroll !important; /* Things will break if this is overridden */ 613 | /* 30px is the magic margin used to hide the element's real scrollbars */ 614 | /* See overflow: hidden in .CodeMirror */ 615 | margin-bottom: -30px; margin-right: -30px; 616 | padding-bottom: 30px; 617 | height: 100%; 618 | outline: none; /* Prevent dragging from highlighting the element */ 619 | position: relative; 620 | } 621 | .CodeMirror-sizer { 622 | position: relative; 623 | border-right: 30px solid transparent; 624 | } 625 | 626 | /* The fake, visible scrollbars. Used to force redraw during scrolling 627 | before actuall scrolling happens, thus preventing shaking and 628 | flickering artifacts. */ 629 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 630 | position: absolute; 631 | z-index: 6; 632 | display: none; 633 | } 634 | .CodeMirror-vscrollbar { 635 | right: 0; top: 0; 636 | overflow-x: hidden; 637 | overflow-y: scroll; 638 | } 639 | .CodeMirror-hscrollbar { 640 | bottom: 0; left: 0; 641 | overflow-y: hidden; 642 | overflow-x: scroll; 643 | } 644 | .CodeMirror-scrollbar-filler { 645 | right: 0; bottom: 0; 646 | } 647 | .CodeMirror-gutter-filler { 648 | left: 0; bottom: 0; 649 | } 650 | 651 | .CodeMirror-gutters { 652 | position: absolute; left: 0; top: 0; 653 | z-index: 3; 654 | } 655 | .CodeMirror-gutter { 656 | white-space: normal; 657 | height: 100%; 658 | display: inline-block; 659 | margin-bottom: -30px; 660 | /* Hack to make IE7 behave */ 661 | *zoom:1; 662 | *display:inline; 663 | } 664 | .CodeMirror-gutter-wrapper { 665 | position: absolute; 666 | z-index: 4; 667 | height: 100%; 668 | } 669 | .CodeMirror-gutter-elt { 670 | position: absolute; 671 | cursor: default; 672 | z-index: 4; 673 | } 674 | .CodeMirror-gutter-wrapper { 675 | -webkit-user-select: none; 676 | -moz-user-select: none; 677 | user-select: none; 678 | } 679 | 680 | .CodeMirror-lines { 681 | cursor: text; 682 | min-height: 1px; /* prevents collapsing before first draw */ 683 | } 684 | .CodeMirror pre { 685 | /* Reset some styles that the rest of the page might have set */ 686 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 687 | border-width: 0; 688 | background: transparent; 689 | font-family: inherit; 690 | font-size: inherit; 691 | margin: 0; 692 | white-space: pre; 693 | word-wrap: normal; 694 | line-height: inherit; 695 | color: inherit; 696 | z-index: 2; 697 | position: relative; 698 | overflow: visible; 699 | -webkit-tap-highlight-color: transparent; 700 | } 701 | .CodeMirror-wrap pre { 702 | word-wrap: break-word; 703 | white-space: pre-wrap; 704 | word-break: normal; 705 | } 706 | 707 | .CodeMirror-linebackground { 708 | position: absolute; 709 | left: 0; right: 0; top: 0; bottom: 0; 710 | z-index: 0; 711 | } 712 | 713 | .CodeMirror-linewidget { 714 | position: relative; 715 | z-index: 2; 716 | overflow: auto; 717 | } 718 | 719 | .CodeMirror-widget {} 720 | 721 | .CodeMirror-code { 722 | outline: none; 723 | } 724 | 725 | /* Force content-box sizing for the elements where we expect it */ 726 | .CodeMirror-scroll, 727 | .CodeMirror-sizer, 728 | .CodeMirror-gutter, 729 | .CodeMirror-gutters, 730 | .CodeMirror-linenumber { 731 | -moz-box-sizing: content-box; 732 | box-sizing: content-box; 733 | } 734 | 735 | .CodeMirror-measure { 736 | position: absolute; 737 | width: 100%; 738 | height: 0; 739 | overflow: hidden; 740 | visibility: hidden; 741 | } 742 | .CodeMirror-measure pre { position: static; } 743 | 744 | .CodeMirror div.CodeMirror-cursor { 745 | position: absolute; 746 | border-right: none; 747 | width: 0; 748 | } 749 | 750 | div.CodeMirror-cursors { 751 | visibility: hidden; 752 | position: relative; 753 | z-index: 3; 754 | } 755 | .CodeMirror-focused div.CodeMirror-cursors { 756 | visibility: visible; 757 | } 758 | 759 | .CodeMirror-selected { background: #d9d9d9; } 760 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 761 | .CodeMirror-crosshair { cursor: crosshair; } 762 | .CodeMirror ::selection { background: #d7d4f0; } 763 | .CodeMirror ::-moz-selection { background: #d7d4f0; } 764 | 765 | .cm-searching { 766 | background: #ffa; 767 | background: rgba(255, 255, 0, .4); 768 | } 769 | 770 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 771 | .CodeMirror span { *vertical-align: text-bottom; } 772 | 773 | /* Used to force a border model for a node */ 774 | .cm-force-border { padding-right: .1px; } 775 | 776 | @media print { 777 | /* Hide the cursor when printing */ 778 | .CodeMirror div.CodeMirror-cursors { 779 | visibility: hidden; 780 | } 781 | } 782 | 783 | /* See issue #2901 */ 784 | .cm-tab-wrap-hack:after { content: ''; } 785 | 786 | /* Help users use markselection to safely style text background */ 787 | span.CodeMirror-selectedtext { background: none; } 788 | #graphiql-container .doc-explorer { 789 | background: white; 790 | } 791 | 792 | #graphiql-container .doc-explorer-title-bar { 793 | cursor: default; 794 | display: -webkit-flex; 795 | display: flex; 796 | height: 34px; 797 | line-height: 14px; 798 | padding: 8px 8px 5px; 799 | position: relative; 800 | -webkit-user-select: none; 801 | user-select: none; 802 | } 803 | 804 | #graphiql-container .doc-explorer-title { 805 | padding: 10px 0 10px 10px; 806 | font-weight: bold; 807 | text-align: center; 808 | text-overflow: ellipsis; 809 | white-space: nowrap; 810 | overflow-x: hidden; 811 | -webkit-flex: 1; 812 | flex: 1; 813 | } 814 | 815 | #graphiql-container .doc-explorer-back { 816 | color: #3B5998; 817 | cursor: pointer; 818 | margin: -7px 0 -6px -8px; 819 | overflow-x: hidden; 820 | padding: 17px 12px 16px 16px; 821 | text-overflow: ellipsis; 822 | white-space: nowrap; 823 | } 824 | 825 | #graphiql-container .doc-explorer-back:before { 826 | border-left: 2px solid #3B5998; 827 | border-top: 2px solid #3B5998; 828 | content: ''; 829 | display: inline-block; 830 | height: 9px; 831 | margin: 0 3px -1px 0; 832 | position: relative; 833 | width: 9px; 834 | -webkit-transform: rotate(-45deg); 835 | transform: rotate(-45deg); 836 | } 837 | 838 | #graphiql-container .doc-explorer-rhs { 839 | position: relative; 840 | } 841 | 842 | #graphiql-container .doc-explorer-contents { 843 | background-color: #ffffff; 844 | border-top: 1px solid #d6d6d6; 845 | bottom: 0; 846 | left: 0; 847 | min-width: 300px; 848 | overflow-y: auto; 849 | padding: 20px 15px; 850 | position: absolute; 851 | right: 0; 852 | top: 47px; 853 | } 854 | 855 | #graphiql-container .doc-type-description p:first-child , 856 | #graphiql-container .doc-type-description blockquote:first-child { 857 | margin-top: 0; 858 | } 859 | 860 | #graphiql-container .doc-explorer-contents a { 861 | cursor: pointer; 862 | text-decoration: none; 863 | } 864 | 865 | #graphiql-container .doc-explorer-contents a:hover { 866 | text-decoration: underline; 867 | } 868 | 869 | #graphiql-container .doc-value-description { 870 | padding: 4px 0 8px 12px; 871 | } 872 | 873 | #graphiql-container .doc-category { 874 | margin: 20px 0; 875 | } 876 | 877 | #graphiql-container .doc-category-title { 878 | border-bottom: 1px solid #e0e0e0; 879 | color: #777; 880 | cursor: default; 881 | font-size: 14px; 882 | font-variant: small-caps; 883 | font-weight: bold; 884 | letter-spacing: 1px; 885 | margin: 0 -15px 10px 0; 886 | padding: 10px 0; 887 | -webkit-user-select: none; 888 | user-select: none; 889 | } 890 | 891 | #graphiql-container .doc-category-item { 892 | margin: 12px 0; 893 | color: #555; 894 | } 895 | 896 | #graphiql-container .keyword { 897 | color: #B11A04; 898 | } 899 | 900 | #graphiql-container .type-name { 901 | color: #CA9800; 902 | } 903 | 904 | #graphiql-container .field-name { 905 | color: #1F61A0; 906 | } 907 | 908 | #graphiql-container .value-name { 909 | color: #0B7FC7; 910 | } 911 | 912 | #graphiql-container .arg-name { 913 | color: #8B2BB9; 914 | } 915 | 916 | #graphiql-container .arg:after { 917 | content: ', '; 918 | } 919 | 920 | #graphiql-container .arg:last-child:after { 921 | content: ''; 922 | } 923 | .CodeMirror-foldmarker { 924 | color: blue; 925 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 926 | font-family: arial; 927 | line-height: .3; 928 | cursor: pointer; 929 | } 930 | .CodeMirror-foldgutter { 931 | width: .7em; 932 | } 933 | .CodeMirror-foldgutter-open, 934 | .CodeMirror-foldgutter-folded { 935 | cursor: pointer; 936 | } 937 | .CodeMirror-foldgutter-open:after { 938 | content: "\25BE"; 939 | } 940 | .CodeMirror-foldgutter-folded:after { 941 | content: "\25B8"; 942 | } 943 | /* The lint marker gutter */ 944 | .CodeMirror-lint-markers { 945 | width: 16px; 946 | } 947 | 948 | .CodeMirror-lint-tooltip { 949 | background-color: infobackground; 950 | border: 1px solid black; 951 | border-radius: 4px 4px 4px 4px; 952 | color: infotext; 953 | font-family: monospace; 954 | font-size: 10pt; 955 | overflow: hidden; 956 | padding: 2px 5px; 957 | position: fixed; 958 | white-space: pre; 959 | white-space: pre-wrap; 960 | z-index: 100; 961 | max-width: 600px; 962 | opacity: 0; 963 | transition: opacity .4s; 964 | -moz-transition: opacity .4s; 965 | -webkit-transition: opacity .4s; 966 | -o-transition: opacity .4s; 967 | -ms-transition: opacity .4s; 968 | } 969 | 970 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { 971 | background-position: left bottom; 972 | background-repeat: repeat-x; 973 | } 974 | 975 | .CodeMirror-lint-mark-error { 976 | background-image: 977 | url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") 978 | ; 979 | } 980 | 981 | .CodeMirror-lint-mark-warning { 982 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 983 | } 984 | 985 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { 986 | background-position: center center; 987 | background-repeat: no-repeat; 988 | cursor: pointer; 989 | display: inline-block; 990 | height: 16px; 991 | width: 16px; 992 | vertical-align: middle; 993 | position: relative; 994 | } 995 | 996 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { 997 | padding-left: 18px; 998 | background-position: top left; 999 | background-repeat: no-repeat; 1000 | } 1001 | 1002 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 1003 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); 1004 | } 1005 | 1006 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 1007 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); 1008 | } 1009 | 1010 | .CodeMirror-lint-marker-multiple { 1011 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); 1012 | background-repeat: no-repeat; 1013 | background-position: right bottom; 1014 | width: 100%; height: 100%; 1015 | } 1016 | .CodeMirror-hints { 1017 | background: white; 1018 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1019 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1020 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1021 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 1022 | font-size: 13px; 1023 | list-style: none; 1024 | margin: 0; 1025 | margin-left: -6px; 1026 | max-height: 14.5em; 1027 | overflow-y: auto; 1028 | overflow: hidden; 1029 | padding: 0; 1030 | position: absolute; 1031 | z-index: 10; 1032 | } 1033 | 1034 | .CodeMirror-hints-wrapper { 1035 | background: white; 1036 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1037 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1038 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1039 | margin-left: -6px; 1040 | position: absolute; 1041 | z-index: 10; 1042 | } 1043 | 1044 | .CodeMirror-hints-wrapper .CodeMirror-hints { 1045 | -webkit-box-shadow: none; 1046 | -moz-box-shadow: none; 1047 | box-shadow: none; 1048 | position: relative; 1049 | margin-left: 0; 1050 | z-index: 0; 1051 | } 1052 | 1053 | .CodeMirror-hint { 1054 | border-top: solid 1px #f7f7f7; 1055 | color: #141823; 1056 | cursor: pointer; 1057 | margin: 0; 1058 | max-width: 300px; 1059 | overflow: hidden; 1060 | padding: 2px 6px; 1061 | white-space: pre; 1062 | } 1063 | 1064 | li.CodeMirror-hint-active { 1065 | background-color: #08f; 1066 | border-top-color: white; 1067 | color: white; 1068 | } 1069 | 1070 | .CodeMirror-hint-information { 1071 | border-top: solid 1px #c0c0c0; 1072 | max-width: 300px; 1073 | padding: 4px 6px; 1074 | position: relative; 1075 | z-index: 1; 1076 | } 1077 | 1078 | .CodeMirror-hint-information:first-child { 1079 | border-bottom: solid 1px #c0c0c0; 1080 | border-top: none; 1081 | margin-bottom: -1px; 1082 | } 1083 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | flask-graphql-playground 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Loading... 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /static/js/app-graphiql.js: -------------------------------------------------------------------------------- 1 | $(function (global) { 2 | /** 3 | * This GraphiQL example illustrates how to use some of GraphiQL's props 4 | * in order to enable reading and updating the URL parameters, making 5 | * link sharing of queries a little bit easier. 6 | * 7 | * This is only one example of this kind of feature, GraphiQL exposes 8 | * various React params to enable interesting integrations. 9 | */ 10 | 11 | // Parse the search string to get url parameters. 12 | var search = window.location.search; 13 | var parameters = {}; 14 | search.substr(1).split('&').forEach(function (entry) { 15 | var eq = entry.indexOf('='); 16 | if (eq >= 0) { 17 | parameters[decodeURIComponent(entry.slice(0, eq))] = 18 | decodeURIComponent(entry.slice(eq + 1)); 19 | } 20 | }); 21 | 22 | // if variables was provided, try to format it. 23 | if (parameters.variables) { 24 | try { 25 | parameters.variables = 26 | JSON.stringify(JSON.parse(query.variables), null, 2); 27 | } catch (e) { 28 | // Do nothing 29 | } 30 | } 31 | 32 | // When the query and variables string is edited, update the URL bar so 33 | // that it can be easily shared 34 | function onEditQuery(newQuery) { 35 | parameters.query = newQuery; 36 | updateURL(); 37 | } 38 | 39 | function onEditVariables(newVariables) { 40 | parameters.variables = newVariables; 41 | updateURL(); 42 | } 43 | 44 | function updateURL() { 45 | var newSearch = '?' + Object.keys(parameters).map(function (key) { 46 | return encodeURIComponent(key) + '=' + 47 | encodeURIComponent(parameters[key]); 48 | }).join('&'); 49 | history.replaceState(null, null, newSearch); 50 | } 51 | 52 | // Defines a GraphQL fetcher using the fetch API. 53 | function graphQLFetcher(graphQLParams) { 54 | return fetch(window.location.origin + '/graph-query', { 55 | method: 'post', 56 | headers: { 'Content-Type': 'application/json' }, 57 | body: JSON.stringify(graphQLParams) 58 | }).then(function (response) { 59 | return response.json(); 60 | }); 61 | } 62 | 63 | global.renderGraphiql = function (elem) { 64 | // Render into the body. 65 | var toolbar = React.createElement(GraphiQL.Toolbar, {}, [ 66 | "Source available at ", 67 | React.createElement("a", { 68 | href: "https://github.com/msoedov", 69 | }, "github") 70 | ]); 71 | React.render( 72 | React.createElement(GraphiQL, { 73 | fetcher: graphQLFetcher, 74 | query: parameters.query, 75 | variables: parameters.variables, 76 | onEditQuery: onEditQuery, 77 | onEditVariables: onEditVariables, 78 | defaultQuery: "# Welcome to GraphiQL\n" + 79 | "#\n" + 80 | "# GraphiQL is an in-browser IDE for writing, validating, and\n" + 81 | "# testing GraphQL queries.\n" + 82 | "#\n" + 83 | "# Type queries into this side of the screen, and you will\n" + 84 | "# see intelligent typeaheads aware of the current GraphQL type schema and\n" + 85 | "# live syntax and validation errors highlighted within the text.\n" + 86 | "#\n" + 87 | "# To bring up the auto-complete at any point, just press Ctrl-Space.\n" + 88 | "#\n" + 89 | "# Press the run button above, or Cmd-Enter to execute the query, and the result\n" + 90 | "# will appear in the pane to the right.\n\n" + 91 | "query Yo {\n user(email: \"idella00@hotmail.com\" )" + 92 | "{\n email,\n posts {\n title\n etags\n tags\n comments {\n name\n content\n }\n }\n }\n}\n" 93 | }, toolbar), 94 | elem 95 | ); 96 | } 97 | }(window)) 98 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from flask.ext.testing import TestCase 2 | 3 | from api import app, schema, GraphQLError 4 | from factories import PostFactory, UserFactory 5 | from utils import run_query 6 | 7 | 8 | class AssertionMixin(object): 9 | 10 | """Helpfull assertion methods""" 11 | 12 | def assertDictContainsSubset(self, subset, dictionary, msg=None): 13 | """ 14 | Checks whether dictionary is a superset of subset. 15 | Deprecated from python 3.2 16 | https://bugs.python.org/file32662/issue13248.diff 17 | """ 18 | subset = dict(subset) 19 | dictionary = dict(dictionary) 20 | missing = [] 21 | mismatched = [] 22 | for key, value in subset.items(): 23 | if key not in dictionary: 24 | missing.append(key) 25 | elif value is id: 26 | continue 27 | elif isinstance(dictionary[key], dict) and isinstance(value, dict): 28 | self.assertDictContainsSubset(value, dictionary[key]) 29 | elif value != dictionary[key]: 30 | mismatched.append('%s, expected: %s, actual: %s' % 31 | ((key), (value), 32 | (dictionary[key]))) 33 | 34 | if not (missing or mismatched): 35 | return 36 | 37 | standardMsg = '' 38 | if missing: 39 | standardMsg = 'Missing: %s' % ','.join((m) for m in 40 | missing) 41 | if mismatched: 42 | if standardMsg: 43 | standardMsg += '; ' 44 | standardMsg += 'Mismatched values: %s' % ','.join(mismatched) 45 | 46 | self.fail(self._formatMessage(msg, standardMsg)) 47 | 48 | 49 | class QueryTestCase(AssertionMixin, TestCase): 50 | 51 | def create_app(self): 52 | return app 53 | 54 | def test_user_creation(self): 55 | query = """ 56 | mutation myFirstMutation { 57 | createUser(email: "email@host.com", firstName: "Joe", lastName: "Doe") { 58 | user { 59 | id 60 | } 61 | } 62 | } 63 | """ 64 | data = run_query(schema, query) 65 | 66 | expect = { 67 | "createUser": { 68 | "user": { 69 | 'id': id 70 | }, 71 | } 72 | } 73 | self.assertDictContainsSubset(expect, data) 74 | 75 | def test_user_creation_validation_error(self): 76 | query = """ 77 | mutation myFirstMutation { 78 | createUser(email: 178, firstName: "Joe", lastName: "Doe") { 79 | user { 80 | id 81 | } 82 | } 83 | } 84 | """ 85 | with self.assertRaises(GraphQLError): 86 | run_query(schema, query) 87 | 88 | def test_post_creation(self): 89 | user = UserFactory() 90 | query = """ 91 | mutation myFirstMutation { 92 | createPost(userId: "%s", title: "Just do it", content: "Yesterday you sad tomorrow") { 93 | post { 94 | id 95 | } 96 | } 97 | } 98 | """ % user.id 99 | data = run_query(schema, query) 100 | 101 | expect = { 102 | "createPost": { 103 | "post": { 104 | 'id': id 105 | }, 106 | } 107 | } 108 | self.assertDictContainsSubset(expect, data) 109 | 110 | def test_make_commnet(self): 111 | post = PostFactory() 112 | query = """ 113 | mutation myFirstMutation { 114 | makeComment(postId: "%s", name: "Just do it", content: "Yesterday you sad tomorrow") { 115 | post { 116 | id 117 | } 118 | } 119 | } 120 | """ % post.id 121 | data = run_query(schema, query) 122 | 123 | expect = { 124 | "makeComment": { 125 | "post": { 126 | 'id': id 127 | }, 128 | } 129 | } 130 | self.assertDictContainsSubset(expect, data) 131 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from flask_api import parsers 3 | from graphql.core.error import GraphQLError, format_error 4 | 5 | from factories import * 6 | from models import * 7 | 8 | 9 | logger = logging.getLogger(__package__) 10 | 11 | 12 | class GraphQLParser(parsers.BaseParser): 13 | 14 | """docstring for GraphQLParser""" 15 | 16 | # media_type = 'application/graphql' 17 | media_type = '*/*' 18 | 19 | def parse(self, stream, media_type, content_length=None): 20 | data = stream.read().decode('ascii') 21 | return data 22 | 23 | 24 | def form_error(error): 25 | if isinstance(error, GraphQLError): 26 | return format_error(error) 27 | return error 28 | 29 | 30 | def format_result(result): 31 | if result.errors: 32 | logger.debug(result.errors) 33 | raise result.errors[0] 34 | data = result.data 35 | return data 36 | 37 | 38 | def run_query(schema, query): 39 | result = schema.execute(query) 40 | result_hash = format_result(result) 41 | return result_hash 42 | --------------------------------------------------------------------------------