diff --git a/graphql_base/README.rst b/graphql_base/README.rst new file mode 100644 index 000000000..39af3cd30 --- /dev/null +++ b/graphql_base/README.rst @@ -0,0 +1,155 @@ +============ +Graphql Base +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7550bc59f47cae17ac3a8ac5e3340fd5e11cdefbc421215be6447377d282a85e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/18.0/graphql_base + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-18-0/rest-framework-18-0-graphql_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This modules enables the creation of `GraphQL `__ +endpoints. In itself, it does nothing and must be used by a developer to +create the GraphQL schema and resolvers using +`graphene `__, and expose them through a +controller. An example is available in the ``graphql_demo`` module. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to + +- create your graphene schema +- create your controller to expose your GraphQL endpoint, and + optionally a GraphiQL IDE. + +This module does not attempt to expose the whole Odoo object model. This +could be the purpose of another module based on this one. We believe +however that it is preferable to expose a specific well tested endpoint +for each customer, so as to reduce coupling by knowing precisely what is +exposed and needs to be tested when upgrading Odoo. + +To start working with this module, we recommend the following approach: + +- Learn `GraphQL basics `__ +- Learn `graphene `__, the python library + used to create GraphQL schemas and resolvers. +- Examine the ``graphql_demo`` module in this repo, copy it, adapt the + controller to suit your needs (routes, authentication methods). +- Start building your own schema and resolver. + +Building your schema +-------------------- + +The schema can be built using native graphene types. An +``odoo.addons.graphql_base.types.OdooObjectType`` is provided as a +convenience. It is a graphene ``ObjectType`` with a default attribute +resolver which: + +- converts False to None (except for Boolean types), to avoid Odoo's + weird ``False`` strings being rendered as json ``"false"``; +- adds the user timezone to Datetime fields; +- raises an error if an attribute is absent to avoid field name typing + errors. + +Creating GraphQL controllers +---------------------------- + +The module provides an +``odoo.addons.graphql_base.GraphQLControllerMixin`` class to help you +build GraphQL controllers providing GraphiQL and/or GraphQL endpoints. + +.. code:: python + + from odoo import http + from odoo.addons.graphql_base import GraphQLControllerMixin + + from ..schema import schema + + + class GraphQLController(http.Controller, GraphQLControllerMixin): + + # The GraphiQL route, providing an IDE for developers + @http.route("/graphiql/demo", auth="user") + def graphiql(self, **kwargs): + return self._handle_graphiql_request(schema) + + # The graphql route, for applications. + # Note csrf=False: you may want to apply extra security + # (such as origin restrictions) to this route. + @http.route("/graphql/demo", auth="user", csrf=False) + def graphql(self, **kwargs): + return self._handle_graphql_request(schema) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Ajay Javiya + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px + :target: https://github.com/sbidoul + :alt: sbidoul + +Current `maintainer `__: + +|maintainer-sbidoul| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/graphql_base/__init__.py b/graphql_base/__init__.py new file mode 100644 index 000000000..7fded2bb6 --- /dev/null +++ b/graphql_base/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from .controllers import GraphQLControllerMixin +from .graphene_types import OdooObjectType, odoo_attr_resolver diff --git a/graphql_base/__manifest__.py b/graphql_base/__manifest__.py new file mode 100644 index 000000000..e61dbda35 --- /dev/null +++ b/graphql_base/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2018 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Graphql Base", + "summary": """ + Base GraphQL/GraphiQL controller""", + "version": "18.0.1.0.0", + "license": "LGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/rest-framework", + "depends": ["base"], + "data": ["views/graphiql.xml"], + "external_dependencies": {"python": ["graphene", "graphql_server"]}, + "development_status": "Production/Stable", + "maintainers": ["sbidoul"], + "installable": True, +} diff --git a/graphql_base/controllers/__init__.py b/graphql_base/controllers/__init__.py new file mode 100644 index 000000000..f38bb6f8e --- /dev/null +++ b/graphql_base/controllers/__init__.py @@ -0,0 +1 @@ +from .main import GraphQLControllerMixin diff --git a/graphql_base/controllers/main.py b/graphql_base/controllers/main.py new file mode 100644 index 000000000..3e675cb06 --- /dev/null +++ b/graphql_base/controllers/main.py @@ -0,0 +1,85 @@ +# Copyright 2018 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from functools import partial + +from graphql_server import ( + HttpQueryError, + encode_execution_results, + format_error_default, + json_encode, + load_json_body, + run_http_query, +) + +from odoo import http + + +class GraphQLControllerMixin: + def _parse_body(self): + req = http.request.httprequest + # We use mimetype here since we don't need the other + # information provided by content_type + content_type = req.mimetype + if content_type == "application/graphql": + return {"query": req.data.decode("utf8")} + elif content_type == "application/json": + return load_json_body(req.data.decode("utf8")) + elif content_type in ( + "application/x-www-form-urlencoded", + "multipart/form-data", + ): + return http.request.params + return {} + + def _process_request(self, schema, data): + try: + request = http.request.httprequest + execution_results, all_params = run_http_query( + schema, + request.method.lower(), + data, + query_data=request.args, + batch_enabled=False, + catch=False, + context_value={"env": http.request.env}, + ) + result, status_code = encode_execution_results( + execution_results, + is_batch=isinstance(data, list), + format_error=format_error_default, + encode=partial(json_encode, pretty=False), + ) + headers = dict() + headers["Content-Type"] = "application/json" + response = http.request.make_response(result, headers=headers) + response.status_code = status_code + if any(er.errors for er in execution_results): + env = http.request.env + env.cr.rollback() + env.clear() + return response + except HttpQueryError as e: + result = json_encode({"errors": [{"message": str(e)}]}) + headers = dict() + headers["Content-Type"] = "application/json" + response = http.request.make_response(result, headers=headers) + response.status_code = e.status_code + env = http.request.env + env.cr.rollback() + env.clear() + return response + + def _handle_graphql_request(self, schema): + data = self._parse_body() + return self._process_request(schema, data) + + def _handle_graphiql_request(self, schema): + req = http.request.httprequest + if req.method == "GET" and req.accept_mimetypes.accept_html: + return http.request.render("graphql_base.graphiql", {}) + # this way of passing a GraphQL query over http is not spec compliant + # (https://graphql.org/learn/serving-over-http/), but we use + # this only for our GraphiQL UI, and it works with Odoo's way + # of passing the csrf token + return self._process_request(schema, http.request.params) diff --git a/graphql_base/graphene_types.py b/graphql_base/graphene_types.py new file mode 100644 index 000000000..6af554c54 --- /dev/null +++ b/graphql_base/graphene_types.py @@ -0,0 +1,43 @@ +# Copyright 2018 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import graphene + +from odoo import fields + + +def odoo_attr_resolver(attname, default_value, root, info, **args): + """An attr resolver that is specialized for Odoo recordsets. + + It converts False to None, except for Odoo Boolean fields. + This is necessary because Odoo null values are often represented + as False, and graphene would convert a String field with value False + to "false". + + It converts datetimes to the user timezone. + + It also raises an error if the attribute is not present, ignoring + any default value, so as to return if the schema declares a field + that is not present in the underlying Odoo model. + """ + value = getattr(root, attname) + field = root._fields.get(attname) + if value is False: + if not isinstance(field, fields.Boolean): + return None + elif isinstance(field, fields.Datetime): + return fields.Datetime.context_timestamp(root, value) + return value + + +class OdooObjectType(graphene.ObjectType): + """A graphene ObjectType with an Odoo aware default resolver.""" + + @classmethod + def __init_subclass_with_meta__(cls, default_resolver=None, **options): + if default_resolver is None: + default_resolver = odoo_attr_resolver + + return super().__init_subclass_with_meta__( + default_resolver=default_resolver, **options + ) diff --git a/graphql_base/i18n/graphql_base.pot b/graphql_base/i18n/graphql_base.pot new file mode 100644 index 000000000..541e88ec4 --- /dev/null +++ b/graphql_base/i18n/graphql_base.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * graphql_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: graphql_base +#: model_terms:ir.ui.view,arch_db:graphql_base.graphiql +msgid "Loading..." +msgstr "" diff --git a/graphql_base/i18n/it.po b/graphql_base/i18n/it.po new file mode 100644 index 000000000..ac2289a84 --- /dev/null +++ b/graphql_base/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * graphql_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-15 17:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: graphql_base +#: model_terms:ir.ui.view,arch_db:graphql_base.graphiql +msgid "Loading..." +msgstr "Caricamento ..." diff --git a/graphql_base/pyproject.toml b/graphql_base/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/graphql_base/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/graphql_base/readme/DESCRIPTION.md b/graphql_base/readme/DESCRIPTION.md new file mode 100644 index 000000000..7e47902af --- /dev/null +++ b/graphql_base/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This modules enables the creation of [GraphQL](https://graphql.org/) +endpoints. In itself, it does nothing and must be used by a developer to +create the GraphQL schema and resolvers using +[graphene](https://graphene-python.org/), and expose them through a +controller. An example is available in the `graphql_demo` module. diff --git a/graphql_base/readme/USAGE.md b/graphql_base/readme/USAGE.md new file mode 100644 index 000000000..067e857ac --- /dev/null +++ b/graphql_base/readme/USAGE.md @@ -0,0 +1,61 @@ +To use this module, you need to + + - create your graphene schema + - create your controller to expose your GraphQL endpoint, and + optionally a GraphiQL IDE. + +This module does not attempt to expose the whole Odoo object model. This +could be the purpose of another module based on this one. We believe +however that it is preferable to expose a specific well tested endpoint +for each customer, so as to reduce coupling by knowing precisely what is +exposed and needs to be tested when upgrading Odoo. + +To start working with this module, we recommend the following approach: + + - Learn [GraphQL basics](https://graphql.org/learn/) + - Learn [graphene](https://graphene-python.org/), the python library + used to create GraphQL schemas and resolvers. + - Examine the `graphql_demo` module in this repo, copy it, adapt the + controller to suit your needs (routes, authentication methods). + - Start building your own schema and resolver. + +## Building your schema + +The schema can be built using native graphene types. An +`odoo.addons.graphql_base.types.OdooObjectType` is provided as a +convenience. It is a graphene `ObjectType` with a default attribute +resolver which: + + - converts False to None (except for Boolean types), to avoid Odoo's + weird `False` strings being rendered as json `"false"`; + - adds the user timezone to Datetime fields; + - raises an error if an attribute is absent to avoid field name typing + errors. + +## Creating GraphQL controllers + +The module provides an `odoo.addons.graphql_base.GraphQLControllerMixin` +class to help you build GraphQL controllers providing GraphiQL and/or +GraphQL endpoints. + +``` python +from odoo import http +from odoo.addons.graphql_base import GraphQLControllerMixin + +from ..schema import schema + + +class GraphQLController(http.Controller, GraphQLControllerMixin): + + # The GraphiQL route, providing an IDE for developers + @http.route("/graphiql/demo", auth="user") + def graphiql(self, **kwargs): + return self._handle_graphiql_request(schema) + + # The graphql route, for applications. + # Note csrf=False: you may want to apply extra security + # (such as origin restrictions) to this route. + @http.route("/graphql/demo", auth="user", csrf=False) + def graphql(self, **kwargs): + return self._handle_graphql_request(schema) +``` diff --git a/graphql_base/static/description/icon.png b/graphql_base/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/graphql_base/static/description/icon.png differ diff --git a/graphql_base/static/description/index.html b/graphql_base/static/description/index.html new file mode 100644 index 000000000..2ec6e9f03 --- /dev/null +++ b/graphql_base/static/description/index.html @@ -0,0 +1,491 @@ + + + + + +Graphql Base + + + +
+

