Skip to content

Commit 2ef3abe

Browse files
committed
[ADD] graphql_base and graphql_demo
1 parent 6352709 commit 2ef3abe

24 files changed

Lines changed: 469 additions & 1 deletion

File tree

graphql_base/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this file will be generated after merging

graphql_base/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2018 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3+
4+
from .controllers import GraphQLControllerMixin
5+
from .types import OdooObjectType

graphql_base/__manifest__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2018 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3+
4+
{
5+
"name": "Graphql Base",
6+
"summary": """
7+
Base GraphQL/GraphiQL controller""",
8+
"version": "12.0.1.0.0",
9+
"license": "LGPL-3",
10+
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
11+
"website": "https://github.com/OCA/rest-framework",
12+
"depends": ["base"],
13+
"data": ["views/graphiql.xml"],
14+
"external_dependencies": {"python": ["graphene", "graphql_server"]},
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .main import GraphQLControllerMixin

graphql_base/controllers/main.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2018 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3+
4+
import re
5+
from functools import partial
6+
7+
from graphql_server import (
8+
default_format_error,
9+
encode_execution_results,
10+
json_encode,
11+
load_json_body,
12+
run_http_query,
13+
)
14+
15+
from odoo import http
16+
17+
18+
class GraphQLControllerMixin(object):
19+
@staticmethod
20+
def patch_for_json(path_re):
21+
# this is to avoid Odoo, which assumes json always means json+rpc,
22+
# complaining about "function declared as capable of handling request
23+
# of type 'http' but called with a request of type 'json'"
24+
path_re = re.compile(path_re)
25+
orig_get_request = http.Root.get_request
26+
27+
def get_request(self, httprequest):
28+
if path_re.match(httprequest.path):
29+
return http.HttpRequest(httprequest)
30+
return orig_get_request(self, httprequest)
31+
32+
http.Root.get_request = get_request
33+
34+
def _parse_body(self):
35+
req = http.request.httprequest
36+
# We use mimetype here since we don't need the other
37+
# information provided by content_type
38+
content_type = req.mimetype
39+
if content_type == "application/graphql":
40+
return {"query": req.data.decode("utf8")}
41+
elif content_type == "application/json":
42+
return load_json_body(req.data.decode("utf8"))
43+
elif content_type in (
44+
"application/x-www-form-urlencoded",
45+
"multipart/form-data",
46+
):
47+
return http.request.params
48+
return {}
49+
50+
def _process_request(self, schema, data, catch):
51+
request = http.request.httprequest
52+
execution_results, all_params = run_http_query(
53+
schema,
54+
request.method.lower(),
55+
data,
56+
query_data=request.args,
57+
batch_enabled=False,
58+
catch=catch,
59+
context={"env": http.request.env},
60+
)
61+
result, status_code = encode_execution_results(
62+
execution_results,
63+
is_batch=isinstance(data, list),
64+
format_error=default_format_error,
65+
encode=partial(json_encode, pretty=False),
66+
)
67+
# TODO what to do with status_code?
68+
return http.request.make_response(
69+
result, headers={"Content-Type": "application/json"}
70+
)
71+
72+
def _handle_graphql_request(self, schema):
73+
data = self._parse_body()
74+
return self._process_request(schema, data, catch=False)
75+
76+
def _handle_graphiql_request(self, schema):
77+
req = http.request.httprequest
78+
if req.method == "GET" and req.accept_mimetypes.accept_html:
79+
return http.request.render("graphql_base.graphiql", {})
80+
# this way of passing a GraphQL query over http is not spec compliant
81+
# (https://graphql.org/learn/serving-over-http/), but we use
82+
# this only for our GraphiQL UI, and it works with Odoo's way
83+
# of passing the csrf token
84+
return self._process_request(schema, http.request.params, catch=True)

graphql_base/i18n/graphql_base.pot

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
#
4+
msgid ""
5+
msgstr ""
6+
"Project-Id-Version: Odoo Server 12.0+e\n"
7+
"Report-Msgid-Bugs-To: \n"
8+
"Last-Translator: <>\n"
9+
"Language-Team: \n"
10+
"MIME-Version: 1.0\n"
11+
"Content-Type: text/plain; charset=UTF-8\n"
12+
"Content-Transfer-Encoding: \n"
13+
"Plural-Forms: \n"
14+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
This modules enables the creation of `GraphQL <https://graphql.org/>`_ endpoints.
2+
In itself, it does nothing and must be used by a developer to
3+
create the GraphQL schema and resolvers using
4+
`graphene <https://graphene-python.org/>`_,
5+
and expose them through a controller.
6+
An example is available in the ``graphql_demo`` module.

graphql_base/readme/USAGE.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
To use this module, you need to
2+
3+
- create your graphene schema
4+
- create your controller to expose your GraphQL endpoint,
5+
and optionally a GraphiQL UI.
6+
7+
This module does not attempt to expose the whole Odoo object model.
8+
This could be the purpose of another module based on this one.
9+
We believe however that it is preferable to expose a specific well tested
10+
endpoint for each customer, so as to reduce coupling by knowing precisely
11+
what is exposed and needs to be tested when upgrading Odoo.
9.23 KB
Loading

graphql_base/types.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2018 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3+
4+
import graphene
5+
6+
from odoo import fields
7+
8+
9+
def odoo_attr_resolver(attname, default_value, root, info, **args):
10+
"""An attr resolver that is specialized for Odoo recordsets.
11+
12+
It converts False to None, except for Odoo Boolean fields.
13+
This is necessary because Odoo null values are often represented
14+
as False, and graphene would convert a String field with value False
15+
to "false".
16+
17+
It converts datetimes to the user timezone.
18+
19+
It also raises an error if the attribute is not present, ignoring
20+
any default value, so as to return if the schema declares a field
21+
that is not present in the underlying Odoo model.
22+
"""
23+
value = getattr(root, attname)
24+
field = root._fields.get(attname)
25+
if value is False:
26+
if not isinstance(field, fields.Boolean):
27+
return None
28+
elif isinstance(field, fields.Datetime):
29+
return fields.Datetime.context_timestamp(root, value)
30+
return value
31+
32+
33+
class OdooObjectType(graphene.ObjectType):
34+
"""A graphene ObjectType with an Odoo aware default resolver."""
35+
36+
@classmethod
37+
def __init_subclass_with_meta__(cls, default_resolver=None, **options):
38+
if default_resolver is None:
39+
default_resolver = odoo_attr_resolver
40+
41+
return super(OdooObjectType, cls).__init_subclass_with_meta__(
42+
default_resolver=default_resolver, **options
43+
)

0 commit comments

Comments
 (0)