I've written too many CRUD implementations for APIs with ReactJS frontends, so I thought I'd spend some time making it trivial to generate them from a datamodel, and then I can spend my time working on the more challenging parts of a product.
I'm a big believer that your database model should be entirely independent from your API model. If they're interconnected then you will have a difficult time making changes to either without upsetting your API consumers. Having a layer between the two gives you more control over making changes as you need to.
Define a datamodel, then the generator will run your chosen template with the config provided and spit out some code that should work for the datamodel specified.
See example.yaml.
Templates are directory structures within templates/$name
whereby every
directory path, and every file, will be run through the Python Jinja2
templating framework. That means you can include templating variables (or
conditionals) in file or directory names, as well as the files themselves.
Variables that are defined as of 5th Jan 2022:
- config - this is an instance of Config from the config.py file, containing the config information defined at runtime
- options - a list of options specified on the command line via
--option
At the time of writing (5th Jan 2022) there is a single template available - python-ariadne - which will take your datamodel and spit out a Flask/Ariadne backed Python application to provide a GraphQL implementation.
Each template can offer a list of different --option
flags which can be turned off or on and then acted on accordingly. For example, the backend might want to offer built-in support for exception handling via rollbar, and include code like below:
{%- if 'rollbar' in options %}
import rollbar.contrib.flask
from flask import got_request_exception
{%- endif %}
# ...
{%- if 'rollbar' in options %}
# send exceptions from `app` to rollbar, using flask's signal system.
got_request_exception.connect(rollbar.contrib.flask.report_exception, app)
{%- endif %}
➜ ~ git clone [email protected]:ironslob/rapid-api.git
Cloning into 'rapid-api'...
remote: Enumerating objects: 143, done.
remote: Counting objects: 100% (143/143), done.
remote: Compressing objects: 100% (82/82), done.
remote: Total 143 (delta 58), reused 129 (delta 44), pack-reused 0
Receiving objects: 100% (143/143), 26.92 KiB | 362.00 KiB/s, done.
Resolving deltas: 100% (58/58), done.
➜ ~ cd rapid-api
➜ rapid-api git:(main) python3 -m venv venv
➜ rapid-api git:(main) ✗ source venv/bin/activate
(venv) ➜ rapid-api git:(main) ✗ pip install -r requirements.txt
Collecting click==8.0.3
Using cached click-8.0.3-py3-none-any.whl (97 kB)
Collecting Jinja2==3.0.3
Using cached Jinja2-3.0.3-py3-none-any.whl (133 kB)
Collecting MarkupSafe==2.0.1
Using cached MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl (30 kB)
Collecting pydantic==1.8.2
Using cached pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl (13.7 MB)
Collecting PyYAML==6.0
Using cached PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (701 kB)
Collecting typing-extensions==4.0.1
Using cached typing_extensions-4.0.1-py3-none-any.whl (22 kB)
Installing collected packages: click, MarkupSafe, Jinja2, typing-extensions, pydantic, PyYAML
Successfully installed Jinja2-3.0.3 MarkupSafe-2.0.1 PyYAML-6.0 click-8.0.3 pydantic-1.8.2 typing-extensions-4.0.1
There's an example.yaml file provided which will create a datamodel for a simple restaurant review system. There is no authentication or authorisation, just the CRUD work. Run it like so:
➜ rapid-graphql git:(main) ✗ python3 generator.py --config example.yaml --template templates/python-ariadne --output test
Generating test/schema.graphql
Generating test/wsgi.py
Generating test/docker-compose.yml
Generating test/internal/constants.py
Generating test/internal/db/session.py
Generating test/internal/db/models.py
Generating test/internal/app/nocache.py
Generating test/internal/app/schema.py
Generating test/internal/app/instance.py
Generating test/internal/app/query.py
Generating test/internal/app/mutation.py
Generating test/internal/app/graphql.py
Generating test/internal/app/resolver.py
Generating test/templates/404.html
➜ rapid-graphql git:(main) ✗
A directory called test
will be created and contain the code for the generated API.
If you'd like to clean it up, I suggest running the code through black(1)
to make it nicer.
$ black test
We have to do several things:
- Create a Python virtual environment
- Run Postgres via docker-compose file
- Install dependencies in it
- Run the server
➜ rapid-graphql git:(main) ✗ cd test
➜ test git:(main) ✗ python3 -m venv env
➜ test git:(main) ✗ source env/bin/activate
(env) ➜ test git:(main) ✗ docker-compose up -d
Starting test_db_1 ... done
(env) ➜ test git:(main) ✗ pip install -r requirements.txt
...
(env) ➜ test git:(main) ✗ DATABASE_URL="postgresql+psycopg2://postgres:postgres@db:5433/db_test" PYTHONPATH=. python3 -c 'from internal.db import session, models; models.Base.metadata.create_all(session.engine)'
(env) ➜ test git:(main) ✗ DATABASE_URL="postgresql+psycopg2://postgres:postgres@db:5433/db_test" FLASK_APP=wsgi flask run --reload --without-threads
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
INFO:werkzeug: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
INFO:werkzeug: * Restarting with stat
We're going to issue a simple command - get me all reviews, and corresponding restaurant ids and basic user info:
curl 'http://localhost:5000/graphql' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:5000' --data-binary '{"query":"query {\n getReview {\n data {\n reviewId\n rating\n restaurant{\n restaurantId\n }\n user {\n userId\n username\n }\n }\n }\n}"}' --compressed