Graphql Base

+ + +

Production/Stable License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This modules enables the creation of GraphQL +endpoints. In itself, it does nothing and must be used by a developer to +create the GraphQL schema and resolvers using +graphene, and expose them through a +controller. An example is available in the graphql_demo module.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to

+
    +
  • create your graphene schema
  • +
  • create your controller to expose your GraphQL endpoint, and +optionally a GraphiQL IDE.
  • +
+

This module does not attempt to expose the whole Odoo object model. This +could be the purpose of another module based on this one. We believe +however that it is preferable to expose a specific well tested endpoint +for each customer, so as to reduce coupling by knowing precisely what is +exposed and needs to be tested when upgrading Odoo.

+

To start working with this module, we recommend the following approach:

+
    +
  • Learn GraphQL basics
  • +
  • Learn graphene, the python library +used to create GraphQL schemas and resolvers.
  • +
  • Examine the graphql_demo module in this repo, copy it, adapt the +controller to suit your needs (routes, authentication methods).
  • +
  • Start building your own schema and resolver.
  • +
+
+

Building your schema

+

The schema can be built using native graphene types. An +odoo.addons.graphql_base.types.OdooObjectType is provided as a +convenience. It is a graphene ObjectType with a default attribute +resolver which:

+
    +
  • converts False to None (except for Boolean types), to avoid Odoo’s +weird False strings being rendered as json "false";
  • +
  • adds the user timezone to Datetime fields;
  • +
  • raises an error if an attribute is absent to avoid field name typing +errors.
  • +
