├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── improve-request.md │ └── improve_request ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Server ├── __init__.py ├── config │ ├── __init__.py │ └── dev.py ├── model │ ├── __init__.py │ ├── account.py │ └── post.py ├── schema │ ├── __init__.py │ ├── fields.py │ ├── mutations │ │ ├── __init__.py │ │ ├── account.py │ │ └── post.py │ ├── queries │ │ ├── __init__.py │ │ ├── account.py │ │ └── post.py │ └── unions.py └── test │ └── __init__.py ├── requirements.txt └── server.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Expected behavior** 11 | A clear and concise description of what you expected to happen. 12 | 13 | **Stacktrace** 14 | ``` 15 | stack trace here 16 | ``` 17 | 18 | **Dependencies (please complete the following information):** 19 | - Python: [e.g. 3.7] 20 | - Graphene: [e.g. over 2.0] 21 | - Mongoengine and etc 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improve-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improve request 3 | about: Create a report to that you want to improve before PR 4 | 5 | --- 6 | 7 | **Describe the issue** 8 | A clear and concise description of what the bug is. 9 | 10 | **Explain the improvement you want** 11 | A clear and concise description of what you want to improve. 12 | 13 | **Codes or files that to be changed** 14 | A clear and concise description of what you want to change. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improve_request: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improve request 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the issue** 8 | A clear and concise description of what the bug is. 9 | 10 | **Explain the improvement you want** 11 | A clear and concise description of what you want to improve. 12 | 13 | **Codes or files that to be changed** 14 | A clear and concise description of what you want to change. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | *.xml 103 | *.iml 104 | Test/app.py 105 | Test/database.py 106 | Test/model.py 107 | Test/schema.py 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at artoria@artoria.us. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lewis 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨✨Flask-GraphQL-Large-Application-Example✨✨ 2 | 3 | ## Summary 4 | This is how I structure my GraphQL server with Flask + Graphene 5 | 6 | ## About 7 | This is basic example of large Flask+Graphene server. 8 | all essential use examples have been covered and advanced use examples will be added step by step 9 | 10 | - Features 11 | - [x] Query example 12 | - [x] Mutauion example 13 | - [x] Union example 14 | - [x] Field example 15 | - [ ] Relay example 16 | - [ ] InputObjectType example 17 | - [ ] Dataloader example 18 | - [ ] Middleware example 19 | - [ ] Interfaces example 20 | - [ ] AbstractTypes example 21 | - [x] Basic authentication example 22 | - [x] MongoDB example 23 | - [ ] MySQL(SQLAlchemy, PeeWee) example 24 | 25 | 26 | ## Project dependencies 27 | - GraphQL framework 28 | - Graphene 29 | - HTTP serving 30 | - Flask-GraphQL 31 | - Flask 32 | - Database ORM 33 | - Mongoengine 34 | - Authentication 35 | - [Flask-GraphQL-Auth](https://flask-graphql-auth.readthedocs.io/en/latest/) (JWT based authentication) 36 | 37 | ## How to test this example? 38 | This repository implemented minitwit to cover examples. Use the following command to run the server 39 | 40 | ```sh 41 | pip install -r requirements.txt 42 | python server.py 43 | ``` 44 | GraphiQL enabled by default. 45 | 46 | ## Diagrams 47 | not covered 48 | 49 | ## Core components 50 | 51 | ### /Server/app/schema 52 | GraphQL 서버를 구성하는 필드, 쿼리, 뮤테이션을 담고 있습니다. 53 | 54 | This directory contains fields, queries, and interactions that make up the GraphQL schema 55 | 56 | #### /Server/app/schema/__init__.py 57 | GraphQL 스키마를 구성하고 구성된 스키마를 Flask 인스턴스에 할당합니다. 58 | 59 | Structure GraphQL schema and add schema with flask.add_url_rule 60 | 61 | #### /Server/app/schema/fields.py 62 | GraphQL 쿼리와 뮤테이션에서 사용하는 필드들 63 | 64 | This file contains the fields that make up the GraphQL schema. 65 | 66 | #### /Server/app/graphql_view/unions.py 67 | GraphQL 쿼리와 뮤테이션에서 사용하는 유니온들 68 | 69 | This file contains the unions that make up the GraphQL schema. 70 | 71 | #### /Server/app/schema/queries 72 | 이 디렉터리는 쿼리와 쿼리 resolver들로 구성됩니다. 유연한 구조를 위해 resolver들을 독립적인 파일에 담습니다 73 | 74 | This directory consists root query class and query resolvers. Place the resolver in an independent file for flexible structure 75 | 76 | #### /Server/app/schema/mutations 77 | 이 디렉터리는 뮤테이션들로 구성됩니다. 유연한 구조를 위해 뮤테이션들을 독립적인 파일에 담습니다 78 | 79 | This directory consists mutations. Place the mutation in an independent file for flexible structure 80 | 81 | ## I refered 82 | ### People 83 | [Syrus Akbary](https://twitter.com/syrusakbary) 84 | 85 | ### Repository 86 | [Flask-Large-Application-Example](https://github.com/JoMingyu/Flask-Large-Application-Example) 87 | 88 | ### Website 89 | [Designing GraphQL mutations](https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97) 90 | [Authorization in GraphQL](https://dev-blog.apollodata.com/authorization-in-graphql-452b1c402a9) 91 | [GraphQL ansd authentication](https://medium.com/the-graphqlhub/graphql-and-authentication-b73aed34bbeb) 92 | 93 | ### Library Docs 94 | [Graphene](https://medium.com/the-graphqlhub/graphql-and-authentication-b73aed34bbeb) 95 | -------------------------------------------------------------------------------- /Server/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from model import Mongo 3 | from schema import Schema 4 | from flask_graphql_auth import GraphQLAuth 5 | 6 | 7 | def create_app(*config_cls): 8 | app = Flask(__name__) 9 | 10 | for config in config_cls: 11 | app.config.from_object(config) 12 | 13 | print("[INFO] Flask application initialized with {0}".format([config.__name__ for config in config_cls])) 14 | 15 | Mongo(app) 16 | Schema(app) 17 | GraphQLAuth(app) 18 | 19 | return app 20 | 21 | 22 | -------------------------------------------------------------------------------- /Server/config/__init__.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | GRAPHIQL = True 3 | DEBUG = False 4 | HOST = '0.0.0.0' 5 | 6 | SERVICE_NAME = 'graphql-flask-example' 7 | 8 | RUN_SETTING = { 9 | 'host': HOST, 10 | 'port': 80, 11 | 'debug': DEBUG 12 | } -------------------------------------------------------------------------------- /Server/config/dev.py: -------------------------------------------------------------------------------- 1 | from config import Config 2 | 3 | class DevConfig(Config): 4 | DEBUG = True 5 | 6 | RUN_SETTING = { 7 | 'host': Config.HOST, 8 | 'port': 5000, 9 | 'debug': DEBUG 10 | } 11 | 12 | MONGODB_SETTINGS = { 13 | 'db': Config.SERVICE_NAME, 14 | 'host': 'mongomock://localhost' 15 | } 16 | 17 | JWT_SECRET_KEY = "affogato" 18 | -------------------------------------------------------------------------------- /Server/model/__init__.py: -------------------------------------------------------------------------------- 1 | from mongoengine import connect 2 | 3 | from model.account import AccountModel 4 | from model.post import PostModel, CommentModel 5 | 6 | 7 | class Mongo: 8 | 9 | def __init__(self, app): 10 | settings = app.config['MONGODB_SETTINGS'] 11 | 12 | connect(**settings) 13 | 14 | print('[INFO] MongoEngine initialized with {}'.format(settings)) -------------------------------------------------------------------------------- /Server/model/account.py: -------------------------------------------------------------------------------- 1 | from mongoengine import * 2 | from datetime import datetime 3 | 4 | 5 | class AccountModel(Document): 6 | meta = {'collection': 'account'} 7 | id = StringField(required=True, primary_key=True) 8 | username = StringField(required=True) 9 | password = StringField(required=True) 10 | register_on = DateTimeField(required=True, default=datetime.now()) -------------------------------------------------------------------------------- /Server/model/post.py: -------------------------------------------------------------------------------- 1 | from mongoengine import * 2 | from .account import AccountModel 3 | from datetime import datetime 4 | 5 | 6 | class CommentModel(Document): 7 | text = StringField(required=True) 8 | author = ReferenceField(AccountModel) 9 | 10 | 11 | class PostModel(Document): 12 | meta = {'collection': 'post'} 13 | id = IntField(required=True, primary_key=True) 14 | title = StringField(required=True) 15 | text = StringField(required=True) 16 | comment = ListField(CommentModel) 17 | author = ReferenceField(AccountModel, required=True) 18 | upload_on = DateTimeField(required=True, default=datetime.now()) -------------------------------------------------------------------------------- /Server/schema/__init__.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from flask_graphql import GraphQLView 3 | from flask import jsonify 4 | 5 | from schema.mutations import Mutation 6 | from schema.queries import Query 7 | 8 | 9 | class Schema: 10 | def __init__(self, app): 11 | schema = graphene.Schema(query=Query, mutation=Mutation) 12 | 13 | app.add_url_rule( 14 | '/graphql', 15 | view_func=GraphQLView.as_view('graphql', 16 | schema=schema, 17 | graphiql=app.config['GRAPHIQL']) 18 | ) 19 | 20 | schema_json = schema.introspect() 21 | 22 | @app.route("/schema") 23 | def schema_view(): 24 | return jsonify(schema_json) 25 | 26 | print('[INFO] GraphQLView was successfully added with GraphiQL:{0}'.format(app.config['GRAPHIQL'])) 27 | -------------------------------------------------------------------------------- /Server/schema/fields.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class AccountField(graphene.ObjectType): 5 | id = graphene.String() 6 | username = graphene.String() 7 | register_on = graphene.String() 8 | 9 | 10 | class CommentField(graphene.ObjectType): 11 | text = graphene.String() 12 | author = graphene.Field(type=AccountField) 13 | 14 | 15 | class ResponseMessageField(graphene.ObjectType): 16 | is_success = graphene.Boolean() 17 | message = graphene.String() 18 | 19 | 20 | class RefreshField(graphene.ObjectType): 21 | access_token = graphene.String() 22 | message = graphene.String() 23 | 24 | 25 | class AuthField(graphene.ObjectType): 26 | access_token = graphene.String() 27 | refresh_token = graphene.String() 28 | message = graphene.String() 29 | 30 | 31 | class PostField(graphene.ObjectType): 32 | id = graphene.String() 33 | title = graphene.String() 34 | text = graphene.String() 35 | upload_on = graphene.DateTime() 36 | comment = graphene.List(of_type=CommentField) 37 | author = graphene.Field(AccountField) 38 | 39 | 40 | class AccountResults(graphene.ObjectType): 41 | accounts = graphene.List(of_type=AccountField) 42 | 43 | 44 | class PostResults(graphene.ObjectType): 45 | posts = graphene.List(of_type=PostField) 46 | -------------------------------------------------------------------------------- /Server/schema/mutations/__init__.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from schema.mutations.post import (PostUploadMutation, PostDeleteMutation, CommentLeaveMutation) 4 | from schema.mutations.account import (AuthMutation, RefreshMutation, RegisterMutation) 5 | 6 | 7 | class Mutation(graphene.ObjectType): 8 | register = RegisterMutation.Field() 9 | auth = AuthMutation.Field() 10 | refresh = RefreshMutation.Field() 11 | post_upload = PostUploadMutation.Field() 12 | post_delete = PostDeleteMutation.Field() 13 | comment_leave = CommentLeaveMutation.Field() 14 | -------------------------------------------------------------------------------- /Server/schema/mutations/account.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | import graphene 4 | from flask_graphql_auth import create_access_token, create_refresh_token, mutation_jwt_refresh_token_required, \ 5 | get_jwt_identity 6 | 7 | from model import AccountModel 8 | from schema.fields import AuthField, ResponseMessageField, RefreshField 9 | from schema.unions import AuthUnion, RefreshUnion 10 | 11 | 12 | class AuthMutation(graphene.Mutation): 13 | class Arguments(object): 14 | id = graphene.String() 15 | password = graphene.String() 16 | 17 | result = graphene.Field(AuthUnion) 18 | 19 | def mutate(self, info, **kwargs): 20 | user = AccountModel.objects(**kwargs).first() 21 | 22 | if user is not None: 23 | access_token = create_access_token(identity=kwargs["id"]) 24 | refresh_token = create_refresh_token(identity=str(uuid4())) 25 | 26 | return AuthMutation(AuthField(access_token=access_token, refresh_token=refresh_token, message="Login Success")) 27 | else: 28 | return AuthMutation(ResponseMessageField(is_success=False, message="Login failed")) 29 | 30 | 31 | class RefreshMutation(graphene.Mutation): 32 | class Arguments(object): 33 | refresh_token = graphene.String() 34 | 35 | result = graphene.Field(RefreshUnion) 36 | 37 | @mutation_jwt_refresh_token_required 38 | def mutate(self, info, refresh_token): 39 | return RefreshMutation(RefreshField(acces_token=create_access_token(get_jwt_identity()), message="Refresh success")) 40 | 41 | 42 | class RegisterMutation(graphene.Mutation): 43 | 44 | class Arguments(object): 45 | id = graphene.String() 46 | username = graphene.String() 47 | password = graphene.String() 48 | description = graphene.String() 49 | 50 | result = graphene.Field(ResponseMessageField) 51 | 52 | @staticmethod 53 | def mutate(root, info, **kwargs): 54 | AccountModel(**kwargs).save() 55 | 56 | return RegisterMutation(ResponseMessageField(is_success=True, message="Successfully registered")) -------------------------------------------------------------------------------- /Server/schema/mutations/post.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from flask_graphql_auth import mutation_jwt_required, get_jwt_identity 3 | 4 | from model import PostModel, AccountModel, CommentModel 5 | from schema.fields import ResponseMessageField 6 | from schema.unions import ResponseUnion 7 | 8 | 9 | class CommentLeaveMutation(graphene.Mutation): 10 | 11 | class Arguments(object): 12 | token = graphene.String() 13 | post_id = graphene.Int() 14 | comment = graphene.String() 15 | 16 | result = graphene.Field(ResponseUnion) 17 | 18 | @classmethod 19 | @mutation_jwt_required 20 | def mutate(cls, _, info, post_id, comment): 21 | post = PostModel.objects(id=post_id).first() 22 | new_comment = CommentModel(text=comment, author=AccountModel.objects(id=get_jwt_identity()).first()) 23 | 24 | if post is None: 25 | return CommentLeaveMutation(ResponseMessageField(is_success=False, message="Unknown post id")) 26 | 27 | post.update_one(push_comment=new_comment) 28 | 29 | return CommentLeaveMutation(ResponseMessageField(is_success=True, message="Comment successfully uploaded")) 30 | 31 | 32 | class PostUploadMutation(graphene.Mutation): 33 | 34 | class Arguments(object): 35 | token = graphene.String() 36 | title = graphene.String() 37 | text = graphene.String() 38 | 39 | result = graphene.Field(ResponseUnion) 40 | 41 | @classmethod 42 | @mutation_jwt_required 43 | def mutate(cls, _, info, title, text): 44 | count = PostModel.objects.count() 45 | new_post = PostModel(id=count+1, 46 | title=title, 47 | text=text, 48 | comment=[], 49 | author=AccountModel.objects(id=get_jwt_identity()).first()) 50 | 51 | new_post.save() 52 | 53 | return PostUploadMutation(ResponseMessageField(is_success=True, 54 | message="Upload successful")) 55 | 56 | 57 | class PostDeleteMutation(graphene.Mutation): 58 | 59 | class Arguments(object): 60 | token = graphene.String() 61 | post_id = graphene.Int() 62 | 63 | result = graphene.Field(ResponseUnion) 64 | 65 | @classmethod 66 | @mutation_jwt_required 67 | def mutate(cls, _, info, token, post_id): 68 | username = get_jwt_identity() 69 | post = PostModel.objects(id=post_id) 70 | 71 | if post and post.author == username: 72 | post.delete() 73 | 74 | return PostDeleteMutation(ResponseMessageField(is_success=True, 75 | message="Delete successful")) 76 | elif post is None: 77 | return PostDeleteMutation(ResponseMessageField(is_success=False, 78 | message="Unknown post id")) 79 | elif post.author != username: 80 | return PostDeleteMutation(ResponseMessageField(is_success=False, 81 | message="No authority to delete this post")) -------------------------------------------------------------------------------- /Server/schema/queries/__init__.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from schema.unions import AccountUnion, PostUnion 4 | from schema.queries.post import resolve_post 5 | from schema.queries.account import resolve_account 6 | 7 | 8 | class Query(graphene.ObjectType): 9 | post = graphene.Field(type=PostUnion, 10 | token=graphene.NonNull(graphene.String), 11 | id=graphene.Int(default_value=None), 12 | title=graphene.String(default_value=None), 13 | resolver=resolve_post) 14 | 15 | account = graphene.Field(type=AccountUnion, 16 | token=graphene.NonNull(graphene.String), 17 | id=graphene.String(default_value=None), 18 | username=graphene.String(default_value=None), 19 | resolver=resolve_account) 20 | -------------------------------------------------------------------------------- /Server/schema/queries/account.py: -------------------------------------------------------------------------------- 1 | from model import AccountModel 2 | from schema.fields import AccountField, ResponseMessageField, AccountResults 3 | 4 | from flask_graphql_auth import query_jwt_required 5 | 6 | 7 | @query_jwt_required 8 | def resolve_account(root, info, **kwargs): 9 | id = kwargs.get('id', None) 10 | username = kwargs.get('username', None) 11 | 12 | accounts = AccountModel.objects(id=id, username=username) 13 | 14 | if accounts.first() is None: 15 | return ResponseMessageField(is_success=False, message="Not found") 16 | 17 | return AccountResults(accounts=[AccountField(id=account.id, 18 | username=account.username, 19 | register_on=account.register_on) 20 | for account in accounts]) 21 | -------------------------------------------------------------------------------- /Server/schema/queries/post.py: -------------------------------------------------------------------------------- 1 | from schema.fields import PostField, AccountField, CommentField, ResponseMessageField, PostResults 2 | from model import PostModel 3 | 4 | from flask_graphql_auth import query_jwt_required 5 | 6 | 7 | @query_jwt_required 8 | def resolve_post(root, info, **kwargs): 9 | id = kwargs.get('id', None) 10 | title = kwargs.get('title', None) 11 | 12 | posts = PostModel.objects(id=id, title=title) 13 | 14 | if posts.first() is None: 15 | return ResponseMessageField(is_success=False, message="Not found") 16 | 17 | return PostResults(posts=[PostField(id=post.id, 18 | title=post.title, 19 | text=post.text, 20 | upload_on=post.upload_on, 21 | comment=[CommentField(text=c.text, 22 | author=AccountField(id=c.author.id, 23 | username=c.author.username, 24 | register_on=c.author.register_on)) 25 | for c in post.comment], 26 | author=AccountField(id=post.author.id, 27 | username=post.author.username, 28 | register_on=post.author.register_on)) 29 | for post in posts]) 30 | -------------------------------------------------------------------------------- /Server/schema/unions.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from flask_graphql_auth import AuthInfoField 3 | 4 | from schema.fields import ResponseMessageField, AccountResults, PostResults 5 | 6 | 7 | AccountUnion = type("AccountUnion", (graphene.Union,), { 8 | "Meta": type("Meta", (), { 9 | "types": (AccountResults, ResponseMessageField, AuthInfoField) 10 | }) 11 | }) 12 | 13 | 14 | PostUnion = type('PostUnion', (graphene.Union, ), { 15 | "Meta": type("Meta", (), { 16 | "types": (PostResults, ResponseMessageField, AuthInfoField) 17 | }) 18 | }) 19 | 20 | 21 | class MutationUnion: 22 | @classmethod 23 | def resolve_type(cls, instance, info): 24 | return type(instance) 25 | 26 | 27 | ResponseUnion = type("ResponseUnion", (MutationUnion, graphene.Union), { 28 | "Meta": type("Meta", (), { 29 | "types": (ResponseMessageField, AuthInfoField) 30 | }) 31 | }) 32 | 33 | AuthUnion = ResponseUnion 34 | RefreshUnion = ResponseUnion 35 | -------------------------------------------------------------------------------- /Server/test/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from config import dev 3 | from Server import create_app 4 | import json 5 | 6 | app = create_app(dev.DevConfig) 7 | 8 | 9 | class BasicTestCase(unittest.TestCase): 10 | 11 | def __init__(self, *args, **kwargs): 12 | unittest.TestCase.__init__(self, *args, **kwargs) 13 | 14 | app.testing = True 15 | self.tester = app.test_client(self) 16 | 17 | def request(self, type, call, body): 18 | query = type + " {" + call + "{" + body + "}" + "}" 19 | 20 | response = self.tester.post('/graphql', 21 | data=json.dumps({"query": query}), 22 | content_type='application/json' 23 | ) 24 | 25 | print(response.json) 26 | return dict(response.json)['data'] 27 | 28 | def _create_fake_data(self): 29 | # Create fake data 30 | pass 31 | 32 | def _get_tokens(self): 33 | response = self.request(type="mutation", 34 | call='auth(id:"{0}", password:"{1}")'.format("fake user's id", "fake user's pw"), 35 | body=''' 36 | result{ 37 | ... on AuthField{ 38 | refreshToken 39 | accessToken 40 | message 41 | } 42 | } 43 | ''') 44 | 45 | response = response['auth'] 46 | self.access_token = response['result']['accessToken'] 47 | self.refresh_token = response['result']['refreshToken'] 48 | 49 | def setUp(self): 50 | self._create_fake_data() 51 | self._get_tokens() 52 | 53 | def tearDown(self): 54 | # teardown code ex. drop tables 55 | pass 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.2 2 | click==6.7 3 | Flask==1.0.2 4 | Flask-GraphQL==2.0.0 5 | Flask-GraphQL-Auth==1.1 6 | Flask-JWT-Extended==3.8.1 7 | graphene==2.1.3 8 | graphql-core==2.1 9 | graphql-relay==0.4.5 10 | graphql-server-core==1.1.1 11 | itsdangerous==0.24 12 | Jinja2>=2.10.1 13 | MarkupSafe==1.0 14 | mock==2.0.0 15 | mongoengine==0.15.0 16 | mongomock==3.10.0 17 | pbr==4.0.2 18 | promise==2.1 19 | PyJWT==1.6.4 20 | pymongo==3.6.1 21 | Rx==1.6.1 22 | sentinels==1.0.0 23 | six==1.11.0 24 | typing==3.6.6 25 | Werkzeug==0.15.3 26 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from Server import create_app 2 | from config.dev import DevConfig 3 | 4 | if __name__ == '__main__': 5 | app = create_app(DevConfig) 6 | 7 | app.run(**DevConfig.RUN_SETTING) --------------------------------------------------------------------------------