+
+
+

Creating GraphQL controllers

+

The module provides an +odoo.addons.graphql_base.GraphQLControllerMixin class to help you +build GraphQL controllers providing GraphiQL and/or GraphQL endpoints.

+
+from odoo import http
+from odoo.addons.graphql_base import GraphQLControllerMixin
+
+from ..schema import schema
+
+
+class GraphQLController(http.Controller, GraphQLControllerMixin):
+
+    # The GraphiQL route, providing an IDE for developers
+    @http.route("/graphiql/demo", auth="user")
+    def graphiql(self, **kwargs):
+        return self._handle_graphiql_request(schema)
+
+    # The graphql route, for applications.
+    # Note csrf=False: you may want to apply extra security
+    # (such as origin restrictions) to this route.
+    @http.route("/graphql/demo", auth="user", csrf=False)
+    def graphql(self, **kwargs):
+        return self._handle_graphql_request(schema)
+
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

sbidoul

+

This module is part of the OCA/rest-framework project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/graphql_base/views/graphiql.xml b/graphql_base/views/graphiql.xml new file mode 100644 index 000000000..33b6b6f8a --- /dev/null +++ b/graphql_base/views/graphiql.xml @@ -0,0 +1,160 @@ + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..b64f49c06 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +graphene +graphql_server